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, Pencil, Trash2, type LucideIcon, } from 'lucide-react' import { api, ApiError, type Bookmark, type BookmarkCategory } from '../lib/api' import { guessServiceIconUrl } from '../lib/serviceIcons' 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 } function isIconUrl(icon: string | null | undefined): boolean { return !!icon && /^https?:\/\//.test(icon) } /** Renders a bookmark's icon — a real brand logo if `icon` is a CDN URL, falling back to the generic lucide set (and to a plain link icon if the image 404s). */ function BookmarkIcon({ icon, size = 16, style }: { icon: string | null | undefined; size?: number; style?: React.CSSProperties }) { const [imgFailed, setImgFailed] = useState(false) if (isIconUrl(icon) && !imgFailed) { return ( setImgFailed(true)} /> ) } const Icon = resolveIcon(imgFailed ? null : icon) return } 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, onEdit, onDelete, }: { bookmark: Bookmark onToggleFavorite: () => void onEdit: () => void onDelete: () => void }) { return (
{bookmark.title}
) } function BookmarkModal({ categories, bookmark, onClose, onCreated, onUpdated, onCategoryCreated, }: { categories: BookmarkCategory[] bookmark?: Bookmark onClose: () => void onCreated: (bookmark: Bookmark) => void onUpdated: (bookmark: Bookmark) => void onCategoryCreated: (category: BookmarkCategory) => void }) { const isEdit = !!bookmark const [title, setTitle] = useState(bookmark?.title ?? '') const [url, setUrl] = useState(bookmark?.url ?? '') const [icon, setIcon] = useState(bookmark?.icon ?? 'link2') const [iconMode, setIconMode] = useState<'auto' | 'manual'>(isIconUrl(bookmark?.icon) || !bookmark ? 'auto' : 'manual') const [categoryId, setCategoryId] = useState(bookmark?.category_id ?? '') const [newCategoryName, setNewCategoryName] = useState('') const [error, setError] = useState('') const [busy, setBusy] = useState(false) useEffect(() => { if (iconMode !== 'auto') return setIcon(guessServiceIconUrl(title, url) ?? 'link2') }, [title, url, iconMode]) 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 } if (isEdit && bookmark) { await api.updateBookmark(bookmark.id, { title: title.trim(), url: url.trim(), icon, categoryId: resolvedCategoryId, }) onUpdated({ ...bookmark, title: title.trim(), url: url.trim(), icon, category_id: resolvedCategoryId }) } else { 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 ${isEdit ? 'update' : 'create'} bookmark`) } finally { setBusy(false) } } return (
e.stopPropagation()} onSubmit={handleSubmit} style={{ ...cardBase, width: '420px', padding: '24px', gap: '14px' }} >

{isEdit ? 'Edit Bookmark' : 'Add Bookmark'}

setTitle(e.target.value)} placeholder="e.g. Proxmox" />
setUrl(e.target.value)} placeholder="https://..." />
{iconMode === 'manual' && ( )}
{iconMode === 'auto' ? (
{isIconUrl(icon) ? 'Detected from title/URL' : 'No match yet — type a title or pick one'}
) : ( )}
{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) const [editingBookmark, setEditingBookmark] = useState(null) useEffect(() => { Promise.all([api.listBookmarks(), api.listBookmarkCategories()]).then(([b, c]) => { setBookmarks(b.bookmarks) setCategories(c.categories) }) }, []) async function deleteBookmark(bookmark: Bookmark) { if (!window.confirm(`Delete "${bookmark.title}"?`)) return setBookmarks((prev) => prev?.filter((b) => b.id !== bookmark.id) ?? prev) try { await api.deleteBookmark(bookmark.id) } catch { setBookmarks((prev) => (prev ? [...prev, bookmark] : prev)) } } async function deleteAllBookmarks() { const count = bookmarks?.length ?? 0 if (count === 0) return if (!window.confirm(`Delete all ${count} bookmark${count === 1 ? '' : 's'}? This cannot be undone.`)) return const prevBookmarks = bookmarks setBookmarks([]) try { await api.deleteAllBookmarks() } catch { setBookmarks(prevBookmarks) } } 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) => 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 || editingBookmark) && ( { setShowAddModal(false); setEditingBookmark(null) }} onCreated={(b) => setBookmarks((prev) => [b, ...(prev ?? [])])} onUpdated={(b) => setBookmarks((prev) => prev?.map((x) => (x.id === b.id ? b : x)) ?? 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
{bookmarks.length > 0 && ( )}
{/* 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)} onEdit={() => setEditingBookmark(link)} onDelete={() => deleteBookmark(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) => ( {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

) }