Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 01:45:54 +00:00
co-authored by renee-png
parent 2236a45d91
commit 3483e065ec
7 changed files with 869 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { ProtectedLayout } from "@/components/protected-layout";
import { PageContainer, PageHeader } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { toast } from "sonner";
import { Loader2, Plus, Save, Trash2 } from "lucide-react";
export const Route = createFileRoute("/documents/templates/new")({
component: NewTemplatePage,
});
interface FieldDef { key: string; label: string; type: "text" | "textarea" | "date" | "number" }
function NewTemplatePage() {
const { user } = useAuth();
const navigate = useNavigate();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [fields, setFields] = useState<FieldDef[]>([]);
const [body, setBody] = useState("Dear {{client_name}},\n\n");
const [saving, setSaving] = useState(false);
const addField = () => setFields((f) => [...f, { key: `field_${f.length + 1}`, label: `Field ${f.length + 1}`, type: "text" }]);
const removeField = (i: number) => setFields((f) => f.filter((_, idx) => idx !== i));
const updateField = (i: number, patch: Partial<FieldDef>) =>
setFields((f) => f.map((x, idx) => (idx === i ? { ...x, ...patch } : x)));
const insertToken = (key: string) => setBody((b) => b + `{{${key}}}`);
const onSave = async () => {
if (!name.trim()) { toast.error("Name is required"); return; }
setSaving(true);
const { data, error } = await supabase.from("document_templates").insert({
name: name.trim(),
description: description.trim() || null,
kind: "general",
body,
fields,
created_by: user?.id,
}).select().single();
setSaving(false);
if (error) { toast.error(error.message); return; }
toast.success("Template created");
navigate({ to: "/documents/templates/$templateId", params: { templateId: data.id } });
};
return (
<ProtectedLayout>
<PageContainer>
<PageHeader
title="New template"
description="Define merge fields and a body. Use {{field_key}} where you want each value inserted."
actions={
<Button onClick={onSave} disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
Save template
</Button>
}
/>
<div className="grid lg:grid-cols-2 gap-6">
<Card>
<CardContent className="p-5 space-y-4">
<div className="space-y-1.5">
<Label>Name</Label>
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Demand Letter" />
</div>
<div className="space-y-1.5">
<Label>Description</Label>
<Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optional" />
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Custom fields</Label>
<Button variant="outline" size="sm" onClick={addField}><Plus className="h-3.5 w-3.5 mr-1" /> Add field</Button>
</div>
{fields.length === 0 && <div className="text-xs text-muted-foreground">No fields yet. Add fields like client_name, amount_due, due_date.</div>}
<div className="space-y-2">
{fields.map((f, i) => (
<div key={i} className="grid grid-cols-12 gap-2 items-center">
<Input className="col-span-4" value={f.key} onChange={(e) => updateField(i, { key: e.target.value.replace(/[^a-zA-Z0-9_]/g, "_") })} placeholder="key" />
<Input className="col-span-4" value={f.label} onChange={(e) => updateField(i, { label: e.target.value })} placeholder="Label" />
<select
className="col-span-3 h-9 rounded-md border border-input bg-transparent px-2 text-sm"
value={f.type}
onChange={(e) => updateField(i, { type: e.target.value as any })}
>
<option value="text">Text</option>
<option value="textarea">Long text</option>
<option value="date">Date</option>
<option value="number">Number</option>
</select>
<Button variant="ghost" size="icon" className="col-span-1" onClick={() => removeField(i)}><Trash2 className="h-4 w-4 text-destructive" /></Button>
<div className="col-span-12 -mt-1">
<button type="button" onClick={() => insertToken(f.key)} className="text-[11px] text-primary hover:underline">
Insert {"{{"}{f.key}{"}}"} into body
</button>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-5 space-y-2">
<Label>Body</Label>
<Textarea rows={20} value={body} onChange={(e) => setBody(e.target.value)} className="font-mono text-sm" />
<p className="text-xs text-muted-foreground">
Use double curly braces around field keys, e.g. <code>{"{{client_name}}"}</code>. They&apos;ll be replaced when generating a document.
</p>
</CardContent>
</Card>
</div>
</PageContainer>
</ProtectedLayout>
);
}