"use client"; import { RiAddLine, RiDeleteBinLine } from "@remixicon/react"; import { type ReactNode, useEffect, useMemo, useRef, useState, useTransition, } from "react"; import { toast } from "sonner"; import { createNoteAction, updateNoteAction, } from "@/app/(dashboard)/anotacoes/actions"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Textarea } from "@/components/ui/textarea"; import { useControlledState } from "@/hooks/use-controlled-state"; import { useFormState } from "@/hooks/use-form-state"; import { type Note, type NoteFormValues, sortTasksByStatus, type Task, } from "./types"; type NoteDialogMode = "create" | "update"; interface NoteDialogProps { mode: NoteDialogMode; trigger?: ReactNode; note?: Note; open?: boolean; onOpenChange?: (open: boolean) => void; } const MAX_TITLE = 30; const MAX_DESC = 350; const normalize = (s: string) => s.replace(/\s+/g, " ").trim(); const buildInitialValues = (note?: Note): NoteFormValues => ({ title: note?.title ?? "", description: note?.description ?? "", type: note?.type ?? "nota", tasks: note?.tasks ?? [], }); const generateTaskId = () => { return `task-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; }; export function NoteDialog({ mode, trigger, note, open, onOpenChange, }: NoteDialogProps) { const [isPending, startTransition] = useTransition(); const [errorMessage, setErrorMessage] = useState(null); const [newTaskText, setNewTaskText] = useState(""); const titleRef = useRef(null); const descRef = useRef(null); const newTaskRef = useRef(null); // Use controlled state hook for dialog open state const [dialogOpen, setDialogOpen] = useControlledState( open, false, onOpenChange, ); const initialState = buildInitialValues(note); // Use form state hook for form management const { formState, updateField, setFormState } = useFormState(initialState); useEffect(() => { if (dialogOpen) { setFormState(buildInitialValues(note)); setErrorMessage(null); setNewTaskText(""); requestAnimationFrame(() => titleRef.current?.focus()); } }, [dialogOpen, note, setFormState]); const title = mode === "create" ? "Nova anotação" : "Editar anotação"; const submitLabel = mode === "create" ? "Salvar" : "Atualizar"; const titleCount = formState.title.length; const descCount = formState.description.length; const isNote = formState.type === "nota"; const sortedTasks = useMemo( () => sortTasksByStatus(formState.tasks || []), [formState.tasks], ); const onlySpaces = normalize(formState.title).length === 0 || (isNote && formState.description.trim().length === 0) || (!isNote && (!formState.tasks || formState.tasks.length === 0)); const invalidLen = titleCount > MAX_TITLE || descCount > MAX_DESC; const unchanged = mode === "update" && normalize(formState.title) === normalize(note?.title ?? "") && formState.description.trim() === (note?.description ?? "").trim() && JSON.stringify(formState.tasks) === JSON.stringify(note?.tasks); const disableSubmit = isPending || onlySpaces || unchanged || invalidLen; const handleOpenChange = (v: boolean) => { setDialogOpen(v); if (!v) setErrorMessage(null); }; const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") (e.currentTarget as HTMLFormElement).requestSubmit(); if (e.key === "Escape") handleOpenChange(false); }; const handleAddTask = () => { const text = normalize(newTaskText); if (!text) return; const newTask: Task = { id: generateTaskId(), text, completed: false, }; updateField("tasks", [...(formState.tasks || []), newTask]); setNewTaskText(""); requestAnimationFrame(() => newTaskRef.current?.focus()); }; const handleRemoveTask = (taskId: string) => { updateField( "tasks", (formState.tasks || []).filter((t) => t.id !== taskId), ); }; const handleToggleTask = (taskId: string) => { updateField( "tasks", (formState.tasks || []).map((t) => t.id === taskId ? { ...t, completed: !t.completed } : t, ), ); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setErrorMessage(null); const payload = { title: normalize(formState.title), description: formState.description.trim(), type: formState.type, tasks: formState.tasks, }; if (onlySpaces || invalidLen) { setErrorMessage("Preencha os campos respeitando os limites."); titleRef.current?.focus(); return; } if (mode === "update" && !note?.id) { const msg = "Não foi possível identificar a anotação a ser editada."; setErrorMessage(msg); toast.error(msg); return; } if (unchanged) { toast.info("Nada para atualizar."); return; } startTransition(async () => { let result: { success: boolean; message?: string; error?: string }; if (mode === "create") { result = await createNoteAction(payload); } else { if (!note?.id) { const msg = "ID da anotação não encontrado."; setErrorMessage(msg); toast.error(msg); return; } result = await updateNoteAction({ id: note.id, ...payload }); } if (result.success) { toast.success(result.message); setDialogOpen(false); return; } setErrorMessage(result.error); toast.error(result.error); titleRef.current?.focus(); }); }; return ( {trigger ? {trigger} : null} {title} {mode === "create" ? "Criar nova anotação" : "Editar anotação existente"}
{mode === "create" && ( updateField("type", value as "nota" | "tarefa") } disabled={isPending} className="flex gap-4" >
)} updateField("title", e.target.value)} placeholder="Título" maxLength={MAX_TITLE} disabled={isPending} required /> {isNote && (