"use client"; import type { ReactElement } from "react"; import React, { useState, useEffect, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; import type { KnowledgeEntryWithTags, KnowledgeTag, KnowledgeBacklink } from "@mosaic/shared"; import { EntryStatus, Visibility } from "@mosaic/shared"; import { EntryViewer } from "@/components/knowledge/EntryViewer"; import { EntryEditor } from "@/components/knowledge/EntryEditor"; import { EntryMetadata } from "@/components/knowledge/EntryMetadata"; import { VersionHistory } from "@/components/knowledge/VersionHistory"; import { BacklinksList } from "@/components/knowledge/BacklinksList"; import { fetchEntry, updateEntry, deleteEntry, fetchTags, fetchBacklinks, } from "@/lib/api/knowledge"; /** * Knowledge Entry Detail/Editor Page * View and edit mode for a single knowledge entry */ export default function EntryPage(): ReactElement { const router = useRouter(); const params = useParams(); const slug = params.slug as string; const [entry, setEntry] = useState(null); const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); // Backlinks state const [backlinks, setBacklinks] = useState([]); const [backlinksLoading, setBacklinksLoading] = useState(false); const [backlinksError, setBacklinksError] = useState(null); // Edit state const [editTitle, setEditTitle] = useState(""); const [editContent, setEditContent] = useState(""); const [editStatus, setEditStatus] = useState(EntryStatus.DRAFT); const [editVisibility, setEditVisibility] = useState(Visibility.WORKSPACE); const [editTags, setEditTags] = useState([]); const [availableTags, setAvailableTags] = useState([]); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [activeTab, setActiveTab] = useState<"content" | "history">("content"); // Load entry data useEffect(() => { async function loadEntry(): Promise { try { setIsLoading(true); const data = await fetchEntry(slug); setEntry(data); setEditTitle(data.title); setEditContent(data.content); setEditStatus(data.status); setEditVisibility(data.visibility); setEditTags(data.tags.map((tag) => tag.id)); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load entry"); } finally { setIsLoading(false); } } void loadEntry(); }, [slug]); // Load backlinks useEffect(() => { async function loadBacklinks(): Promise { try { setBacklinksLoading(true); setBacklinksError(null); const data = await fetchBacklinks(slug); setBacklinks(data.backlinks); } catch (err) { setBacklinksError(err instanceof Error ? err.message : "Failed to load backlinks"); } finally { setBacklinksLoading(false); } } void loadBacklinks(); }, [slug]); // Load available tags useEffect(() => { async function loadTags(): Promise { try { const tags = await fetchTags(); setAvailableTags(tags); } catch (err) { console.error("Failed to load tags:", err); } } void loadTags(); }, []); // Track unsaved changes useEffect(() => { if (!entry || !isEditing) { setHasUnsavedChanges(false); return; } const changed = editTitle !== entry.title || editContent !== entry.content || editStatus !== entry.status || editVisibility !== entry.visibility || JSON.stringify(editTags.sort()) !== JSON.stringify(entry.tags.map((t) => t.id).sort()); setHasUnsavedChanges(changed); }, [entry, isEditing, editTitle, editContent, editStatus, editVisibility, editTags]); // Warn before leaving with unsaved changes useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent): string => { if (hasUnsavedChanges) { e.preventDefault(); return "You have unsaved changes. Are you sure you want to leave?"; } return ""; }; window.addEventListener("beforeunload", handleBeforeUnload); return (): void => { window.removeEventListener("beforeunload", handleBeforeUnload); }; }, [hasUnsavedChanges]); // Save changes const handleSave = useCallback(async (): Promise => { if (!entry || isSaving || !editTitle.trim() || !editContent.trim()) { return; } setIsSaving(true); setError(null); try { const updated = await updateEntry(slug, { title: editTitle.trim(), content: editContent.trim(), status: editStatus, visibility: editVisibility, tags: editTags, }); setEntry(updated); setHasUnsavedChanges(false); setIsEditing(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save changes"); } finally { setIsSaving(false); } }, [entry, slug, editTitle, editContent, editStatus, editVisibility, editTags, isSaving]); // Cmd+S / Ctrl+S to save useEffect(() => { const handleKeyDown = (e: KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key === "s" && isEditing) { e.preventDefault(); void handleSave(); } }; window.addEventListener("keydown", handleKeyDown); return (): void => { window.removeEventListener("keydown", handleKeyDown); }; }, [handleSave, isEditing]); const handleEdit = (): void => { if (!entry) return; setIsEditing(true); }; const handleCancel = (): void => { if (!entry) return; if ( !hasUnsavedChanges || confirm("You have unsaved changes. Are you sure you want to cancel?") ) { setEditTitle(entry.title); setEditContent(entry.content); setEditStatus(entry.status); setEditVisibility(entry.visibility); setEditTags(entry.tags.map((tag) => tag.id)); setIsEditing(false); setHasUnsavedChanges(false); } }; const handleDelete = async (): Promise => { if ( !confirm( "Are you sure you want to delete this entry? It will be archived and can be restored later." ) ) { return; } try { await deleteEntry(slug); router.push("/knowledge"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete entry"); } }; const handleVersionRestore = (): void => { // Reload entry after version restore async function reload(): Promise { try { const data = await fetchEntry(slug); setEntry(data); setEditTitle(data.title); setEditContent(data.content); setEditStatus(data.status); setEditVisibility(data.visibility); setEditTags(data.tags.map((tag) => tag.id)); setActiveTab("content"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to reload entry"); } } void reload(); }; if (isLoading) { return (
); } if (error && !entry) { return (

{error}

); } if (!entry) { return null; } return (
{/* Header */}
{isEditing ? (
) : ( <>

{entry.title}

{/* Status Badge */} {entry.status} {/* Visibility Badge */} {entry.visibility} {/* Tags */} {entry.tags.map((tag) => ( {tag.name} ))}
)}
{error && (

{error}

)} {/* Tabs */} {!isEditing && (
)} {/* Content */}
{isEditing ? ( ) : activeTab === "content" ? ( <> {/* Backlinks Section */}
) : ( )}
{/* Actions */}
{isEditing && ( )}
{isEditing ? ( <> ) : ( )}
{isEditing && (

Press Cmd+S or{" "} Ctrl+S to save

)}
); }