import { useState, useEffect, useCallback, useRef } from 'react' import { ExternalLink, Star, Search } from 'lucide-react' import { Button } from '@/components/ui/Button' import { PageHeader } from '@/components/admin' import { adminApi } from '@/api/admin' import type { GalleryFlowItem, GalleryScriptItem } from '@/api/admin' import { toast } from '@/lib/toast' import { cn } from '@/lib/utils' // --------------------------------------------------------------------------- // Toggle switch component // --------------------------------------------------------------------------- function ToggleSwitch({ checked, onChange, disabled, }: { checked: boolean onChange: (value: boolean) => void disabled?: boolean }) { return ( ) } // --------------------------------------------------------------------------- // Sort order input // --------------------------------------------------------------------------- function SortOrderInput({ value, onSave, disabled, }: { value: number onSave: (v: number) => void disabled?: boolean }) { const [local, setLocal] = useState(String(value)) const ref = useRef(null) // Sync if external value changes useEffect(() => { setLocal(String(value)) }, [value]) const commit = () => { const parsed = parseInt(local, 10) if (!isNaN(parsed) && parsed !== value) { onSave(parsed) } else { setLocal(String(value)) // revert invalid input } } return ( setLocal(e.target.value)} onBlur={commit} onKeyDown={e => { if (e.key === 'Enter') { commit() ref.current?.blur() } }} className={cn( 'w-20 rounded-[8px] border border-[#1e2130] bg-[#14161d] px-2 py-1 text-sm text-[#e2e5eb]', 'focus:border-[rgba(6,182,212,0.3)] focus:outline-none', disabled && 'cursor-not-allowed opacity-50', )} /> ) } // --------------------------------------------------------------------------- // Filter bar // --------------------------------------------------------------------------- type FilterMode = 'all' | 'featured' | 'unfeatured' function FilterBar({ search, onSearchChange, filter, onFilterChange, }: { search: string onSearchChange: (v: string) => void filter: FilterMode onFilterChange: (v: FilterMode) => void }) { return (
onSearchChange(e.target.value)} className={cn( 'w-full rounded-lg border border-[#1e2130] bg-[#14161d] pl-9 pr-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b]', 'focus:border-[rgba(6,182,212,0.3)] focus:outline-none', )} />
{(['all', 'featured', 'unfeatured'] as FilterMode[]).map(mode => ( ))}
) } // --------------------------------------------------------------------------- // Flows table // --------------------------------------------------------------------------- function FlowsTable({ flows, onToggleFeatured, onSortOrder, toggling, }: { flows: GalleryFlowItem[] onToggleFeatured: (id: string, value: boolean) => void onSortOrder: (id: string, value: number) => void toggling: Set }) { if (flows.length === 0) { return (

No flows match the current filter.

) } return (
{flows.map(flow => ( ))}
Name Type Featured Sort Order
{flow.name} {flow.tree_type}
onToggleFeatured(flow.id, v)} disabled={toggling.has(flow.id)} /> {flow.is_gallery_featured && ( )}
onSortOrder(flow.id, v)} disabled={toggling.has(flow.id)} />
) } // --------------------------------------------------------------------------- // Scripts table // --------------------------------------------------------------------------- function ScriptsTable({ scripts, onToggleFeatured, onSortOrder, toggling, }: { scripts: GalleryScriptItem[] onToggleFeatured: (id: string, value: boolean) => void onSortOrder: (id: string, value: number) => void toggling: Set }) { if (scripts.length === 0) { return (

No scripts match the current filter.

) } return (
{scripts.map(script => ( ))}
Name Status Featured Sort Order
{script.name} {script.is_active ? 'Active' : 'Inactive'}
onToggleFeatured(script.id, v)} disabled={toggling.has(script.id)} /> {script.is_gallery_featured && ( )}
onSortOrder(script.id, v)} disabled={toggling.has(script.id)} />
) } // --------------------------------------------------------------------------- // Main page // --------------------------------------------------------------------------- export default function GalleryManagementPage() { const [flows, setFlows] = useState([]) const [scripts, setScripts] = useState([]) const [loading, setLoading] = useState(true) // Per-table filter state const [flowSearch, setFlowSearch] = useState('') const [flowFilter, setFlowFilter] = useState('all') const [scriptSearch, setScriptSearch] = useState('') const [scriptFilter, setScriptFilter] = useState('all') // Track in-flight requests per item const [toggling, setToggling] = useState>(new Set()) const fetchData = useCallback(async () => { setLoading(true) try { const data = await adminApi.getGalleryAllItems() setFlows(data.flows) setScripts(data.scripts) } catch { toast.error('Failed to load gallery items') } finally { setLoading(false) } }, []) useEffect(() => { fetchData() }, [fetchData]) // Flow actions const handleToggleFlow = async (id: string, value: boolean) => { setToggling(prev => new Set(prev).add(id)) try { const updated = await adminApi.toggleFlowFeatured(id, value) setFlows(prev => prev.map(f => f.id === id ? { ...f, ...updated } : f)) toast.success(value ? 'Flow added to gallery' : 'Flow removed from gallery') } catch { toast.error('Failed to update flow') } finally { setToggling(prev => { const s = new Set(prev); s.delete(id); return s }) } } const handleFlowSortOrder = async (id: string, value: number) => { setToggling(prev => new Set(prev).add(id)) try { const updated = await adminApi.updateFlowSortOrder(id, value) setFlows(prev => prev.map(f => f.id === id ? { ...f, ...updated } : f)) } catch { toast.error('Failed to update sort order') } finally { setToggling(prev => { const s = new Set(prev); s.delete(id); return s }) } } // Script actions const handleToggleScript = async (id: string, value: boolean) => { setToggling(prev => new Set(prev).add(id)) try { const updated = await adminApi.toggleScriptFeatured(id, value) setScripts(prev => prev.map(s => s.id === id ? { ...s, ...updated } : s)) toast.success(value ? 'Script added to gallery' : 'Script removed from gallery') } catch { toast.error('Failed to update script') } finally { setToggling(prev => { const s = new Set(prev); s.delete(id); return s }) } } const handleScriptSortOrder = async (id: string, value: number) => { setToggling(prev => new Set(prev).add(id)) try { const updated = await adminApi.updateScriptSortOrder(id, value) setScripts(prev => prev.map(s => s.id === id ? { ...s, ...updated } : s)) } catch { toast.error('Failed to update sort order') } finally { setToggling(prev => { const s = new Set(prev); s.delete(id); return s }) } } // Filtering const filteredFlows = flows.filter(f => { const matchesSearch = f.name.toLowerCase().includes(flowSearch.toLowerCase()) const matchesFilter = flowFilter === 'all' || (flowFilter === 'featured' && f.is_gallery_featured) || (flowFilter === 'unfeatured' && !f.is_gallery_featured) return matchesSearch && matchesFilter }) const filteredScripts = scripts.filter(s => { const matchesSearch = s.name.toLowerCase().includes(scriptSearch.toLowerCase()) const matchesFilter = scriptFilter === 'all' || (scriptFilter === 'featured' && s.is_gallery_featured) || (scriptFilter === 'unfeatured' && !s.is_gallery_featured) return matchesSearch && matchesFilter }) const featuredFlowCount = flows.filter(f => f.is_gallery_featured).length const featuredScriptCount = scripts.filter(s => s.is_gallery_featured).length return (
window.open('/templates', '_blank')} > Preview Gallery } /> {loading ? (
Loading gallery items…
) : (
{/* Summary stats */}
{featuredFlowCount} featured flow{featuredFlowCount !== 1 ? 's' : ''}
{featuredScriptCount} featured script{featuredScriptCount !== 1 ? 's' : ''}
{/* Featured Flows section */}

Featured Flows

Toggle flows to show in the gallery. Lower sort order = shown first.

{/* Featured Scripts section */}

Featured Scripts

Toggle scripts to show in the gallery. Lower sort order = shown first.

)}
) }