Files
verse-quote-app/app/components/quotes/SubmitQuoteDialog.tsx
T
exe.dev user 0a18c774a5
CI / build (push) Successful in 4m26s
Build Verse: a poem-quote app on the agent-native chat template
Adds a Postgres-backed quotes domain (poems/quotes/favorites) exposed as
five agent-native actions (list-quotes, get-random-quote, create-quote,
toggle-favorite, list-favorites) callable from both chat and the React UI
via useActionQuery/useActionMutation. Moves the primary UI from chat to a
mobile-first Quotes/Favorites experience (chat moves to /chat), adds
manual OpenTelemetry instrumentation exporting traces/metrics/logs over
OTLP, and wires a local Docker Postgres for shared state.
2026-07-25 17:42:04 +00:00

144 lines
4.9 KiB
TypeScript

import { useActionMutation } from "@agent-native/core/client/hooks";
import { IconPlus } from "@tabler/icons-react";
import { useId, useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
const emptyForm = { text: "", author: "", poemTitle: "", source: "", year: "", tags: "" };
export function SubmitQuoteDialog() {
const [open, setOpen] = useState(false);
const [form, setForm] = useState(emptyForm);
const formId = useId();
const { mutate, isPending } = useActionMutation("create-quote", {
onSuccess: () => {
toast.success("Quote added");
setForm(emptyForm);
setOpen(false);
},
onError: () => toast.error("Couldn't save that quote — try again"),
});
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
if (!form.text.trim() || !form.author.trim() || !form.poemTitle.trim()) return;
mutate({
text: form.text.trim(),
author: form.author.trim(),
poemTitle: form.poemTitle.trim(),
source: form.source.trim() || undefined,
year: form.year ? Number(form.year) : undefined,
tags: form.tags
.split(",")
.map((t) => t.trim().toLowerCase())
.filter(Boolean),
});
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button type="button" className="gap-1.5">
<IconPlus className="size-4" />
Submit a quote
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Submit a quote</DialogTitle>
<DialogDescription>
Share a line or passage from a poem you love.
</DialogDescription>
</DialogHeader>
<form id={formId} onSubmit={handleSubmit} className="grid gap-4">
<div className="grid gap-1.5">
<Label htmlFor={`${formId}-text`}>Quote</Label>
<Textarea
id={`${formId}-text`}
required
rows={4}
value={form.text}
onChange={(e) => setForm((f) => ({ ...f, text: e.target.value }))}
placeholder="Two roads diverged in a wood..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-1.5">
<Label htmlFor={`${formId}-author`}>Poet</Label>
<Input
id={`${formId}-author`}
required
value={form.author}
onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}
placeholder="Robert Frost"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor={`${formId}-title`}>Poem title</Label>
<Input
id={`${formId}-title`}
required
value={form.poemTitle}
onChange={(e) => setForm((f) => ({ ...f, poemTitle: e.target.value }))}
placeholder="The Road Not Taken"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-1.5">
<Label htmlFor={`${formId}-source`}>Source (optional)</Label>
<Input
id={`${formId}-source`}
value={form.source}
onChange={(e) => setForm((f) => ({ ...f, source: e.target.value }))}
placeholder="Collection name"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor={`${formId}-year`}>Year (optional)</Label>
<Input
id={`${formId}-year`}
inputMode="numeric"
value={form.year}
onChange={(e) => setForm((f) => ({ ...f, year: e.target.value.replace(/\D/g, "") }))}
placeholder="1916"
/>
</div>
</div>
<div className="grid gap-1.5">
<Label htmlFor={`${formId}-tags`}>Tags (optional, comma separated)</Label>
<Input
id={`${formId}-tags`}
value={form.tags}
onChange={(e) => setForm((f) => ({ ...f, tags: e.target.value }))}
placeholder="hope, nature, journey"
/>
</div>
</form>
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit" form={formId} disabled={isPending}>
{isPending ? "Saving..." : "Save quote"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}