"use client"; import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type KeyboardEvent } from 'react'; import { Folder, Link as LinkIcon } from 'lucide-react'; import { Node } from '@/types/database'; import { openExternalUrl } from '@/utils/openExternalUrl'; interface NodeWithMetrics extends Node { edge_count?: number; } interface AppliedFilters { search?: string; dimensions?: string[]; sortBy: 'updated' | 'edges'; } interface PopularDimension { dimension: string; count: number; isPriority: boolean; } const LIMIT = 50; export default function DatabaseViewer() { const [nodes, setNodes] = useState([]); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchInput, setSearchInput] = useState(''); const [dimensionInput, setDimensionInput] = useState(''); const [dimensionFilters, setDimensionFilters] = useState([]); const [sortBy, setSortBy] = useState<'updated' | 'edges'>('updated'); const [appliedFilters, setAppliedFilters] = useState({ sortBy: 'updated' }); const [lockedDimensionSet, setLockedDimensionSet] = useState>(new Set()); const [contextHubIds, setContextHubIds] = useState>(new Set()); const filtersActive = useMemo( () => Boolean(appliedFilters.search || (appliedFilters.dimensions && appliedFilters.dimensions.length > 0)), [appliedFilters] ); const fetchNodes = useCallback( async (pageNumber: number, filters: AppliedFilters) => { setLoading(true); setError(null); try { const params = new URLSearchParams(); params.set('limit', LIMIT.toString()); params.set('offset', ((pageNumber - 1) * LIMIT).toString()); params.set('sortBy', filters.sortBy); if (filters.search) params.set('search', filters.search); if (filters.dimensions && filters.dimensions.length > 0) { params.set('dimensions', filters.dimensions.join(',')); } const response = await fetch(`/api/nodes?${params.toString()}`); if (!response.ok) { throw new Error('Failed to fetch nodes'); } const data = await response.json(); setNodes(data.data || []); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); setNodes([]); } finally { setLoading(false); } }, [] ); useEffect(() => { fetchNodes(page, appliedFilters); }, [page, appliedFilters, fetchNodes]); useEffect(() => { const loadLockedDimensions = async () => { try { const response = await fetch('/api/dimensions/popular'); if (!response.ok) return; const result = await response.json(); if (result.success) { const priorityDimensions: PopularDimension[] = result.data; setLockedDimensionSet(new Set(priorityDimensions.filter((d) => d.isPriority).map((d) => d.dimension))); } } catch (err) { console.warn('Failed to load locked dimensions', err); } }; loadLockedDimensions(); }, []); useEffect(() => { const loadContextHubs = async () => { try { const response = await fetch('/api/nodes?sortBy=edges&limit=10'); if (!response.ok) return; const payload = await response.json(); const ids = new Set((payload.data || []).map((node: Node) => node.id)); setContextHubIds(ids); } catch (err) { console.warn('Failed to load auto-context hubs', err); } }; loadContextHubs(); }, []); const handleApplyFilters = () => { const payload: AppliedFilters = { sortBy, }; if (searchInput.trim()) payload.search = searchInput.trim(); if (dimensionFilters.length > 0) payload.dimensions = dimensionFilters; setAppliedFilters(payload); setPage(1); }; const handleClearFilters = () => { setSearchInput(''); setDimensionInput(''); setDimensionFilters([]); setSortBy('updated'); setAppliedFilters({ sortBy: 'updated' }); setPage(1); }; const handleAddDimension = () => { const next = dimensionInput.trim(); if (!next) return; setDimensionFilters((prev) => (prev.includes(next) ? prev : [...prev, next])); setDimensionInput(''); }; const handleDimensionKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { event.preventDefault(); handleAddDimension(); } }; const handleRemoveDimension = (dimension: string) => { setDimensionFilters((prev) => prev.filter((dim) => dim !== dimension)); }; const handleSortChange = (event: ChangeEvent) => { const value = event.target.value === 'edges' ? 'edges' : 'updated'; setSortBy(value); setAppliedFilters((prev) => ({ ...prev, sortBy: value })); }; const handlePrevious = () => { setPage((prev) => (prev > 1 ? prev - 1 : prev)); }; const handleNext = () => { if (nodes.length === LIMIT) { setPage((prev) => prev + 1); } }; const isFirstPage = page === 1; const isLastPage = nodes.length < LIMIT; const filterStatus = filtersActive ? 'Filtered results' : `Showing ${(page - 1) * LIMIT + 1}-${(page - 1) * LIMIT + nodes.length}`; const formatTimestamp = (value?: string) => { if (!value) return '—'; const date = new Date(value); return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const formatRelative = (value?: string) => { if (!value) return ''; const diffMs = Date.now() - new Date(value).getTime(); const diffMinutes = Math.round(diffMs / (1000 * 60)); if (diffMinutes < 60) return `${diffMinutes}m ago`; const diffHours = Math.round(diffMinutes / 60); if (diffHours < 24) return `${diffHours}h ago`; const diffDays = Math.round(diffHours / 24); return `${diffDays}d ago`; }; if (loading) { return (
Loading database...
); } if (error) { return (
Error: {error}
); } if (nodes.length === 0) { return (
No nodes found
); } return (
{dimensionFilters.length > 0 && (
{dimensionFilters.map((dimension) => ( {dimension} ))}
)}
{filterStatus}
{['Node', 'Categories', 'Edges', 'Highlights', 'Updated', 'Created'].map((column) => ( ))} {nodes.map((node, index) => { const belongsToLocked = node.dimensions?.some((dimension) => lockedDimensionSet.has(dimension)); return ( ); })}
{column}
{node.title || 'Untitled node'} {node.link && ( )}
ID: {node.id}
{node.dimensions && node.dimensions.length > 0 ? ( node.dimensions.slice(0, 3).map((dimension) => ( {dimension} )) ) : ( No categories )} {node.dimensions && node.dimensions.length > 3 && ( +{node.dimensions.length - 3} more )}
{node.edge_count ?? 0}
connections
{contextHubIds.has(node.id) ? ( Auto-context hub ) : ( )} {belongsToLocked && ( Priority dimension )}
{formatTimestamp(node.updated_at)}
{formatRelative(node.updated_at)}
{formatTimestamp(node.created_at)}
{formatRelative(node.created_at)}
); }