mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-11 09:51:47 +00:00
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:
committed by
GitHub
parent
24f9a5fa45
commit
c0f63f4d25
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import DashboardOverview from "@/app/ui/dashboard-overview";
|
||||
|
||||
export default function Page() {
|
||||
return <div>Dashboard Overview</div>
|
||||
return (
|
||||
<main>
|
||||
<DashboardOverview />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
14
dashboard/15-final/app/lib/calculations.tsx
Normal file
14
dashboard/15-final/app/lib/calculations.tsx
Normal 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",
|
||||
});
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
38
dashboard/15-final/app/ui/card.tsx
Normal file
38
dashboard/15-final/app/ui/card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
dashboard/15-final/app/ui/dashboard-overview.tsx
Normal file
23
dashboard/15-final/app/ui/dashboard-overview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
3
dashboard/15-final/app/ui/dashboard-topnav.tsx
Normal file
3
dashboard/15-final/app/ui/dashboard-topnav.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function TopNav() {
|
||||
return <div className="h-16 border-b">Search</div>;
|
||||
}
|
||||
@@ -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}`);
|
||||
};
|
||||
|
||||
17
dashboard/15-final/package-lock.json
generated
17
dashboard/15-final/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user