diff --git a/dashboard/15-final/app/dashboard/layout.tsx b/dashboard/15-final/app/dashboard/layout.tsx
index 03d4b88..12c6697 100644
--- a/dashboard/15-final/app/dashboard/layout.tsx
+++ b/dashboard/15-final/app/dashboard/layout.tsx
@@ -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 (
-
- );
+ return (
+
+ );
}
diff --git a/dashboard/15-final/app/dashboard/page.tsx b/dashboard/15-final/app/dashboard/page.tsx
index 3f609d4..f6953bf 100644
--- a/dashboard/15-final/app/dashboard/page.tsx
+++ b/dashboard/15-final/app/dashboard/page.tsx
@@ -1,3 +1,9 @@
+import DashboardOverview from "@/app/ui/dashboard-overview";
+
export default function Page() {
- return Dashboard Overview
+ return (
+
+
+
+ );
}
diff --git a/dashboard/15-final/app/layout.tsx b/dashboard/15-final/app/layout.tsx
index db09ef8..cc89e10 100644
--- a/dashboard/15-final/app/layout.tsx
+++ b/dashboard/15-final/app/layout.tsx
@@ -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 (
{children}
- )
+ );
}
diff --git a/dashboard/15-final/app/lib/calculations.tsx b/dashboard/15-final/app/lib/calculations.tsx
new file mode 100644
index 0000000..091633c
--- /dev/null
+++ b/dashboard/15-final/app/lib/calculations.tsx
@@ -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",
+ });
+};
diff --git a/dashboard/15-final/app/lib/dummy-data.tsx b/dashboard/15-final/app/lib/dummy-data.tsx
index d2a331d..5be5b0b 100644
--- a/dashboard/15-final/app/lib/dummy-data.tsx
+++ b/dashboard/15-final/app/lib/dummy-data.tsx
@@ -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",
},
-]
+];
diff --git a/dashboard/15-final/app/ui/card.tsx b/dashboard/15-final/app/ui/card.tsx
new file mode 100644
index 0000000..bb43e8a
--- /dev/null
+++ b/dashboard/15-final/app/ui/card.tsx
@@ -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 (
+
+
+
{title}
+
{value}
+
+00% since last month
+
+ {Icon ? (
+
+ ) : null}
+
+ );
+}
diff --git a/dashboard/15-final/app/ui/dashboard-overview.tsx b/dashboard/15-final/app/ui/dashboard-overview.tsx
new file mode 100644
index 0000000..32f0361
--- /dev/null
+++ b/dashboard/15-final/app/ui/dashboard-overview.tsx
@@ -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 (
+
+
+
+
+
+
+ );
+}
diff --git a/dashboard/15-final/app/ui/dashboard-sidenav.tsx b/dashboard/15-final/app/ui/dashboard-sidenav.tsx
index c200510..e8c7126 100644
--- a/dashboard/15-final/app/ui/dashboard-sidenav.tsx
+++ b/dashboard/15-final/app/ui/dashboard-sidenav.tsx
@@ -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 (
-
-
-
-
- {tabs.map((tab, i) => {
- const TabIcon = tab.icon;
- return (
-
-
-
{tab.name}
-
- );
- })}
-
-
-
Sign Out
-
-
- );
+ return (
+
+
+
+
+ {tabs.map((tab, i) => {
+ const TabIcon = tab.icon;
+ return (
+
+
+
{tab.name}
+
+ );
+ })}
+
+
+
Sign Out
+
+
+ );
}
diff --git a/dashboard/15-final/app/ui/dashboard-topnav.tsx b/dashboard/15-final/app/ui/dashboard-topnav.tsx
new file mode 100644
index 0000000..2da596e
--- /dev/null
+++ b/dashboard/15-final/app/ui/dashboard-topnav.tsx
@@ -0,0 +1,3 @@
+export default function TopNav() {
+ return Search
;
+}
diff --git a/dashboard/15-final/app/ui/login-form.tsx b/dashboard/15-final/app/ui/login-form.tsx
index 1ca08a6..c5a51d5 100644
--- a/dashboard/15-final/app/ui/login-form.tsx
+++ b/dashboard/15-final/app/ui/login-form.tsx
@@ -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) => {
e.preventDefault();
console.log(`Email: ${email}, Password: ${password}`);
};
diff --git a/dashboard/15-final/package-lock.json b/dashboard/15-final/package-lock.json
index c357c8c..d84b405 100644
--- a/dashboard/15-final/package-lock.json
+++ b/dashboard/15-final/package-lock.json
@@ -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",
diff --git a/dashboard/15-final/package.json b/dashboard/15-final/package.json
index e2d852b..325aec1 100644
--- a/dashboard/15-final/package.json
+++ b/dashboard/15-final/package.json
@@ -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",