Files
edgeever/apps/web/src/components/dialogs/TemplatesDialog.tsx
T

144 lines
7.7 KiB
TypeScript

import { useState } from "react";
import { Check, LayoutList, Pencil, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { MemoTemplate } from "@edgeever/shared";
export const TemplatesDialog = ({
canCreateMemo,
isCreating,
onClose,
savedTemplates,
onUseSavedTemplate,
onDeleteSavedTemplate,
onUpdateSavedTemplate,
}: {
canCreateMemo: boolean;
isCreating: boolean;
onClose: () => void;
savedTemplates: MemoTemplate[];
onUseSavedTemplate: (template: MemoTemplate) => void;
onDeleteSavedTemplate: (template: MemoTemplate) => void;
onUpdateSavedTemplate: (templateId: string, payload: { name: string; description: string | null; title: string | null; contentMarkdown: string; tags: string[] }) => Promise<void>;
}) => {
const { t } = useTranslation();
const [editingTemplate, setEditingTemplate] = useState<MemoTemplate | null>(null);
const [draft, setDraft] = useState({ name: "", description: "", title: "", contentMarkdown: "", tags: "" });
const startEditing = (template: MemoTemplate) => {
setEditingTemplate(template);
setDraft({
name: template.name,
description: template.description ?? "",
title: template.title ?? "",
contentMarkdown: template.contentMarkdown,
tags: template.tags.join(", "),
});
};
const cancelEditing = () => setEditingTemplate(null);
const saveEditing = async () => {
if (!editingTemplate || !draft.name.trim()) return;
await onUpdateSavedTemplate(editingTemplate.id, {
name: draft.name.trim(),
description: draft.description.trim() || null,
title: draft.title.trim() || null,
contentMarkdown: draft.contentMarkdown,
tags: draft.tags.split(",").map((tag) => tag.trim()).filter(Boolean),
});
setEditingTemplate(null);
};
return (
<Dialog open={true} onOpenChange={(open) => { if (!open && !isCreating) onClose(); }}>
<DialogContent className="max-w-[620px] p-0 overflow-hidden border border-slate-200 bg-white shadow-lg rounded-lg">
<DialogHeader className="flex flex-row items-start justify-between gap-3 border-b border-slate-200 px-5 py-4 text-left">
<div className="min-w-0">
<DialogTitle className="flex items-center gap-2 text-base font-semibold text-slate-950">
<LayoutList className="h-4 w-4 text-emerald-700" />
{t("templates.title")}
</DialogTitle>
<DialogDescription className="mt-1 text-xs text-slate-500">
{t("templates.description")}
</DialogDescription>
</div>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto p-5">
{savedTemplates.length > 0 ? (
<section>
<h3 className="mb-2 text-xs font-bold text-slate-900">{t("templates.myTemplates")}</h3>
{editingTemplate && (
<div className="mb-4 space-y-3 rounded-xl border border-emerald-200/80 bg-white p-4 shadow-xs">
<h4 className="text-xs font-semibold text-slate-800">{t("templates.editTemplateTitle")}</h4>
<div className="grid gap-2.5 sm:grid-cols-2">
<Input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={t("templates.namePlaceholder")} />
<Input value={draft.title} onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))} placeholder={t("templates.noteTitlePlaceholder")} />
<Input value={draft.description} onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value }))} placeholder={t("templates.descriptionPlaceholder")} />
<Input value={draft.tags} onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))} placeholder={t("templates.tagsPlaceholder")} />
</div>
<textarea
className="min-h-36 w-full resize-y rounded-lg border border-slate-200 bg-white p-3 font-mono text-xs text-slate-900 outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-500/20"
value={draft.contentMarkdown}
onChange={(event) => setDraft((current) => ({ ...current, contentMarkdown: event.target.value }))}
placeholder={t("templates.contentPlaceholder")}
/>
<div className="flex justify-end gap-2">
<Button type="button" size="sm" variant="ghost" onClick={cancelEditing}><X className="mr-1 h-4 w-4" />{t("common.cancel")}</Button>
<Button type="button" size="sm" className="bg-emerald-600 hover:bg-emerald-700 text-white" onClick={() => void saveEditing()} disabled={!draft.name.trim()}><Check className="mr-1 h-4 w-4" />{t("common.save")}</Button>
</div>
</div>
)}
<div className="grid gap-3 sm:grid-cols-2">
{savedTemplates.map((template) => (
<div key={template.id} className="rounded-xl border border-slate-200 bg-white p-3.5 shadow-2xs hover:border-emerald-200 transition">
<button
className="block w-full text-left disabled:opacity-50"
type="button"
disabled={!canCreateMemo || isCreating}
onClick={() => onUseSavedTemplate(template)}
>
<div className="flex items-center justify-between gap-1">
<span className="text-sm font-bold text-slate-900 truncate">{template.name}</span>
</div>
<span className="mt-1 block line-clamp-2 text-xs leading-relaxed text-slate-500">{template.description || template.title || t("templates.savedDescription")}</span>
</button>
<div className="mt-2.5 flex items-center justify-between border-t border-slate-100 pt-2 text-xs">
<div className="flex gap-2">
<button className="inline-flex items-center gap-1 text-slate-500 hover:text-slate-800 transition" type="button" disabled={isCreating} onClick={() => startEditing(template)}>
<Pencil className="h-3 w-3" />{t("templates.edit")}
</button>
<button className="text-rose-600 hover:text-rose-700 transition" type="button" disabled={isCreating} onClick={() => onDeleteSavedTemplate(template)}>
{t("templates.delete")}
</button>
</div>
<button
type="button"
className="text-xs font-semibold text-emerald-700 hover:text-emerald-800 transition"
disabled={!canCreateMemo || isCreating}
onClick={() => onUseSavedTemplate(template)}
>
{t("templates.useThisTemplate")}
</button>
</div>
</div>
))}
</div>
</section>
) : (
<p className="text-xs text-slate-500">{t("templates.emptyMyTemplatesHint")}</p>
)}
{!canCreateMemo && (
<div className="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-3.5 py-2.5 text-xs font-medium text-amber-900">
{t("templates.unavailable")}
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};