From acc531b370fcc184438e106c077a4ca8a4b8c761 Mon Sep 17 00:00:00 2001 From: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:38:55 +0100 Subject: [PATCH] Add dashboard overview page (#135) * 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 * Misc * Switch to flex * Add revenue data and definitions * Misc * Merge branch 'master' into example-f9sv * Remove unused code * Add sort invoices calc - to be replaced with SQL * Add date to invoices table * Add customer images * Add LatestInvoices component * Optimize for mobile * Misc * Tweak * Remove duplicate date fields * Mobile tweaks --- dashboard/15-final/app/dashboard/layout.tsx | 4 +- dashboard/15-final/app/lib/calculations.tsx | 27 ++++++- dashboard/15-final/app/lib/definitions.tsx | 36 ++++++---- dashboard/15-final/app/lib/dummy-data.tsx | 66 ++++++++++++++---- dashboard/15-final/app/ui/card.tsx | 16 +++-- .../15-final/app/ui/dashboard-overview.tsx | 30 +++++--- .../15-final/app/ui/dashboard-sidenav.tsx | 2 +- .../15-final/app/ui/dashboard-topnav.tsx | 4 +- dashboard/15-final/app/ui/latest-invoices.tsx | 52 ++++++++++++++ dashboard/15-final/app/ui/revenue-chart.tsx | 49 +++++++++++++ .../public/customers/ada-lovelace.png | Bin 0 -> 4336 bytes .../public/customers/grace-hopper.png | Bin 0 -> 3753 bytes .../15-final/public/customers/hedy-lammar.png | Bin 0 -> 5012 bytes .../public/customers/margaret-hamilton.png | Bin 0 -> 3950 bytes dashboard/15-final/tailwind.config.ts | 21 +++--- 15 files changed, 246 insertions(+), 61 deletions(-) create mode 100644 dashboard/15-final/app/ui/latest-invoices.tsx create mode 100644 dashboard/15-final/app/ui/revenue-chart.tsx create mode 100644 dashboard/15-final/public/customers/ada-lovelace.png create mode 100644 dashboard/15-final/public/customers/grace-hopper.png create mode 100644 dashboard/15-final/public/customers/hedy-lammar.png create mode 100644 dashboard/15-final/public/customers/margaret-hamilton.png 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 0000000000000000000000000000000000000000..962308e9ea3ecc9be82461f09d43306142709e5b GIT binary patch literal 4336 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 e9HZxw$NvMl literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4334b3cf80a45f2c84d605e1dfe9c015e08db00c GIT binary patch literal 3753 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 IM6N<$f>OzYp#T5? literal 0 HcmV?d00001 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;