feat: adição de novos ícones SVG e configuração do ambiente

- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter
- Implementados ícones para modos claro e escuro do ChatGPT
- Criado script de inicialização para PostgreSQL com extensão pgcrypto
- Adicionado script de configuração de ambiente que faz backup do .env
- Configurado tsconfig.json para TypeScript com opções de compilação
This commit is contained in:
Felipe Coutinho
2025-11-15 15:49:36 -03:00
commit ea0b8618e0
441 changed files with 53569 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { RiDeleteBin5Line, RiEyeLine, RiPencilLine } from "@remixicon/react";
import { CheckIcon } from "lucide-react";
import { useMemo } from "react";
import type { Note } from "./types";
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
dateStyle: "medium",
});
interface NoteCardProps {
note: Note;
onEdit?: (note: Note) => void;
onDetails?: (note: Note) => void;
onRemove?: (note: Note) => void;
}
export function NoteCard({ note, onEdit, onDetails, onRemove }: NoteCardProps) {
const { formattedDate, displayTitle } = useMemo(() => {
const resolvedTitle = note.title.trim().length
? note.title
: "Anotação sem título";
return {
displayTitle: resolvedTitle,
formattedDate: DATE_FORMATTER.format(new Date(note.createdAt)),
};
}, [note.createdAt, note.title]);
const isTask = note.type === "tarefa";
const tasks = note.tasks || [];
const completedCount = tasks.filter((t) => t.completed).length;
const totalCount = tasks.length;
const actions = [
{
label: "editar",
icon: <RiPencilLine className="size-4" aria-hidden />,
onClick: onEdit,
variant: "default" as const,
},
{
label: "detalhes",
icon: <RiEyeLine className="size-4" aria-hidden />,
onClick: onDetails,
variant: "default" as const,
},
{
label: "remover",
icon: <RiDeleteBin5Line className="size-4" aria-hidden />,
onClick: onRemove,
variant: "destructive" as const,
},
].filter((action) => typeof action.onClick === "function");
return (
<Card className="h-[300px] w-[440px] gap-0">
<CardContent className="flex flex-1 flex-col gap-4">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-2">
<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>
</div>
{isTask ? (
<div className="flex-1 overflow-auto space-y-2">
{tasks.slice(0, 4).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 ${
task.completed
? "bg-green-600 border-green-600"
: "border-input"
}`}
>
{task.completed && (
<CheckIcon className="h-3 w-3 text-background" />
)}
</div>
<span
className={`leading-relaxed ${
task.completed
? "line-through text-muted-foreground"
: "text-foreground"
}`}
>
{task.text}
</span>
</div>
))}
{tasks.length > 4 && (
<p className="text-xs text-muted-foreground pl-5 py-1">
+{tasks.length - 4}{" "}
{tasks.length - 4 === 1 ? "tarefa" : "tarefas"}...
</p>
)}
</div>
) : (
<p className="flex-1 overflow-auto whitespace-pre-line text-sm text-muted-foreground wrap-break-word leading-relaxed">
{note.description}
</p>
)}
</CardContent>
{actions.length > 0 ? (
<CardFooter className="flex flex-wrap gap-3 px-6 pt-3 text-sm">
{actions.map(({ label, icon, onClick, variant }) => (
<button
key={label}
type="button"
onClick={() => onClick?.(note)}
className={`flex items-center gap-1 font-medium transition-opacity hover:opacity-80 ${
variant === "destructive" ? "text-destructive" : "text-primary"
}`}
aria-label={`${label} anotação`}
>
{icon}
{label}
</button>
))}
</CardFooter>
) : null}
</Card>
);
}