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:
Michael Chihlas
2026-02-07 23:06:46 -05:00
parent c7b2c59ef6
commit 996b664ca9
30 changed files with 2973 additions and 92 deletions

View File

@@ -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}
/>
)}
</>