feat: adicionar página de anotações arquivadas e componente de notificação
- Implementa a página de anotações arquivadas, que busca as notas arquivadas do usuário e as exibe utilizando o componente NotesPage. - Cria o componente NotificationBell para gerenciar e exibir notificações de pagamentos, incluindo a formatação de datas e valores monetários. O componente também apresenta um sistema de tooltip e dropdown para interação do usuário.
This commit is contained in:
@@ -3,9 +3,11 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
RiArchiveLine,
|
||||
RiCheckLine,
|
||||
RiDeleteBin5Line,
|
||||
RiEyeLine,
|
||||
RiInboxUnarchiveLine,
|
||||
RiPencilLine,
|
||||
} from "@remixicon/react";
|
||||
import { useMemo } from "react";
|
||||
@@ -20,9 +22,18 @@ interface NoteCardProps {
|
||||
onEdit?: (note: Note) => void;
|
||||
onDetails?: (note: Note) => void;
|
||||
onRemove?: (note: Note) => void;
|
||||
onArquivar?: (note: Note) => void;
|
||||
isArquivadas?: boolean;
|
||||
}
|
||||
|
||||
export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
||||
export function NoteCard({
|
||||
note,
|
||||
onEdit,
|
||||
onDetails,
|
||||
onRemove,
|
||||
onArquivar,
|
||||
isArquivadas = false,
|
||||
}: NoteCardProps) {
|
||||
const { formattedDate, displayTitle } = useMemo(() => {
|
||||
const resolvedTitle = note.title.trim().length
|
||||
? note.title
|
||||
@@ -52,6 +63,16 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
||||
onClick: onDetails,
|
||||
variant: "default" as const,
|
||||
},
|
||||
{
|
||||
label: isArquivadas ? "desarquivar" : "arquivar",
|
||||
icon: isArquivadas ? (
|
||||
<RiInboxUnarchiveLine className="size-4" aria-hidden />
|
||||
) : (
|
||||
<RiArchiveLine className="size-4" aria-hidden />
|
||||
),
|
||||
onClick: onArquivar,
|
||||
variant: "default" as const,
|
||||
},
|
||||
{
|
||||
label: "remover",
|
||||
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
|
||||
@@ -68,20 +89,17 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
||||
<h3 className="text-lg font-semibold leading-tight text-foreground wrap-break-word">
|
||||
{displayTitle}
|
||||
</h3>
|
||||
{isTask && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{completedCount}/{totalCount} concluídas
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground whitespace-nowrap">
|
||||
{formattedDate}
|
||||
</span>
|
||||
{isTask && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{completedCount}/{totalCount} concluídas
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isTask ? (
|
||||
<div className="flex-1 overflow-auto space-y-2">
|
||||
{tasks.slice(0, 4).map((task) => (
|
||||
<div className="flex-1 overflow-auto space-y-2 mt-2">
|
||||
{tasks.slice(0, 5).map((task) => (
|
||||
<div key={task.id} className="flex items-start gap-2 text-sm">
|
||||
<div
|
||||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
||||
@@ -96,24 +114,22 @@ export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
|
||||
</div>
|
||||
<span
|
||||
className={`leading-relaxed ${
|
||||
task.completed
|
||||
? "line-through text-muted-foreground"
|
||||
: "text-foreground"
|
||||
task.completed ? "text-muted-foreground" : "text-foreground"
|
||||
}`}
|
||||
>
|
||||
{task.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{tasks.length > 4 && (
|
||||
{tasks.length > 5 && (
|
||||
<p className="text-xs text-muted-foreground pl-5 py-1">
|
||||
+{tasks.length - 4}{" "}
|
||||
{tasks.length - 4 === 1 ? "tarefa" : "tarefas"}...
|
||||
+{tasks.length - 5}
|
||||
{tasks.length - 5 === 1 ? "tarefa" : "tarefas"}...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="flex-1 overflow-auto whitespace-pre-line text-sm text-muted-foreground wrap-break-word leading-relaxed">
|
||||
<p className="flex-1 overflow-auto whitespace-pre-line text-sm text-muted-foreground wrap-break-word leading-relaxed mt-2">
|
||||
{note.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { RiCheckLine } from "@remixicon/react";
|
||||
import { useMemo } from "react";
|
||||
import { Card } from "../ui/card";
|
||||
import type { Note } from "./types";
|
||||
|
||||
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
|
||||
@@ -71,9 +72,9 @@ export function NoteDetailsDialog({
|
||||
{isTask ? (
|
||||
<div className="max-h-[320px] overflow-auto space-y-3">
|
||||
{tasks.map((task) => (
|
||||
<div
|
||||
<Card
|
||||
key={task.id}
|
||||
className="flex items-start gap-3 p-3 rounded-lg border bg-card"
|
||||
className="flex gap-3 p-3 flex-row items-center"
|
||||
>
|
||||
<div
|
||||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
||||
@@ -88,14 +89,12 @@ export function NoteDetailsDialog({
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm ${
|
||||
task.completed
|
||||
? "line-through text-muted-foreground"
|
||||
: "text-foreground"
|
||||
task.completed ? "text-muted-foreground" : "text-foreground"
|
||||
}`}
|
||||
>
|
||||
{task.text}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Card } from "../ui/card";
|
||||
import type { Note, NoteFormValues, Task } from "./types";
|
||||
|
||||
type NoteDialogMode = "create" | "update";
|
||||
@@ -404,11 +405,12 @@ export function NoteDialog({
|
||||
</label>
|
||||
<div className="space-y-2 max-h-[240px] overflow-y-auto pr-1">
|
||||
{formState.tasks.map((task) => (
|
||||
<div
|
||||
<Card
|
||||
key={task.id}
|
||||
className="flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
||||
className="flex items-center gap-3 px-3 py-2 flex-row mt-1"
|
||||
>
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-green-600 data-[state=checked]:border-green-600"
|
||||
checked={task.completed}
|
||||
onCheckedChange={() => handleToggleTask(task.id)}
|
||||
disabled={isPending}
|
||||
@@ -419,7 +421,7 @@ export function NoteDialog({
|
||||
<span
|
||||
className={`flex-1 text-sm wrap-break-word ${
|
||||
task.completed
|
||||
? "line-through text-muted-foreground"
|
||||
? "text-muted-foreground"
|
||||
: "text-foreground"
|
||||
}`}
|
||||
>
|
||||
@@ -436,7 +438,7 @@ export function NoteDialog({
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { deleteNoteAction } from "@/app/(dashboard)/anotacoes/actions";
|
||||
import { arquivarAnotacaoAction, deleteNoteAction } from "@/app/(dashboard)/anotacoes/actions";
|
||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,9 +15,10 @@ import type { Note } from "./types";
|
||||
|
||||
interface NotesPageProps {
|
||||
notes: Note[];
|
||||
isArquivadas?: boolean;
|
||||
}
|
||||
|
||||
export function NotesPage({ notes }: NotesPageProps) {
|
||||
export function NotesPage({ notes, isArquivadas = false }: NotesPageProps) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [noteToEdit, setNoteToEdit] = useState<Note | null>(null);
|
||||
@@ -25,6 +26,8 @@ export function NotesPage({ notes }: NotesPageProps) {
|
||||
const [noteDetails, setNoteDetails] = useState<Note | null>(null);
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const [noteToRemove, setNoteToRemove] = useState<Note | null>(null);
|
||||
const [arquivarOpen, setArquivarOpen] = useState(false);
|
||||
const [noteToArquivar, setNoteToArquivar] = useState<Note | null>(null);
|
||||
|
||||
const sortedNotes = useMemo(
|
||||
() =>
|
||||
@@ -60,6 +63,13 @@ export function NotesPage({ notes }: NotesPageProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleArquivarOpenChange = useCallback((open: boolean) => {
|
||||
setArquivarOpen(open);
|
||||
if (!open) {
|
||||
setNoteToArquivar(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEditRequest = useCallback((note: Note) => {
|
||||
setNoteToEdit(note);
|
||||
setEditOpen(true);
|
||||
@@ -75,6 +85,30 @@ export function NotesPage({ notes }: NotesPageProps) {
|
||||
setRemoveOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleArquivarRequest = useCallback((note: Note) => {
|
||||
setNoteToArquivar(note);
|
||||
setArquivarOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleArquivarConfirm = useCallback(async () => {
|
||||
if (!noteToArquivar) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await arquivarAnotacaoAction({
|
||||
id: noteToArquivar.id,
|
||||
arquivada: !isArquivadas,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}, [noteToArquivar, isArquivadas]);
|
||||
|
||||
const handleRemoveConfirm = useCallback(async () => {
|
||||
if (!noteToRemove) {
|
||||
return;
|
||||
@@ -97,29 +131,51 @@ export function NotesPage({ notes }: NotesPageProps) {
|
||||
: "Remover anotação?"
|
||||
: "Remover anotação?";
|
||||
|
||||
const arquivarTitle = noteToArquivar
|
||||
? noteToArquivar.title.trim().length
|
||||
? isArquivadas
|
||||
? `Desarquivar anotação "${noteToArquivar.title}"?`
|
||||
: `Arquivar anotação "${noteToArquivar.title}"?`
|
||||
: isArquivadas
|
||||
? "Desarquivar anotação?"
|
||||
: "Arquivar anotação?"
|
||||
: isArquivadas
|
||||
? "Desarquivar anotação?"
|
||||
: "Arquivar anotação?";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="flex justify-start">
|
||||
<NoteDialog
|
||||
mode="create"
|
||||
open={createOpen}
|
||||
onOpenChange={handleCreateOpenChange}
|
||||
trigger={
|
||||
<Button>
|
||||
<RiAddCircleLine className="size-4" />
|
||||
Nova anotação
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!isArquivadas && (
|
||||
<div className="flex justify-start">
|
||||
<NoteDialog
|
||||
mode="create"
|
||||
open={createOpen}
|
||||
onOpenChange={handleCreateOpenChange}
|
||||
trigger={
|
||||
<Button>
|
||||
<RiAddCircleLine className="size-4" />
|
||||
Nova anotação
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sortedNotes.length === 0 ? (
|
||||
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
|
||||
<EmptyState
|
||||
media={<RiTodoLine className="size-6 text-primary" />}
|
||||
title="Nenhuma anotação registrada"
|
||||
description="Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
|
||||
title={
|
||||
isArquivadas
|
||||
? "Nenhuma anotação arquivada"
|
||||
: "Nenhuma anotação registrada"
|
||||
}
|
||||
description={
|
||||
isArquivadas
|
||||
? "As anotações arquivadas aparecerão aqui."
|
||||
: "Crie anotações personalizadas para acompanhar lembretes, decisões ou observações financeiras importantes."
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
@@ -131,6 +187,8 @@ export function NotesPage({ notes }: NotesPageProps) {
|
||||
onEdit={handleEditRequest}
|
||||
onDetails={handleDetailsRequest}
|
||||
onRemove={handleRemoveRequest}
|
||||
onArquivar={handleArquivarRequest}
|
||||
isArquivadas={isArquivadas}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -150,6 +208,21 @@ export function NotesPage({ notes }: NotesPageProps) {
|
||||
onOpenChange={handleDetailsOpenChange}
|
||||
/>
|
||||
|
||||
<ConfirmActionDialog
|
||||
open={arquivarOpen}
|
||||
onOpenChange={handleArquivarOpenChange}
|
||||
title={arquivarTitle}
|
||||
description={
|
||||
isArquivadas
|
||||
? "A anotação será movida de volta para a lista principal."
|
||||
: "A anotação será movida para arquivadas."
|
||||
}
|
||||
confirmLabel={isArquivadas ? "Desarquivar" : "Arquivar"}
|
||||
confirmVariant="default"
|
||||
pendingLabel={isArquivadas ? "Desarquivando..." : "Arquivando..."}
|
||||
onConfirm={handleArquivarConfirm}
|
||||
/>
|
||||
|
||||
<ConfirmActionDialog
|
||||
open={removeOpen}
|
||||
onOpenChange={handleRemoveOpenChange}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Note {
|
||||
description: string;
|
||||
type: NoteType;
|
||||
tasks?: Task[];
|
||||
arquivada: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user