Add card component and calculations (#130)

* Run create-next-app

* Add READMEs

* Remove stuff we don't need

* Add dummy data

* Add types for dummy data

* Add dummy routes

* Remove unused CSS

* Split dummy data and definitions

* Add prettier plugin for tailwind

* Create background-blur.tsx

* Create hero.tsx

* Create login-form.tsx

* Tweak

* Install hero icons and clsx

* Create calculations.tsx

* Update dummy-data.tsx

* Create card.tsx

* Create dashboard-overview.tsx

* Update page.tsx

* Add placeholder for dashboard-topnav

* Adjust sizings for whole page

* Update card styles and add icons

* ugh, fonts are hard

* misc

* Remo unused import
This commit is contained in:
Delba de Oliveira
2023-08-31 14:50:05 +01:00
committed by GitHub
parent 24f9a5fa45
commit c0f63f4d25
12 changed files with 187 additions and 65 deletions

View File

@@ -1,10 +1,14 @@
import SideNav from '../ui/dashboard-sidenav';
import TopNav from "@/app/ui/dashboard-topnav";
import SideNav from "../ui/dashboard-sidenav";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen">
<SideNav />
<div>{children}</div>
</div>
);
return (
<div className="flex h-screen">
<SideNav />
<div className="flex-grow">
<TopNav />
<div className="p-4 sm:p-20">{children}</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,9 @@
import DashboardOverview from "@/app/ui/dashboard-overview";
export default function Page() {
return <div>Dashboard Overview</div>
return (
<main>
<DashboardOverview />
</main>
);
}

View File

@@ -1,21 +1,21 @@
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app'
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`bg-white ${inter.className}`}>{children}</body>
</html>
)
);
}

View File

@@ -0,0 +1,14 @@
import { Invoice } from "./definitions";
export const calculateInvoices = (
invoices: Invoice[],
status: "pending" | "paid",
) => {
return invoices
.filter((invoice) => !status || invoice.status === status)
.reduce((total, invoice) => total + invoice.amount / 100, 0)
.toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
};

View File

@@ -1,4 +1,4 @@
import { User, Customer, Invoice } from "./definitions"
import { User, Customer, Invoice } from "./definitions";
// This file contains dummy data that you'll be replacing with real data in Chapter 7.
export const users: User[] = [
@@ -8,7 +8,7 @@ export const users: User[] = [
email: "user@nextmail.com",
password: "123456",
},
]
];
export const customers: Customer[] = [
{
@@ -31,31 +31,31 @@ export const customers: Customer[] = [
name: "Delba",
email: "delba@nextmail.com",
},
]
];
export const invoices: Invoice[] = [
{
id: 1,
customerId: 1,
amount: 10000,
amount: 15795,
status: "pending",
},
{
id: 2,
customerId: 2,
amount: 20000,
amount: 20348,
status: "pending",
},
{
id: 3,
customerId: 3,
amount: 30000,
amount: 3040,
status: "paid",
},
{
id: 4,
customerId: 4,
amount: 40000,
amount: 44800,
status: "paid",
},
]
];

View File

@@ -0,0 +1,38 @@
import {
BanknotesIcon,
ClockIcon,
UserGroupIcon,
InboxIcon,
} from "@heroicons/react/24/outline";
const iconMap = {
collected: BanknotesIcon,
customers: UserGroupIcon,
pending: ClockIcon,
invoices: InboxIcon,
};
export default function Card({
title,
value,
type,
}: {
title: string;
value: number | string;
type: "invoices" | "customers" | "pending" | "collected";
}) {
const Icon = iconMap[type];
return (
<div className="flex justify-between rounded-xl border bg-white p-6 shadow-sm">
<div>
<h3 className="text-sm font-medium">{title}</h3>
<p className="mt-2 text-3xl font-semibold tracking-wide">{value}</p>
<p className="mt-1.5 text-sm text-zinc-400">+00% since last month</p>
</div>
{Icon ? (
<Icon className="h-5 w-5 text-zinc-700" aria-label={type} />
) : null}
</div>
);
}

View File

@@ -0,0 +1,23 @@
import Card from "@/app/ui/card";
import { invoices, customers } from "@/app/lib/dummy-data";
import { calculateInvoices } from "@/app/lib/calculations";
export default function DashboardOverview() {
const totalPaidInvoices = calculateInvoices(invoices, "paid");
const totalPendingInvoices = calculateInvoices(invoices, "pending");
const numberOfInvoices = invoices.length;
const numberOfCustomers = customers.length;
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-4">
<Card title="Collected" value={totalPaidInvoices} type="collected" />
<Card title="Pending" value={totalPendingInvoices} type="pending" />
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
<Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/>
</div>
);
}

View File

@@ -1,47 +1,63 @@
'use client';
"use client";
import { UserGroupIcon, HomeIcon, InboxIcon, PowerIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
UserGroupIcon,
HomeIcon,
InboxIcon,
PowerIcon,
} from "@heroicons/react/24/outline";
import Link from "next/link";
import { usePathname } from "next/navigation";
import clsx from "clsx";
import Image from 'next/image';
import Image from "next/image";
export default function SideNav() {
const pathname = usePathname();
const pathname = usePathname();
const tabs = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
{ name: 'Invoices', href: '/dashboard/invoices', icon: InboxIcon }
];
const tabs = [
{ name: "Home", href: "/dashboard", icon: HomeIcon },
{ name: "Customers", href: "/dashboard/customers", icon: UserGroupIcon },
{ name: "Invoices", href: "/dashboard/invoices", icon: InboxIcon },
];
return (
<div className="flex flex-col p-1 md:p-4 w-12 md:w-64 border-r h-full">
<Link href="/">
<Image priority src="/logo.svg" height={80} width={80} alt="Logo" className="mb-4 md:mb-6" />
</Link>
{tabs.map((tab, i) => {
const TabIcon = tab.icon;
return (
<Link
key={i}
href={tab.href}
className={`mb-2 ${
pathname === tab.href ? 'bg-gray-100 text-blue-600' : ' text-gray-600'
} hover:text-blue-600 hover:bg-gray-100 flex rounded p-2 text-sm font-semibold`}
>
<TabIcon className="h-5 w-5 md:mr-2" />
<div className="hidden md:block">{tab.name}</div>
</Link>
);
})}
<Link
href="/login"
className="flex mt-auto hover:text-blue-600 rounded p-2 text-sm font-semibold"
>
<PowerIcon className="h-5 w-5 md:mr-2" />
<div className="hidden md:block">Sign Out</div>
</Link>
</div>
);
return (
<div className="flex h-full w-12 flex-col border-r p-1 md:w-72 md:p-4">
<Link href="/">
<Image
priority
src="/logo.svg"
height={100}
width={100}
alt="Logo"
className="mb-6 mt-4"
/>
</Link>
{tabs.map((tab, i) => {
const TabIcon = tab.icon;
return (
<Link
key={i}
href={tab.href}
className={clsx(
"mb-2 flex rounded p-2 font-semibold hover:bg-gray-100 hover:text-blue-600",
{
"bg-gray-100 text-blue-600": pathname === tab.href,
},
)}
>
<TabIcon className="h-6 w-6 md:mr-2" />
<div className="hidden md:block">{tab.name}</div>
</Link>
);
})}
<Link
href="/login"
className="mt-auto flex rounded p-2 font-semibold hover:text-blue-600"
>
<PowerIcon className="h-6 w-6 md:mr-2" />
<div className="hidden md:block">Sign Out</div>
</Link>
</div>
);
}

View File

@@ -0,0 +1,3 @@
export default function TopNav() {
return <div className="h-16 border-b">Search</div>;
}

View File

@@ -10,7 +10,7 @@ export default function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e: { preventDefault: () => void }) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(`Email: ${email}, Password: ${password}`);
};

View File

@@ -13,6 +13,7 @@
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"clsx": "^2.0.0",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19",
"next": "13.4.19",
@@ -25,6 +26,9 @@
"devDependencies": {
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -985,6 +989,14 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4711,6 +4723,11 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",

View File

@@ -17,6 +17,7 @@
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"clsx": "^2.0.0",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19",
"next": "13.4.19",