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 ( -
- -
{children}
-
- ); + return ( +
+ +
+ +
{children}
+
+
+ ); } 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 ( -
- - Logo - - {tabs.map((tab, i) => { - const TabIcon = tab.icon; - return ( - - -
{tab.name}
- - ); - })} - - -
Sign Out
- -
- ); + return ( +
+ + Logo + + {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",