import { useEffect, useMemo, useState } from 'react' import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts' import { Link2, FolderOpen, Star, Plus, Server, Bot, Cloud, Network, GitBranch, GitFork, Box, Terminal as TerminalIcon, Database, Shield, Workflow, FileCode, Router, Wifi, BookOpen, GraduationCap, SquarePlay, Briefcase, Wallet, CreditCard, PiggyBank, TrendingUp, Calendar, Mail, Image, HardDrive, FileText, Plane, Sparkles, MessageSquare, Zap, Globe2, Container, X, type LucideIcon, } from 'lucide-react' import { api, ApiError, type Bookmark, type BookmarkCategory } from '../lib/api' const ICONS: Record = { server: Server, bot: Bot, cloud: Cloud, network: Network, gitbranch: GitBranch, gitfork: GitFork, box: Box, terminal: TerminalIcon, database: Database, shield: Shield, workflow: Workflow, filecode: FileCode, router: Router, wifi: Wifi, bookopen: BookOpen, graduationcap: GraduationCap, squareplay: SquarePlay, briefcase: Briefcase, wallet: Wallet, creditcard: CreditCard, piggybank: PiggyBank, trendingup: TrendingUp, calendar: Calendar, mail: Mail, image: Image, harddrive: HardDrive, filetext: FileText, plane: Plane, sparkles: Sparkles, messagesquare: MessageSquare, zap: Zap, globe2: Globe2, container: Container, link2: Link2, } function resolveIcon(name: string | null | undefined): LucideIcon { if (!name) return Link2 return ICONS[name.toLowerCase()] ?? Link2 } const statusColors: Record = { online: '#2ECC71', warning: '#E67E22', offline: '#E74C3C', unknown: '#7A7D85', } const categoryPalette = ['#C8A434', '#3B82F6', '#2ECC71', '#E67E22', '#7A7D85', '#8B5E3C', '#9B59B6', '#E74C3C'] const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', borderRadius: '12px', padding: '18px', boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)', transition: 'border-color 0.2s ease', position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column', } const sectionTitle: React.CSSProperties = { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '14px', } const inputStyle: React.CSSProperties = { backgroundColor: 'rgba(255,255,255,0.03)', border: '1px solid rgba(200,164,52,0.12)', borderRadius: '8px', padding: '9px 12px', fontSize: '13px', color: '#E8E6E0', width: '100%', outline: 'none', } function Donut({ data, centerLabel }: { data: { name: string; value: number; color: string }[]; centerLabel?: string }) { const hasData = data.some((d) => d.value > 0) return (
{hasData && ( {data.map((entry) => ( ))} )} {centerLabel && (
{centerLabel}
)}
{data.map((entry) => (
{entry.name} {entry.value}
))}
) } function LinkRow({ bookmark, onToggleFavorite }: { bookmark: Bookmark; onToggleFavorite: () => void }) { const Icon = resolveIcon(bookmark.icon) return (
{bookmark.title}
) } function AddBookmarkModal({ categories, onClose, onCreated, onCategoryCreated, }: { categories: BookmarkCategory[] onClose: () => void onCreated: (bookmark: Bookmark) => void onCategoryCreated: (category: BookmarkCategory) => void }) { const [title, setTitle] = useState('') const [url, setUrl] = useState('') const [icon, setIcon] = useState('link2') const [categoryId, setCategoryId] = useState('') const [newCategoryName, setNewCategoryName] = useState('') const [error, setError] = useState('') const [busy, setBusy] = useState(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError('') if (!title.trim() || !url.trim()) { setError('Title and URL are required') return } setBusy(true) try { let resolvedCategoryId: number | null = null if (categoryId === 'new') { if (!newCategoryName.trim()) { setError('Enter a name for the new category') setBusy(false) return } const { id } = await api.createBookmarkCategory({ name: newCategoryName.trim() }) resolvedCategoryId = id onCategoryCreated({ id, name: newCategoryName.trim(), icon: null, sort_order: 0 }) } else if (categoryId !== '') { resolvedCategoryId = categoryId } const { id } = await api.createBookmark({ title: title.trim(), url: url.trim(), icon, categoryId: resolvedCategoryId, }) onCreated({ id, category_id: resolvedCategoryId, title: title.trim(), url: url.trim(), icon, favorite: 0, status: 'unknown', last_checked_at: null, created_at: new Date().toISOString(), }) onClose() } catch (err) { setError(err instanceof ApiError ? err.message : 'Failed to create bookmark') } finally { setBusy(false) } } return (
e.stopPropagation()} onSubmit={handleSubmit} style={{ ...cardBase, width: '420px', padding: '24px', gap: '14px' }} >

Add Bookmark

setTitle(e.target.value)} placeholder="e.g. Proxmox" />
setUrl(e.target.value)} placeholder="https://..." />
{categoryId === 'new' && (
setNewCategoryName(e.target.value)} placeholder="e.g. Monitoring" />
)} {error &&

{error}

}
) } export default function BookNest() { const [bookmarks, setBookmarks] = useState(null) const [categories, setCategories] = useState([]) const [showAddModal, setShowAddModal] = useState(false) useEffect(() => { Promise.all([api.listBookmarks(), api.listBookmarkCategories()]).then(([b, c]) => { setBookmarks(b.bookmarks) setCategories(c.categories) }) }, []) async function toggleFavorite(bookmark: Bookmark) { const next = bookmark.favorite ? false : true setBookmarks((prev) => prev?.map((b) => (b.id === bookmark.id ? { ...b, favorite: next ? 1 : 0 } : b)) ?? prev) try { await api.updateBookmark(bookmark.id, { favorite: next }) } catch { setBookmarks((prev) => prev?.map((b) => (b.id === bookmark.id ? { ...b, favorite: bookmark.favorite } : b)) ?? prev) } } const groups = useMemo(() => { if (!bookmarks) return [] const byCategory = new Map() for (const b of bookmarks) { const key = b.category_id if (!byCategory.has(key)) byCategory.set(key, []) byCategory.get(key)!.push(b) } const named = categories .map((c) => ({ title: c.name, links: byCategory.get(c.id) ?? [] })) .filter((g) => g.links.length > 0) const uncategorized = byCategory.get(null) ?? [] return uncategorized.length > 0 ? [...named, { title: 'Uncategorized', links: uncategorized }] : named }, [bookmarks, categories]) const favorites = useMemo(() => (bookmarks ?? []).filter((b) => b.favorite), [bookmarks]) const recentlyAdded = useMemo( () => [...(bookmarks ?? [])].sort((a, b) => (a.created_at < b.created_at ? 1 : -1)).slice(0, 5), [bookmarks] ) const quickAccess = useMemo( () => [...groups] .sort((a, b) => b.links.length - a.links.length) .slice(0, 5) .map((g) => ({ label: g.title, icons: g.links.slice(0, 5).map((l) => resolveIcon(l.icon)), count: g.links.length })), [groups] ) const linkHealthData = useMemo(() => { const counts: Record = { online: 0, warning: 0, offline: 0, unknown: 0 } for (const b of bookmarks ?? []) counts[b.status in counts ? b.status : 'unknown']++ return Object.entries(counts) .filter(([, value]) => value > 0) .map(([name, value]) => ({ name: name.charAt(0).toUpperCase() + name.slice(1), value, color: statusColors[name] })) }, [bookmarks]) const categoryBreakdownData = useMemo( () => groups.map((g, i) => ({ name: g.title, value: g.links.length, color: categoryPalette[i % categoryPalette.length] })), [groups] ) if (!bookmarks) { return (

Loading bookmarks…

) } return (
{showAddModal && ( setShowAddModal(false)} onCreated={(b) => setBookmarks((prev) => [b, ...(prev ?? [])])} onCategoryCreated={(c) => setCategories((prev) => [...prev, c])} /> )}
{/* Page stats — sits directly under the hero title/subtitle, like the blueprint */}
{bookmarks.length} Links {categories.length} Categories {favorites.length} Favorites
{/* Main column */}
{/* Quick Access */}

Quick Access

{quickAccess.length > 0 ? (
{quickAccess.map((qa) => (
{qa.label}
{qa.icons.map((Icon, i) => (
))}
{qa.count} links
))}
) : (

No bookmarks yet — add your first one to get started.

)} {/* Bookmark groups grid */} {groups.length > 0 && (
{groups.map((group) => (

{group.title}

{group.links.map((link) => ( toggleFavorite(link)} /> ))}
))}
)}
{/* Right sidebar — spans both rows so Favorites reaches up near the hero, and stretches to match the main column's full height */}

Favorites

{favorites.length > 0 ? ( favorites.map((f) => { const Icon = resolveIcon(f.icon) return ( {f.title} ) }) ) : (

No favorites yet

)}

Recently Added

{recentlyAdded.length > 0 ? ( recentlyAdded.map((r) => (
{r.title} {new Date(r.created_at).toLocaleDateString()}
)) ) : (

Nothing yet

)}

Link Health

Category Breakdown

) }