Skip to content

Commit

Permalink
feat: ✨ add summary cards
Browse files Browse the repository at this point in the history
  • Loading branch information
neopromic committed Nov 11, 2024
1 parent 929e5cd commit b85fb1d
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 27 deletions.
44 changes: 44 additions & 0 deletions app/(authenticated)/dashboard/_components/summar-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import AddTransactionButton from "@/app/_components/add-transaction-button";
import { Card, CardContent, CardHeader } from "@/app/_components/ui/card";
import { cn } from "@/app/_lib/utils";

interface SummaryCardProps {
title: string;
amount: string;
icon: React.ReactNode;
size?: "sm" | "lg";
}

const SummaryCard = ({
title,
amount,
icon,
size = "sm",
}: SummaryCardProps) => {
return (
<Card>
<CardHeader className="flex-row items-center gap-2">
{icon}
<p
className={cn(
"text-muted-foreground",
size === "sm" ? "text-muted-foreground" : "text-white opacity-70",
)}
>
{title}
</p>
</CardHeader>
<CardContent className="flex justify-between">
<p className={`font-bold ${size === "sm" ? "text-2xl" : "text-4xl"}`}>
{Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(Number(amount))}
</p>
{size === "lg" && <AddTransactionButton />}
</CardContent>
</Card>
);
};

export default SummaryCard;
87 changes: 87 additions & 0 deletions app/(authenticated)/dashboard/_components/summary-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
PiggyBankIcon,
TrendingDownIcon,
TrendingUpIcon,
WalletIcon,
} from "lucide-react";
import SummaryCard from "./summar-card";
import { db } from "@/app/_lib/prisma";

interface SummaryCardsProps {
month: string;
}

const SummaryCards = async ({ month }: SummaryCardsProps) => {
const where = {
date: {
gte: new Date(`2024-${month}-01`),
lt: new Date(`2024-${month}-31`),
},
};

const deposits = await db.transactions.aggregate({
where: {
type: "DEPOSIT",
...where,
},
_sum: {
amount: true,
},
});
const depositsTotal = deposits._sum.amount ?? 0;

const investments = await db.transactions.aggregate({
where: {
type: "INVESTMENT",
...where,
},
_sum: {
amount: true,
},
});
const investmentsTotal = investments._sum.amount ?? 0;

const expenses = await db.transactions.aggregate({
where: {
type: "EXPENSE",
...where,
},
_sum: {
amount: true,
},
});
const expensesTotal = expenses._sum.amount ?? 0;

const balance =
Number(depositsTotal) - Number(investmentsTotal) - Number(expensesTotal);

return (
<div className="space-y-6">
<SummaryCard
title="Saldo"
amount={balance.toString()}
icon={<WalletIcon size={16} />}
size="lg"
/>
<div className="grid grid-cols-3 gap-6">
<SummaryCard
title="Investido"
amount={investmentsTotal.toString()}
icon={<PiggyBankIcon size={16} />}
/>
<SummaryCard
title="Receita"
amount={depositsTotal.toString()}
icon={<TrendingUpIcon size={16} className="text-primary" />}
/>
<SummaryCard
title="Despesas"
amount={expensesTotal.toString()}
icon={<TrendingDownIcon size={16} className="text-danger" />}
/>
</div>
</div>
);
};

export default SummaryCards;
57 changes: 57 additions & 0 deletions app/(authenticated)/dashboard/_components/time-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/app/_components/ui/select";
import { useRouter, useSearchParams } from "next/navigation";

interface TimeSelectProps {
defaultValue?: string;
}

const MONTH_OPTIONS = [
{ value: "1", label: "Janeiro" },
{ value: "2", label: "Fevereiro" },
{ value: "3", label: "Março" },
{ value: "4", label: "Abril" },
{ value: "5", label: "Maio" },
{ value: "6", label: "Junho" },
{ value: "7", label: "Julho" },
{ value: "8", label: "Agosto" },
{ value: "9", label: "Setembro" },
{ value: "10", label: "Outubro" },
{ value: "11", label: "Novembro" },
{ value: "12", label: "Dezembro" },
];

const TimeSelect = ({ defaultValue = "1" }: TimeSelectProps) => {
const router = useRouter();
const searchParams = useSearchParams();

const handleMonthChange = (month: string) => {
const params = new URLSearchParams(searchParams);
params.set("month", month);
router.push(`/dashboard?${params.toString()}`);
};

return (
<Select defaultValue={defaultValue} onValueChange={handleMonthChange}>
<SelectTrigger className="w-[150px] rounded-full">
<SelectValue placeholder="Selecione um mês" />
</SelectTrigger>
<SelectContent>
{MONTH_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
};

export default TimeSelect;
38 changes: 38 additions & 0 deletions app/(authenticated)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
import SummaryCards from "./_components/summary-cards";
import TimeSelect from "./_components/time-select";

interface HomeProps {
searchParams: {
month?: string;
};
}

const Home = async ({ searchParams: { month = "1" } }: HomeProps) => {
const { userId } = await auth();

if (!userId) {
redirect("/login");
}

// Validação do mês
const monthNumber = Number(month);
const isValidMonth = monthNumber >= 1 && monthNumber <= 12;

if (!isValidMonth) {
redirect("/dashboard?month=1");
}

return (
<div className="space-y-6 p-6">
<div className="flex justify-between">
<h1 className="text-2xl font-bold">Dashboard</h1>
<TimeSelect defaultValue={month} />
</div>
<SummaryCards month={month} />
</div>
);
};

export default Home;
18 changes: 0 additions & 18 deletions app/(authenticated)/page.tsx

This file was deleted.

8 changes: 4 additions & 4 deletions app/_components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Header = ({ isEnabled = true }: { isEnabled?: boolean }) => {
const navigation = [
{
label: "Dashboard",
href: "/",
href: "/dashboard",
},
{
label: "Transações",
Expand All @@ -28,7 +28,7 @@ const Header = ({ isEnabled = true }: { isEnabled?: boolean }) => {
return (
<header className="sticky left-0 top-0 z-50 flex h-[72px] items-center justify-between border-b bg-background px-6">
<div className="flex items-center gap-8">
<Link href="/">
<Link href="/dashboard">
<Image src="/logo.svg" alt="logo" width={173} height={32} />
</Link>

Expand All @@ -39,8 +39,8 @@ const Header = ({ isEnabled = true }: { isEnabled?: boolean }) => {
href={item.href}
className={`text-sm transition-colors ${
pathname === item.href
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
? "font-medium text-primary"
: "text-muted-foreground hover:text-primary"
}`}
>
{item.label}
Expand Down
86 changes: 86 additions & 0 deletions app/_components/ui/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as React from "react";

import { cn } from "@/app/_lib/utils";

const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";

const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";

const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";

const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";

const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";

const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";

export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
2 changes: 1 addition & 1 deletion app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function GlobalError({
Tentar novamente
</Button>
<Button
onClick={() => (window.location.href = "/")}
onClick={() => (window.location.href = "/dashboard")}
className="gap-2"
>
<Home className="h-4 w-4" />
Expand Down
3 changes: 1 addition & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ const mulish = Mulish({
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Finance AI",
};

export default function RootLayout({
Expand Down
4 changes: 2 additions & 2 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Image from "next/image";
import { Button } from "../_components/ui/button";
import { Button } from "@/app/_components/ui/button";
import { LogInIcon } from "lucide-react";
import { SignInButton } from "@clerk/nextjs";
import { auth } from "@clerk/nextjs/server";
Expand All @@ -9,7 +9,7 @@ const Login = async () => {
const { userId } = await auth();

if (userId) {
redirect("/");
redirect("/dashboard");
}

return (
Expand Down
Loading

0 comments on commit b85fb1d

Please sign in to comment.