Wothmag07's picture
Doc-MCP Application
1e6a9db
/**
* 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>
);
}