"use client"; import { useCallback, useState } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { HourglassIcon, PencilIcon, Save, ShieldCheck, X } from "lucide-react"; import { RichText } from "@payloadcms/richtext-lexical/react"; import { LexicalComposer } from "@payloadcms/richtext-lexical/lexical/react/LexicalComposer"; import { ContentEditable } from "@payloadcms/richtext-lexical/lexical/react/LexicalContentEditable"; import { LexicalErrorBoundary } from "@payloadcms/richtext-lexical/lexical/react/LexicalErrorBoundary"; import { HistoryPlugin } from "@payloadcms/richtext-lexical/lexical/react/LexicalHistoryPlugin"; import { MarkdownShortcutPlugin } from "@payloadcms/richtext-lexical/lexical/react/LexicalMarkdownShortcutPlugin"; import { OnChangePlugin } from "@payloadcms/richtext-lexical/lexical/react/LexicalOnChangePlugin"; import { RichTextPlugin } from "@payloadcms/richtext-lexical/lexical/react/LexicalRichTextPlugin"; import { HeadingNode, QuoteNode } from "@payloadcms/richtext-lexical/lexical/rich-text"; import { ListItemNode, ListNode } from "@payloadcms/richtext-lexical/lexical/list"; import { AutoLinkNode, LinkNode } from "@payloadcms/richtext-lexical/lexical/link"; import { MarkNode } from "@payloadcms/richtext-lexical/lexical/mark"; import { CodeHighlightNode, CodeNode } from "@lexical/code"; import { HorizontalRuleNode } from "@payloadcms/richtext-lexical/lexical/react/LexicalHorizontalRuleNode"; import { $getRoot } from "@payloadcms/richtext-lexical/lexical"; import type { LexicalEditor } from "@payloadcms/richtext-lexical/lexical"; import { Button } from "@/components/ui/button"; import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item"; import { lexicalPlaintextLength } from "@/lib/lexical/text"; import type { LexicalStateValue } from "@/lib/lexical/text"; import { INTEL_EXCERPT_MAX_LENGTH, PERSONAL_EXCERPT_MAX_LENGTH, } from "@/lib/profile/excerpt-limits"; import { updateIntelExcerpt, updatePersonalExcerpt, } from "@/app/(frontend)/profile/[username]/actions"; import { cn } from "@/lib/utils"; const EDITOR_NODES = [ HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode, HorizontalRuleNode, CodeNode, CodeHighlightNode, MarkNode, ]; const EDITOR_CONTENT_CLASSES = "prose-invert prose-sm max-w-none [&_p]:mb-2 [&_ul]:list-disc [&_ul]:ml-4 [&_ol]:list-decimal [&_ol]:ml-4 [&_strong]:text-foreground [&_em]:italic"; interface ExcerptEditorProps { value: LexicalStateValue | null; maxLength: number; placeholder: string; onEditChange: (draft: LexicalStateValue | null, length: number) => void; } function ExcerptEditor({ value, maxLength, placeholder, onEditChange }: ExcerptEditorProps) { const [length, setLength] = useState(() => lexicalPlaintextLength(value)); const handleChange = useCallback( (editorState: ReturnType) => { const serialized: LexicalStateValue = JSON.parse(JSON.stringify(editorState.toJSON())); setLength(lexicalPlaintextLength(serialized)); onEditChange(serialized, lexicalPlaintextLength(serialized)); }, [onEditChange], ); return ( { console.error(error); }, theme: { heading: { h2: "text-lg font-semibold text-foreground mb-2", h3: "text-base font-semibold text-foreground mb-2", }, quote: "border-l-2 border-border pl-3 italic text-muted-foreground mb-2", list: { ul: "list-disc ml-4 mb-2", ol: "list-decimal ml-4 mb-2" }, link: "text-primary underline underline-offset-2", }, }} >

maxLength && "text-red-500 font-medium")}> {length} /{maxLength}

} placeholder={
{placeholder}
} ErrorBoundary={LexicalErrorBoundary} />
); } interface ExcerptCardProps { profileId: number; kind: "personal" | "intel"; title: string; value: LexicalStateValue | null; maxLength: number; canEdit: boolean; emptyMessage: string; placeholder: string; hasIntelBadge?: boolean; } function ExcerptCard({ profileId, kind, title, value, maxLength, canEdit, emptyMessage, placeholder, hasIntelBadge, }: ExcerptCardProps) { const router = useRouter(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(null); const [draftLength, setDraftLength] = useState(() => lexicalPlaintextLength(value)); const [saving, setSaving] = useState(false); const handleEditChange = useCallback((next: LexicalStateValue | null, length: number) => { setDraft(next); setDraftLength(length); }, []); const handleEdit = () => { setDraft(null); setDraftLength(lexicalPlaintextLength(value)); setEditing(true); }; const handleCancel = () => { setDraft(null); setEditing(false); }; const handleSave = async () => { const content = draft ?? value; if (!content) { handleCancel(); return; } if (draftLength > maxLength) return; setSaving(true); const result = kind === "personal" ? await updatePersonalExcerpt({ profileId, content }) : await updateIntelExcerpt({ profileId, content }); setSaving(false); if (result.success) { toast.success(kind === "personal" ? "Personal excerpt saved." : "Intelligence record saved."); setDraft(null); setEditing(false); router.refresh(); } else { toast.error(result.error ?? "Failed to save."); } }; const showBadge = kind === "intel" && hasIntelBadge; return ( {title} {showBadge ? (
Verified by the Intelligence Division
) : kind === "intel" ? (
Undergoing review by the Intelligence Division{" "}
) : null} {canEdit && !editing ? ( ) : null}
{editing ? (
) : value ? ( ) : (

{emptyMessage}

)}
); } interface DossierExcerptEditorsProps { profileId: number; personalExcerpt: LexicalStateValue | null; intelExcerpt: LexicalStateValue | null; canEditPersonal: boolean; canEditIntel: boolean; } export function DossierExcerptEditors({ profileId, personalExcerpt, intelExcerpt, canEditPersonal, canEditIntel, }: DossierExcerptEditorsProps) { return (
); }