diff --git a/dashboard/15-final/app/dashboard/layout.tsx b/dashboard/15-final/app/dashboard/layout.tsx index 97baef4..0eb56e6 100644 --- a/dashboard/15-final/app/dashboard/layout.tsx +++ b/dashboard/15-final/app/dashboard/layout.tsx @@ -5,9 +5,9 @@ export default function Layout({ children }: { children: React.ReactNode }) { return (
-
+
-
{children}
+
{children}
); diff --git a/dashboard/15-final/app/lib/calculations.tsx b/dashboard/15-final/app/lib/calculations.tsx index 091633c..34ab6dc 100644 --- a/dashboard/15-final/app/lib/calculations.tsx +++ b/dashboard/15-final/app/lib/calculations.tsx @@ -1,4 +1,4 @@ -import { Invoice } from "./definitions"; +import { Invoice, Revenue } from "./definitions"; export const calculateInvoices = ( invoices: Invoice[], @@ -12,3 +12,28 @@ export const calculateInvoices = ( currency: "USD", }); }; + +// Once a database is connected, we can use SQL to query the database directly +// This will be more efficient than querying all invoices and then filtering them +// E.g. "SELECT * FROM invoices +// ORDER BY date DESC +// LIMIT 5;" +export const findLatestInvoices = (invoices: Invoice[]) => { + return [...invoices] + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, 5); +}; + +export const generateYAxis = (revenue: Revenue[]) => { + // Calculate what labels we need to display on the y-axis + // based on highest record and in 1000s + const yAxisLabels = []; + const highestRecord = Math.max(...revenue.map((month) => month.revenue)); + const topLabel = Math.ceil(highestRecord / 1000) * 1000; + + for (let i = topLabel; i >= 0; i -= 1000) { + yAxisLabels.push(`$${i / 1000}K`); + } + + return { yAxisLabels, topLabel }; +}; diff --git a/dashboard/15-final/app/lib/definitions.tsx b/dashboard/15-final/app/lib/definitions.tsx index 95c633a..0e4cb89 100644 --- a/dashboard/15-final/app/lib/definitions.tsx +++ b/dashboard/15-final/app/lib/definitions.tsx @@ -2,23 +2,29 @@ // These describe the shape of the data, and what data type each property should accept. export type User = { - id: number - name: string - email: string - password: string -} + id: number; + name: string; + email: string; + password: string; +}; export type Customer = { - id: number - name: string - email: string -} + id: number; + name: string; + email: string; + imageUrl: string; +}; export type Invoice = { - id: number - customerId: number - amount: number - date: string - status: "pending" | "paid" // In TypeScript, this is called a string union type. + id: number; + customerId: number; + amount: number; + status: "pending" | "paid"; // In TypeScript, this is called a string union type. // It means that the "status" property can only be one of the two strings. -} + date: string; +}; + +export type Revenue = { + month: string; + revenue: number; +}; diff --git a/dashboard/15-final/app/lib/dummy-data.tsx b/dashboard/15-final/app/lib/dummy-data.tsx index bf83355..3351802 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, Revenue } from "./definitions"; // This file contains dummy data that you'll be replacing with real data in Chapter 7. export const users: User[] = [ @@ -13,23 +13,27 @@ export const users: User[] = [ export const customers: Customer[] = [ { id: 1, - name: "Lee", - email: "lee@nextmail.com", + name: "Ada Lovelace", + email: "ada@earlycomputing.com", + imageUrl: "/customers/ada-lovelace.png", }, { id: 2, - name: "Michael", - email: "michael@nextmail.com", + name: "Grace Hopper", + email: "grace@personalcomputers.com", + imageUrl: "/customers/grace-hopper.png", }, { id: 3, - name: "Steph", - email: "steph@nextmail.com", + name: "Hedy Lammar", + email: "hedy@wifi.com", + imageUrl: "/customers/hedy-lammar.png", }, { id: 4, - name: "Delba", - email: "delba@nextmail.com", + name: "Margaret Hamilton", + email: "margaret@nasa.com", + imageUrl: "/customers/margaret-hamilton.png", }, ]; @@ -38,28 +42,64 @@ export const invoices: Invoice[] = [ id: 1, customerId: 1, amount: 15795, - date: "2021-01-01", status: "pending", + date: "2023-12-01", }, { id: 2, customerId: 2, amount: 20348, - date: "2021-02-01", status: "pending", + date: "2023-11-01", }, { id: 3, customerId: 3, amount: 3040, - date: "2021-03-01", status: "paid", + date: "2023-10-01", }, { id: 4, customerId: 4, amount: 44800, - date: "2021-04-01", status: "paid", + date: "2023-09-01", + }, + { + id: 5, + customerId: 1, + amount: 34577, + status: "pending", + date: "2023-08-01", + }, + { + id: 6, + customerId: 2, + amount: 54246, + status: "pending", + date: "2023-07-01", + }, + { + id: 7, + customerId: 3, + amount: 8945, + status: "paid", + date: "2023-06-01", }, ]; + +export const revenue: Revenue[] = [ + { month: "Jan", revenue: 2000 }, + { month: "Feb", revenue: 1800 }, + { month: "Mar", revenue: 2200 }, + { month: "Apr", revenue: 2500 }, + { month: "May", revenue: 2300 }, + { month: "Jun", revenue: 3200 }, + { month: "Jul", revenue: 3500 }, + { month: "Aug", revenue: 3700 }, + { month: "Sep", revenue: 2500 }, + { month: "Oct", revenue: 2800 }, + { month: "Nov", revenue: 3000 }, + { month: "Dec", revenue: 4800 }, +]; diff --git a/dashboard/15-final/app/ui/card.tsx b/dashboard/15-final/app/ui/card.tsx index bb43e8a..c3311b5 100644 --- a/dashboard/15-final/app/ui/card.tsx +++ b/dashboard/15-final/app/ui/card.tsx @@ -24,15 +24,17 @@ export default function Card({ const Icon = iconMap[type]; return ( -
-
+
+

{title}

-

{value}

-

+00% since last month

+ {Icon ? ( + + ) : null}
- {Icon ? ( - - ) : null} +

+ {value} +

+

+00% since last month

); } diff --git a/dashboard/15-final/app/ui/dashboard-overview.tsx b/dashboard/15-final/app/ui/dashboard-overview.tsx index 32f0361..9065ba9 100644 --- a/dashboard/15-final/app/ui/dashboard-overview.tsx +++ b/dashboard/15-final/app/ui/dashboard-overview.tsx @@ -1,6 +1,8 @@ import Card from "@/app/ui/card"; -import { invoices, customers } from "@/app/lib/dummy-data"; +import { invoices, customers, revenue } from "@/app/lib/dummy-data"; import { calculateInvoices } from "@/app/lib/calculations"; +import RevenueChart from "@/app/ui/revenue-chart"; +import LatestInvoices from "@/app/ui/latest-invoices"; export default function DashboardOverview() { const totalPaidInvoices = calculateInvoices(invoices, "paid"); @@ -9,15 +11,21 @@ export default function DashboardOverview() { 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 e8c7126..2b5838a 100644 --- a/dashboard/15-final/app/ui/dashboard-sidenav.tsx +++ b/dashboard/15-final/app/ui/dashboard-sidenav.tsx @@ -47,7 +47,7 @@ export default function SideNav() { )} > -
{tab.name}
+

{tab.name}

); })} diff --git a/dashboard/15-final/app/ui/dashboard-topnav.tsx b/dashboard/15-final/app/ui/dashboard-topnav.tsx index 3ad1349..d5f95eb 100644 --- a/dashboard/15-final/app/ui/dashboard-topnav.tsx +++ b/dashboard/15-final/app/ui/dashboard-topnav.tsx @@ -2,8 +2,8 @@ import Search from "./search"; export default function TopNav() { return ( -
+
- ) + ); } diff --git a/dashboard/15-final/app/ui/latest-invoices.tsx b/dashboard/15-final/app/ui/latest-invoices.tsx new file mode 100644 index 0000000..6a832f5 --- /dev/null +++ b/dashboard/15-final/app/ui/latest-invoices.tsx @@ -0,0 +1,52 @@ +// InvoiceList.tsx +import { Customer, Invoice } from "@/app/lib/definitions"; +import { findLatestInvoices } from "@/app/lib/calculations"; + +export default function LatestInvoices({ + invoices, + customers, +}: { + invoices: Invoice[]; + customers: Customer[]; +}) { + const lastFiveInvoices = findLatestInvoices(invoices); + + return ( +
+

Latest Invoices

+ + {lastFiveInvoices.map((invoice) => { + const customer = customers.find( + (customer) => customer.id === invoice.customerId, + ); + return ( +
+
+ {customer?.name +
+

{customer?.name}

+

+ {customer?.email} +

+
+
+

+ +{" "} + {(invoice.amount / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD", + })} +

+
+ ); + })} +
+ ); +} diff --git a/dashboard/15-final/app/ui/revenue-chart.tsx b/dashboard/15-final/app/ui/revenue-chart.tsx new file mode 100644 index 0000000..f9b0b61 --- /dev/null +++ b/dashboard/15-final/app/ui/revenue-chart.tsx @@ -0,0 +1,49 @@ +import { Revenue } from "@/app/lib/definitions"; +import { generateYAxis } from "@/app/lib/calculations"; + +// This component is representational only. +// For data visualization UI, check out: +// https://www.chartjs.org/ +// https://airbnb.io/visx/ +// https://www.tremor.so/ +export default function RevenueChart({ revenue }: { revenue: Revenue[] }) { + const chartHeight = 350; + const { yAxisLabels, topLabel } = generateYAxis(revenue); + + if (!revenue || revenue.length === 0) { + return

No data available.

; + } + + return ( +
+

Revenue

+
+ {/* y-axis */} +
+ {yAxisLabels.map((label, index) => ( +

{label}

+ ))} +
+ + {revenue.map((month, index) => ( +
+ {/* bars */} +
+ {/* x-axis */} +

+ {month.month} +

+
+ ))} +
+
+ ); +} diff --git a/dashboard/15-final/public/customers/ada-lovelace.png b/dashboard/15-final/public/customers/ada-lovelace.png new file mode 100644 index 0000000..962308e Binary files /dev/null and b/dashboard/15-final/public/customers/ada-lovelace.png differ diff --git a/dashboard/15-final/public/customers/grace-hopper.png b/dashboard/15-final/public/customers/grace-hopper.png new file mode 100644 index 0000000..4334b3c Binary files /dev/null and b/dashboard/15-final/public/customers/grace-hopper.png differ diff --git a/dashboard/15-final/public/customers/hedy-lammar.png b/dashboard/15-final/public/customers/hedy-lammar.png new file mode 100644 index 0000000..3a8fd6c Binary files /dev/null and b/dashboard/15-final/public/customers/hedy-lammar.png differ diff --git a/dashboard/15-final/public/customers/margaret-hamilton.png b/dashboard/15-final/public/customers/margaret-hamilton.png new file mode 100644 index 0000000..3abc439 Binary files /dev/null and b/dashboard/15-final/public/customers/margaret-hamilton.png differ diff --git a/dashboard/15-final/tailwind.config.ts b/dashboard/15-final/tailwind.config.ts index c7ead80..083a5b7 100644 --- a/dashboard/15-final/tailwind.config.ts +++ b/dashboard/15-final/tailwind.config.ts @@ -1,20 +1,23 @@ -import type { Config } from 'tailwindcss' +import type { Config } from "tailwindcss"; const config: Config = { content: [ - './pages/**/*.{js,ts,jsx,tsx,mdx}', - './components/**/*.{js,ts,jsx,tsx,mdx}', - './app/**/*.{js,ts,jsx,tsx,mdx}', + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + gridTemplateColumns: { + "13": "repeat(13, minmax(0, 1fr))", }, }, }, plugins: [], -} -export default config +}; +export default config;