"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 (
);
}
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 ;
}