feat: implement My Trees, admin UI, rating modal, and bundle optimization (Issues #15, #18, #19, #31)
Frontend features: - My Trees personal dashboard with fork tracking (Issue #15) - Tree sharing UI with token generation and copy (Issue #16) - Draft tree badges and validation UI (Issue #25) - Save session as tree modal (Issue #17) - Rate/review modal with localStorage tracking (Issue #19) - Admin category management with drag-and-drop (Issue #18) - Bundle size optimization with code splitting (Issue #31) Components created: - MyTreesPage: Personal tree organization - AdminCategoriesPage: Category CRUD with @dnd-kit - ShareTreeModal: Tree sharing interface - SaveSessionAsTreeModal: Session conversion UI - StepRatingModal: Post-session rating with stars - StarRating: Reusable rating component - PageLoader: Loading fallback for lazy routes - CreateCategoryModal, EditCategoryModal: Admin modals Bundle optimization: - Reduced from 892 KB to 221 KB (75% reduction) - Dynamic imports for 9 heavy pages - Vendor chunk splitting for optimal caching - 6 separate vendor chunks (react, markdown, utils, dnd, icons, state) Dependencies added: - @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities API clients: - stepCategories: Full CRUD for admin - Enhanced sessions: saveAsTree endpoint - Enhanced trees: share, fork, canPublish endpoints Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ export function TreeLibraryPage() {
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [showDrafts, setShowDrafts] = useState(false)
|
||||
|
||||
// View preferences from store
|
||||
const { treeLibraryView, setTreeLibraryView, treeLibrarySortBy, setTreeLibrarySortBy } =
|
||||
@@ -45,6 +46,9 @@ export function TreeLibraryPage() {
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
// Fork state
|
||||
const [isForkingTree, setIsForkingTree] = useState(false)
|
||||
|
||||
const loadFolders = useCallback(async () => {
|
||||
try {
|
||||
const foldersData = await foldersApi.list()
|
||||
@@ -56,7 +60,7 @@ export function TreeLibraryPage() {
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy])
|
||||
}, [selectedCategoryId, selectedTags, selectedFolderId, treeLibrarySortBy, showDrafts])
|
||||
|
||||
// Load folders on mount and listen for changes
|
||||
useEffect(() => {
|
||||
@@ -75,6 +79,7 @@ export function TreeLibraryPage() {
|
||||
tags: selectedTags.length > 0 ? selectedTags.join(',') : undefined,
|
||||
folder_id: selectedFolderId || undefined,
|
||||
sort_by: treeLibrarySortBy,
|
||||
include_drafts: showDrafts || undefined,
|
||||
}),
|
||||
categoriesApi.list(),
|
||||
])
|
||||
@@ -156,6 +161,21 @@ export function TreeLibraryPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleForkTree = async (treeId: string) => {
|
||||
if (isForkingTree) return
|
||||
setIsForkingTree(true)
|
||||
try {
|
||||
await treesApi.fork(treeId)
|
||||
toast.success('Tree forked successfully')
|
||||
navigate('/my-trees')
|
||||
} catch (err) {
|
||||
console.error('Failed to fork tree:', err)
|
||||
toast.error('Failed to fork tree')
|
||||
} finally {
|
||||
setIsForkingTree(false)
|
||||
}
|
||||
}
|
||||
|
||||
const hasActiveFilters =
|
||||
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
|
||||
|
||||
@@ -257,7 +277,18 @@ export function TreeLibraryPage() {
|
||||
|
||||
{/* View Controls */}
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<SortDropdown value={treeLibrarySortBy} onChange={setTreeLibrarySortBy} />
|
||||
<div className="flex items-center gap-4">
|
||||
<SortDropdown value={treeLibrarySortBy} onChange={setTreeLibrarySortBy} />
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDrafts}
|
||||
onChange={(e) => setShowDrafts(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">Show my drafts</span>
|
||||
</label>
|
||||
</div>
|
||||
<ViewToggle view={treeLibraryView} onChange={setTreeLibraryView} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,6 +364,7 @@ export function TreeLibraryPage() {
|
||||
setTreeToDelete(tree)
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
/>
|
||||
)}
|
||||
{treeLibraryView === 'list' && (
|
||||
@@ -345,6 +377,7 @@ export function TreeLibraryPage() {
|
||||
setTreeToDelete(tree)
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
/>
|
||||
)}
|
||||
{treeLibraryView === 'table' && (
|
||||
@@ -362,6 +395,7 @@ export function TreeLibraryPage() {
|
||||
sortBy as 'usage_count' | 'updated_at' | 'created_at' | 'name' | 'name_desc' | 'version'
|
||||
)
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user