diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/feedback/components | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz | |
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/feedback/components')
| -rw-r--r-- | apps/web/src/lib/feedback/components/feedback-popover.tsx | 242 |
1 files changed, 0 insertions, 242 deletions
diff --git a/apps/web/src/lib/feedback/components/feedback-popover.tsx b/apps/web/src/lib/feedback/components/feedback-popover.tsx deleted file mode 100644 index f94b7253..00000000 --- a/apps/web/src/lib/feedback/components/feedback-popover.tsx +++ /dev/null @@ -1,242 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { ClockIcon } from "lucide-react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { Spinner } from "@/components/ui/spinner"; -import { - Form, - FormField, - FormItem, - FormControl, - clearFormDraft, -} from "@/components/ui/form"; -import type { FeedbackEntry } from "../types"; - -const PERSIST_KEY = "feedback-draft"; -const HISTORY_KEY = "feedback-history"; -const MAX_HISTORY = 20; - -interface FeedbackFormValues { - message: string; -} - -function readHistory(): FeedbackEntry[] { - try { - const stored = localStorage.getItem(HISTORY_KEY); - return stored ? (JSON.parse(stored) as FeedbackEntry[]) : []; - } catch { - return []; - } -} - -function writeHistory({ entries }: { entries: FeedbackEntry[] }): void { - try { - localStorage.setItem(HISTORY_KEY, JSON.stringify(entries)); - } catch { - // localStorage may be full or unavailable - } -} - -function useFeedback() { - const [entries, setEntries] = useState<FeedbackEntry[]>(readHistory); - const [isSubmitting, setIsSubmitting] = useState(false); - - async function submit({ - values, - onSuccess, - }: { - values: FeedbackFormValues; - onSuccess: () => void; - }) { - if (isSubmitting) return; - setIsSubmitting(true); - - try { - const res = await fetch("/api/feedback", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(values), - }); - - if (!res.ok) { - const data = await res.json().catch(() => null); - throw new Error(data?.error ?? "Failed to submit"); - } - - const { entry } = await res.json(); - const next = [entry, ...entries].slice(0, MAX_HISTORY); - setEntries(next); - writeHistory({ entries: next }); - onSuccess(); - toast.success("Feedback sent"); - } catch (error) { - toast.error( - error instanceof Error ? error.message : "Failed to send feedback", - ); - } finally { - setIsSubmitting(false); - } - } - - return { entries, isSubmitting, submit }; -} - -export function FeedbackPopover() { - const [open, setOpen] = useState(false); - - return ( - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <Button variant="outline" className="h-8"> - Send feedback - </Button> - </PopoverTrigger> - <PopoverContent align="end" className="w-80 p-0"> - <FeedbackPopoverContent onClose={() => setOpen(false)} /> - </PopoverContent> - </Popover> - ); -} - -type View = "compose" | "history"; - -function FeedbackPopoverContent({ onClose }: { onClose: () => void }) { - const { entries, isSubmitting, submit } = useFeedback(); - const [view, setView] = useState<View>("compose"); - - const form = useForm<FeedbackFormValues>({ - defaultValues: { message: "" }, - }); - - async function handleSubmit(values: FeedbackFormValues) { - await submit({ - values, - onSuccess: () => { - form.reset({ message: "" }); - clearFormDraft({ key: PERSIST_KEY }); - onClose(); - }, - }); - } - - if (view === "history") { - return ( - <div className="flex flex-col"> - <div - className="max-h-72 overflow-y-auto divide-y" - style={{ - maskImage: - "linear-gradient(to bottom, black 80%, transparent 100%)", - }} - > - {entries.map((entry) => ( - <FeedbackEntryItem key={entry.id} entry={entry} /> - ))} - </div> - <div className="border-t px-3 py-2"> - <button - type="button" - onClick={() => setView("compose")} - className="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors" - > - ← Back - </button> - </div> - </div> - ); - } - - return ( - <div className="flex flex-col"> - <Form persistKey={PERSIST_KEY} {...form}> - <form onSubmit={form.handleSubmit(handleSubmit)} className="flex flex-col"> - <FormField - control={form.control} - name="message" - render={({ field }) => ( - <FormItem> - <FormControl> - <Textarea - placeholder="Thoughts, bugs, ideas..." - className="min-h-[7rem] text-sm p-3 bg-background shadow-none border-none! resize-none" - {...field} - /> - </FormControl> - </FormItem> - )} - /> - <div className="flex items-center justify-between border-t px-3 py-2"> - {entries.length > 0 ? ( - <button - type="button" - onClick={() => setView("history")} - className="flex items-center gap-1.5 text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors" - > - <ClockIcon className="size-3" /> - {entries.length} - </button> - ) : ( - <span /> - )} - <div className="flex gap-2"> - {!form.watch("message").trim() && ( - <Button - type="button" - variant="outline" - size="sm" - onClick={onClose} - > - Cancel - </Button> - )} - <Button - type="submit" - size="sm" - disabled={isSubmitting || !form.watch("message").trim()} - > - {isSubmitting ? <Spinner /> : "Send"} - </Button> - </div> - </div> - </form> - </Form> - </div> - ); -} - -function relativeDate(iso: string): string { - const diff = Date.now() - new Date(iso).getTime(); - const mins = Math.floor(diff / 60_000); - if (mins < 1) return "just now"; - if (mins < 60) return `${mins}m ago`; - const hrs = Math.floor(mins / 60); - if (hrs < 24) return `${hrs}h ago`; - const days = Math.floor(hrs / 24); - if (days < 7) return `${days}d ago`; - return new Date(iso).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - }); -} - -function FeedbackEntryItem({ entry }: { entry: FeedbackEntry }) { - return ( - <div className="px-3 py-2.5"> - <p className="text-sm text-muted-foreground leading-snug whitespace-pre-wrap break-words"> - {entry.message} - </p> - <span className="mt-1 block text-[11px] text-muted-foreground/50"> - {relativeDate(entry.createdAt)} - </span> - </div> - ); -} |
