"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { PlusIcon, SearchIcon, XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { v4 } from "uuid"; import { z } from "zod"; import { EditorSheet, FormInput, FormSelect, FormTextarea, FormCheckbox } from "../../../../components"; import { BlockNoteEditorContainer } from "../../../../components"; import { ModuleRegistry, Modules } from "../../../../core"; import { useI18nRouter } from "../../../../hooks"; import { Button, Command, CommandEmpty, CommandItem, CommandList, Input, Label, Popover, PopoverContent, PopoverTrigger, } from "../../../../shadcnui"; import { HowTo } from "../../data/HowTo"; import { HowToInput, HowToInterface } from "../../data/HowToInterface"; import { HowToService } from "../../data/HowToService"; import HowToMultiSelector from "./HowToMultiSelector"; const HOW_TO_TYPES = ["tutorial", "how-to", "reference", "explanation"] as const; function PageSelector({ value, allPageUrls, selectedPages, placeholder, emptyMessage, onSelect, onRemove, }: { value: string; allPageUrls: { id: string; text: string }[]; selectedPages: string[]; placeholder: string; emptyMessage: string; onSelect: (value: string) => void; onRemove: () => void; }) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const selectedLabel = value ? allPageUrls.find((opt) => opt.id === value) : undefined; const filteredOptions = useMemo(() => { const available = allPageUrls.filter((opt) => opt.id === value || !selectedPages.includes(opt.id)); if (!search) return available; const term = search.toLowerCase(); return available.filter((opt) => opt.text.toLowerCase().includes(term) || opt.id.toLowerCase().includes(term)); }, [allPageUrls, selectedPages, value, search]); return (
{selectedLabel ? ( {selectedLabel.text} ({selectedLabel.id}) ) : ( {placeholder} )}
setSearch(e.target.value)} value={search} />
{emptyMessage} {filteredOptions.map((opt) => ( { onSelect(opt.id); setOpen(false); setSearch(""); }} > {opt.text} ({opt.id}) ))}
); } type HowToEditorProps = { howTo?: HowToInterface; propagateChanges?: (howTo: HowToInterface) => void; trigger?: ReactNode; forceShow?: boolean; onClose?: () => void; onRevalidate?: (path: string) => Promise; dialogOpen?: boolean; onDialogOpenChange?: (open: boolean) => void; }; function HowToEditorInternal({ howTo, propagateChanges, trigger, forceShow, onClose, onRevalidate, dialogOpen, onDialogOpenChange, }: HowToEditorProps) { const router = useI18nRouter(); const t = useTranslations(); const formSchema = useMemo( () => z.object({ id: z.string().uuid(), name: z.string().min(1, { message: t(`howto.fields.name.error`) }), description: z.any(), pages: z.array(z.string()), howToType: z.enum(HOW_TO_TYPES), slug: z.string().min(1, { message: t(`howto.fields.slug.error`) }), order: z.number().int().min(0), summary: z.string().optional(), tags: z.string().optional(), contextualKeys: z.string().optional(), draft: z.boolean().optional(), relatedArticles: z.array(z.object({ id: z.string().uuid(), name: z.string() })).optional(), }), [t], ); const getDefaultValues = useCallback( () => ({ id: howTo?.id || v4(), name: howTo?.name || "", description: howTo?.description || [], pages: HowTo.parsePagesFromString(howTo?.pages), howToType: (howTo?.howToType as (typeof HOW_TO_TYPES)[number]) ?? "how-to", slug: howTo?.slug ?? "", order: howTo?.order ?? 0, summary: howTo?.summary ?? "", tags: (howTo?.tags ?? []).join(", "), contextualKeys: (howTo?.contextualKeys ?? []).join(", "), draft: howTo?.draft ?? false, relatedArticles: [] as { id: string; name: string }[], }), [howTo], ); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: getDefaultValues(), }); const initialRelatedIds = useRef([]); useEffect(() => { if (!howTo?.howToType || !howTo?.slug) return; let active = true; HowToService.findRelated({ howToType: howTo.howToType, slug: howTo.slug }) .then((list) => { if (!active) return; initialRelatedIds.current = list.map((r) => r.id); form.setValue( "relatedArticles", list.map((r) => ({ id: r.id, name: r.name })), { shouldDirty: false }, ); }) .catch(() => {}); return () => { active = false; }; }, [howTo, form]); const handleDescriptionChange = useCallback( (content: any) => { form.setValue("description", content, { shouldDirty: true }); }, [form], ); const allPageUrls = useMemo(() => ModuleRegistry.getAllPageUrls(), []); const pages = form.watch("pages"); const addPage = () => { form.setValue("pages", [...pages, ""], { shouldDirty: true }); }; const removePage = (index: number) => { form.setValue( "pages", pages.filter((_: string, i: number) => i !== index), { shouldDirty: true }, ); }; return ( { const toList = (s?: string) => (s ?? "") .split(",") .map((x) => x.trim()) .filter(Boolean); const payload: HowToInput = { id: values.id, name: values.name, authorId: "", description: values.description, pages: HowTo.serializePagesToString(values.pages), howToType: values.howToType, slug: values.slug, order: values.order, summary: values.summary, tags: toList(values.tags), contextualKeys: toList(values.contextualKeys), draft: values.draft ?? false, }; const saved = howTo ? await HowToService.update(payload) : await HowToService.create(payload); const desired = new Set((values.relatedArticles ?? []).map((r) => r.id)); const current = new Set(initialRelatedIds.current); for (const relatedId of desired) { if (!current.has(relatedId)) await HowToService.addRelated({ howToId: values.id, relatedId }); } for (const relatedId of current) { if (!desired.has(relatedId)) await HowToService.removeRelated({ howToId: values.id, relatedId }); } initialRelatedIds.current = Array.from(desired); return saved; }} onReset={() => { return getDefaultValues(); }} onRevalidate={onRevalidate} onNavigate={(url) => router.push(url)} onClose={onClose} trigger={trigger} forceShow={forceShow} dialogOpen={dialogOpen} onDialogOpenChange={onDialogOpenChange} >
({ id: m, text: t(`howto.howToType.${m}` as any) }))} isRequired />
{/* Pages List */}
{pages.map((page: string, index: number) => ( { const updated = [...pages]; updated[index] = value; form.setValue("pages", updated, { shouldDirty: true }); }} onRemove={() => removePage(index)} /> ))}
); } export default function HowToEditor(props: HowToEditorProps) { return ; }