Spaces:
Sleeping
Sleeping
| /** | |
| * T078: Note viewer with rendered markdown, metadata, and backlinks | |
| * T081-T082: Wikilink click handling and broken link styling | |
| */ | |
| import { useMemo } from 'react'; | |
| import ReactMarkdown from 'react-markdown'; | |
| import remarkGfm from 'remark-gfm'; | |
| import { Edit, Trash2, Calendar, Tag as TagIcon, ArrowLeft } from 'lucide-react'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Button } from '@/components/ui/button'; | |
| import { ScrollArea } from '@/components/ui/scroll-area'; | |
| import { Separator } from '@/components/ui/separator'; | |
| import type { Note } from '@/types/note'; | |
| import type { BacklinkResult } from '@/services/api'; | |
| import { createWikilinkComponent } from '@/lib/markdown.tsx'; | |
| interface NoteViewerProps { | |
| note: Note; | |
| backlinks: BacklinkResult[]; | |
| onEdit?: () => void; | |
| onDelete?: () => void; | |
| onWikilinkClick: (linkText: string) => void; | |
| } | |
| export function NoteViewer({ | |
| note, | |
| backlinks, | |
| onEdit, | |
| onDelete, | |
| onWikilinkClick, | |
| }: NoteViewerProps) { | |
| // Create custom markdown components with wikilink handler | |
| const markdownComponents = useMemo( | |
| () => createWikilinkComponent(onWikilinkClick), | |
| [onWikilinkClick] | |
| ); | |
| const formatDate = (dateString: string) => { | |
| return new Date(dateString).toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| }); | |
| }; | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| {/* Header */} | |
| <div className="border-b border-border p-4"> | |
| <div className="flex items-start justify-between gap-4"> | |
| <div className="flex-1 min-w-0"> | |
| <h1 className="text-3xl font-bold truncate">{note.title}</h1> | |
| <p className="text-sm text-muted-foreground mt-1">{note.note_path}</p> | |
| </div> | |
| <div className="flex gap-2"> | |
| {onEdit && ( | |
| <Button variant="outline" size="sm" onClick={onEdit}> | |
| <Edit className="h-4 w-4 mr-2" /> | |
| Edit | |
| </Button> | |
| )} | |
| {onDelete && ( | |
| <Button variant="outline" size="sm" onClick={onDelete}> | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <ScrollArea className="flex-1 p-6"> | |
| <div className="prose prose-slate dark:prose-invert max-w-none"> | |
| <ReactMarkdown | |
| remarkPlugins={[remarkGfm]} | |
| components={markdownComponents} | |
| > | |
| {note.body} | |
| </ReactMarkdown> | |
| </div> | |
| <Separator className="my-8" /> | |
| {/* Metadata Footer */} | |
| <div className="space-y-4 text-sm"> | |
| {/* Tags */} | |
| {note.metadata.tags && note.metadata.tags.length > 0 && ( | |
| <div className="flex items-start gap-2"> | |
| <TagIcon className="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" /> | |
| <div className="flex flex-wrap gap-2"> | |
| {note.metadata.tags.map((tag) => ( | |
| <Badge key={tag} variant="secondary"> | |
| {tag} | |
| </Badge> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Timestamps */} | |
| <div className="flex items-center gap-4 text-muted-foreground"> | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-4 w-4" /> | |
| <span>Created: {formatDate(note.created)}</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Calendar className="h-4 w-4" /> | |
| <span>Updated: {formatDate(note.updated)}</span> | |
| </div> | |
| </div> | |
| {/* Backlinks */} | |
| {backlinks.length > 0 && ( | |
| <> | |
| <Separator className="my-4" /> | |
| <div> | |
| <div className="flex items-center gap-2 mb-3"> | |
| <ArrowLeft className="h-4 w-4 text-muted-foreground" /> | |
| <h3 className="font-semibold"> | |
| Backlinks ({backlinks.length}) | |
| </h3> | |
| </div> | |
| <div className="space-y-2 ml-6"> | |
| {backlinks.map((backlink) => ( | |
| <button | |
| key={backlink.note_path} | |
| className="block text-left text-primary hover:underline" | |
| onClick={() => onWikilinkClick(backlink.title)} | |
| > | |
| • {backlink.title} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| {/* Additional metadata */} | |
| {note.metadata.project && ( | |
| <div className="text-muted-foreground"> | |
| Project: <span className="font-medium">{note.metadata.project}</span> | |
| </div> | |
| )} | |
| <div className="text-xs text-muted-foreground"> | |
| Version: {note.version} • Size: {(note.size_bytes / 1024).toFixed(1)} KB | |
| </div> | |
| </div> | |
| </ScrollArea> | |
| </div> | |
| ); | |
| } | |