"use client";
import { useActionState, useEffect, useId, useState } from "react";
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { Led, Spinner, Switch, TabList, TabPanel, Tabs } from "@unlocalhosted/metalui";
import type { FormState } from "@/app/actions/types";
import { ConfirmButton } from "../confirm-button";
import { vectrumDark, vectrumLight } from "../editor-theme";
import { useLanguage } from "../file-editor";
import { BookIcon, CodeIcon, EyeIcon, TrashIcon } from "../icons";
import { MarkdownPreview } from "../markdown-preview";
import { TextField } from "../settings-fields";
import { useDarkMode } from "../theme-toggle";
import { Alert, Box, Button, ButtonLink, Kbd } from "../ui";
type Tab = "write" | "preview";
/**
* Writing a wiki page, like GitHub's: its title, the Markdown with a preview, and an optional edit
* message; saving commits it to the wiki. Editing an existing page also offers to delete it.
*/
export function WikiEditor({
action,
deleteAction,
wiki,
originalPath,
baseSha,
initialTitle,
initialContent,
cancelHref,
}: {
action: (prev: FormState, formData: FormData) => Promise<FormState>;
deleteAction?: () => Promise<void>;
/** The wiki's base URL, for links in the preview. */
wiki: string;
/** The page's file, or "" for a new page. */
originalPath: string;
baseSha: string;
initialTitle: string;
initialContent: string;
cancelHref: string;
}) {
const formId = useId();
const [state, formAction, pending] = useActionState(action, undefined);
const [title, setTitle] = useState(initialTitle);
const [content, setContent] = useState(initialContent);
const [tab, setTab] = useState<Tab>("write");
const [wrap, setWrap] = useState(true);
const dark = useDarkMode();
const lang = useLanguage("page.md");
const dirty = content !== initialContent || title !== initialTitle;
const extensions = [...(lang ? [lang.ext] : []), ...(wrap ? [EditorView.lineWrapping] : [])];
// Ctrl/Cmd+S saves; warn before leaving with unsaved edits.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
(document.getElementById(formId) as HTMLFormElement | null)?.requestSubmit();
}
};
const onBeforeUnload = (e: BeforeUnloadEvent) => {
if (dirty && !pending) e.preventDefault();
};
window.addEventListener("keydown", onKey);
window.addEventListener("beforeunload", onBeforeUnload);
return () => {
window.removeEventListener("keydown", onKey);
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [dirty, pending, formId]);
return (
<div className="flex flex-col gap-4">
<form id={formId} action={formAction} className="flex flex-col gap-4">
<input type="hidden" name="originalPath" value={originalPath} />
<input type="hidden" name="baseSha" value={baseSha} />
<input type="hidden" name="content" value={content} />
<TextField
label="Title"
name="title"
icon={<BookIcon />}
required
maxLength={200}
value={title}
onChange={(e) => setTitle(e.target.value)}
autoFocus={!initialTitle}
placeholder="Getting Started"
/>
<Tabs<Tab> value={tab} onValueChange={setTab}>
<Box className="overflow-hidden">
<div className="rule-b flex min-h-12 flex-wrap items-center gap-3 px-3 py-2">
<TabList<Tab>
aria-label="Editor view"
size="compact"
items={[
{ value: "write", label: "Write", icon: <CodeIcon /> },
{ value: "preview", label: "Preview", icon: <EyeIcon /> },
]}
/>
<span className="hidden type-meta text-ink3 md:inline">
Markdown. Link pages with <code className="font-mono">[[Page Name]]</code> or <code className="font-mono">[[Text|Page Name]]</code>.
</span>
<label className="ml-auto flex cursor-pointer items-center gap-2 type-meta text-ink2">
<Switch size="small" checked={wrap} onCheckedChange={setWrap} /> Soft wrap
</label>
</div>
<TabPanel<Tab> value="write" keepMounted>
<CodeMirror
value={content}
onChange={setContent}
theme={dark ? vectrumDark : vectrumLight}
extensions={extensions}
minHeight="460px"
basicSetup={{ highlightActiveLine: true, foldGutter: false, tabSize: 2 }}
aria-label="Page content"
className="py-2"
/>
</TabPanel>
<TabPanel<Tab> value="preview">
<div className="min-h-[460px] px-6 py-8 md:px-10">
<MarkdownPreview wiki={wiki}>{content}</MarkdownPreview>
</div>
</TabPanel>
</Box>
</Tabs>
<TextField
label="Edit message"
name="message"
optional
maxLength={500}
placeholder={!originalPath ? `Create ${title.trim() || "page"}` : `Update ${title.trim() || "page"}`}
description="Describes the change in the page’s history."
/>
{state?.error && <Alert>{state.error}</Alert>}
</form>
<div className="flex flex-wrap items-center gap-2">
{deleteAction && (
<ConfirmButton action={deleteAction} message={`Delete ${initialTitle}? Its history stays in the wiki’s repository.`} variant="invisible" confirmLabel="Delete Page">
<TrashIcon /> Delete Page
</ConfirmButton>
)}
<span className="flex items-center gap-1 type-meta text-ink2 max-sm:hidden">
<Kbd>Ctrl</Kbd> <Kbd>S</Kbd> saves.
{dirty && (
<span className="ml-2 inline-flex items-center gap-1.5 text-ink">
<Led kind="waiting" /> Unsaved changes
</span>
)}
</span>
<div className="ml-auto flex gap-2">
<ButtonLink href={cancelHref}>Cancel</ButtonLink>
<Button
form={formId}
variant="primary"
disabled={pending || !title.trim()}
icon={pending ? <Spinner size="small" label="Saving" /> : <BookIcon />}
>
{pending ? "Saving…" : "Save Page"}
</Button>
</div>
</div>
</div>
);
}