Spaces:
Sleeping
Sleeping
File size: 5,128 Bytes
1e6a9db |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
/**
* 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>
);
}
|