/** * @license * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useMemo } from 'react'; import { QuickLink, User } from '../types'; import { LinkIcon, Plus, ExternalLink, Pencil, Trash2, Save, X, Search, Globe, FolderOpen, Star } from 'lucide-react'; interface LinkDashboardProps { links: QuickLink[]; currentUser: User; onAddLink: (link: Omit) => void; onUpdateLink: (link: QuickLink) => void; onDeleteLink: (id: string) => void; } // Accent palette - keys are stored in the DB so they survive reloads. const ACCENTS: Record = { emerald: { ring: 'hover:border-emerald-500/60', text: 'text-emerald-400', bg: 'bg-emerald-950/40', dot: 'bg-emerald-500', bar: 'bg-emerald-500' }, cyan: { ring: 'hover:border-cyan-500/60', text: 'text-cyan-400', bg: 'bg-cyan-950/40', dot: 'bg-cyan-500', bar: 'bg-cyan-500' }, indigo: { ring: 'hover:border-indigo-500/60', text: 'text-indigo-400', bg: 'bg-indigo-950/40', dot: 'bg-indigo-500', bar: 'bg-indigo-500' }, amber: { ring: 'hover:border-amber-500/60', text: 'text-amber-400', bg: 'bg-amber-950/40', dot: 'bg-amber-500', bar: 'bg-amber-500' }, rose: { ring: 'hover:border-rose-500/60', text: 'text-rose-400', bg: 'bg-rose-950/40', dot: 'bg-rose-500', bar: 'bg-rose-500' }, violet: { ring: 'hover:border-violet-500/60', text: 'text-violet-400', bg: 'bg-violet-950/40', dot: 'bg-violet-500', bar: 'bg-violet-500' }, }; const ACCENT_KEYS = Object.keys(ACCENTS); const accent = (key: string) => ACCENTS[key] ?? ACCENTS.emerald; function hostOf(url: string): string { try { return new URL(url).host; } catch { return url.replace(/^https?:\/\//, '').split('/')[0]; } } type Draft = { title: string; url: string; description: string; category: string; color: string }; const EMPTY_DRAFT: Draft = { title: '', url: '', description: '', category: '', color: 'emerald' }; export default function LinkDashboard({ links, currentUser, onAddLink, onUpdateLink, onDeleteLink }: LinkDashboardProps) { const [search, setSearch] = useState(''); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [draft, setDraft] = useState(EMPTY_DRAFT); const [activeCategory, setActiveCategory] = useState('all'); const [editingDescId, setEditingDescId] = useState(null); const [descDraft, setDescDraft] = useState(''); const categories = useMemo(() => { const set = new Set(); links.forEach(l => { if (l.category?.trim()) set.add(l.category.trim()); }); return Array.from(set).sort(); }, [links]); const filtered = useMemo(() => { const q = search.toLowerCase(); return links.filter(l => { const matchesSearch = !q || l.title.toLowerCase().includes(q) || l.description.toLowerCase().includes(q) || l.url.toLowerCase().includes(q) || l.category.toLowerCase().includes(q); const matchesCat = activeCategory === 'all' || (activeCategory === '__uncat' ? !l.category?.trim() : l.category === activeCategory); return matchesSearch && matchesCat; }); }, [links, search, activeCategory]); // Group filtered links by category for a tidy board layout const grouped = useMemo(() => { const map = new Map(); filtered.forEach(l => { const key = l.category?.trim() || 'Uncategorized'; if (!map.has(key)) map.set(key, []); map.get(key)!.push(l); }); return Array.from(map.entries()).sort((a, b) => { if (a[0] === 'Uncategorized') return 1; if (b[0] === 'Uncategorized') return -1; return a[0].localeCompare(b[0]); }); }, [filtered]); const openAdd = () => { setEditingId(null); setDraft({ ...EMPTY_DRAFT, category: activeCategory !== 'all' && activeCategory !== '__uncat' ? activeCategory : '' }); setShowForm(true); }; const openEdit = (link: QuickLink) => { setEditingId(link.id); setDraft({ title: link.title, url: link.url, description: link.description, category: link.category, color: link.color }); setShowForm(true); }; const closeForm = () => { setShowForm(false); setEditingId(null); setDraft(EMPTY_DRAFT); }; const startDescEdit = (link: QuickLink) => { setEditingDescId(link.id); setDescDraft(link.description); }; const commitDescEdit = (link: QuickLink) => { onUpdateLink({ ...link, description: descDraft.trim() }); setEditingDescId(null); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const title = draft.title.trim(); let url = draft.url.trim(); if (!title || !url) return; // Be forgiving - assume https:// if no scheme was typed. if (!/^https?:\/\//i.test(url)) url = `https://${url}`; const payload = { title, url, description: draft.description.trim(), category: draft.category.trim(), color: draft.color, }; if (editingId) { const original = links.find(l => l.id === editingId); if (original) onUpdateLink({ ...original, ...payload }); } else { onAddLink(payload); } closeForm(); }; return (