"use client"; 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 { PlusIcon, Trash2Icon } from "lucide-react"; import { type ReactNode, useCallback, useEffect, useRef, useState, useTransition, } from "react"; import { toast } from "sonner"; import type { Note, NoteFormValues, 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 description = mode === "create" ? "Escolha entre uma nota simples ou uma lista de tarefas." : "Altere o título e/ou conteúdo desta anotação."; const submitLabel = mode === "create" ? "Salvar anotação" : "Atualizar anotação"; const titleCount = formState.title.length; const descCount = formState.description.length; const isNote = formState.type === "nota"; const onlySpaces = normalize(formState.title).length === 0 || (isNote && normalize(formState.description).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 ?? "") && normalize(formState.description) === normalize(note?.description ?? "") && JSON.stringify(formState.tasks) === JSON.stringify(note?.tasks); const disableSubmit = isPending || onlySpaces || unchanged || invalidLen; const handleOpenChange = useCallback( (v: boolean) => { setDialogOpen(v); if (!v) setErrorMessage(null); }, [setDialogOpen] ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") (e.currentTarget as HTMLFormElement).requestSubmit(); if (e.key === "Escape") handleOpenChange(false); }, [handleOpenChange] ); const handleAddTask = useCallback(() => { 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()); }, [newTaskText, formState.tasks, updateField]); const handleRemoveTask = useCallback( (taskId: string) => { updateField( "tasks", (formState.tasks || []).filter((t) => t.id !== taskId) ); }, [formState.tasks, updateField] ); const handleToggleTask = useCallback( (taskId: string) => { updateField( "tasks", (formState.tasks || []).map((t) => t.id === taskId ? { ...t, completed: !t.completed } : t ) ); }, [formState.tasks, updateField] ); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); setErrorMessage(null); const payload = { title: normalize(formState.title), description: normalize(formState.description), 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; 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(); }); }, [ formState.title, formState.description, formState.type, formState.tasks, mode, note, setDialogOpen, onlySpaces, unchanged, invalidLen, ] ); return ( {trigger ? {trigger} : null} {title} {description}
{/* Seletor de Tipo - apenas no modo de criação */} {mode === "create" && (
updateField("type", value as "nota" | "tarefa") } disabled={isPending} className="flex gap-4" >
)} {/* Título */}
updateField("title", e.target.value)} placeholder={ isNote ? "Ex.: Revisar metas do mês" : "Ex.: Tarefas da semana" } maxLength={MAX_TITLE} disabled={isPending} aria-describedby="note-title-help" required />

Até {MAX_TITLE} caracteres. Restantes:{" "} {Math.max(0, MAX_TITLE - titleCount)}.

{/* Conteúdo - apenas para Notas */} {isNote && (