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
This commit is contained in:
Delba de Oliveira
2023-09-01 17:38:55 +01:00
committed by GitHub
parent 5225fc3b54
commit acc531b370
15 changed files with 246 additions and 61 deletions

View File

@@ -5,9 +5,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen">
<SideNav />
<div className="flex-grow">
<div className="w-full">
<TopNav />
<div className="p-4 sm:p-20">{children}</div>
<div className="p-4 sm:p-10 md:p-20">{children}</div>
</div>
</div>
);

View File

@@ -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 };
};

View File

@@ -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;
};

View File

@@ -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 },
];

View File

@@ -24,15 +24,17 @@ export default function Card({
const Icon = iconMap[type];
return (
<div className="flex justify-between rounded-xl border bg-white p-6 shadow-sm">
<div>
<div className="rounded-xl border bg-white p-6 shadow-sm">
<div className="flex justify-between ">
<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>
{Icon ? (
<Icon className="h-5 w-5 text-zinc-700" aria-label={type} />
) : null}
</div>
{Icon ? (
<Icon className="h-5 w-5 text-zinc-700" aria-label={type} />
) : null}
<p className="mt-2 truncate text-2xl font-semibold tracking-wide md:text-3xl">
{value}
</p>
<p className="mt-1.5 text-sm text-zinc-400">+00% since last month</p>
</div>
);
}

View File

@@ -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 (
<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>
<>
<div className="grid gap-6 sm:grid-cols-2 lg: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>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
<RevenueChart revenue={revenue} />
<LatestInvoices invoices={invoices} customers={customers} />
</div>
</>
);
}

View File

@@ -47,7 +47,7 @@ export default function SideNav() {
)}
>
<TabIcon className="h-6 w-6 md:mr-2" />
<div className="hidden md:block">{tab.name}</div>
<p className="hidden md:block">{tab.name}</p>
</Link>
);
})}

View File

@@ -2,8 +2,8 @@ import Search from "./search";
export default function TopNav() {
return (
<div className="h-16 border-b flex items-center">
<div className="flex h-16 items-center border-b">
<Search />
</div>
)
);
}

View File

@@ -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 (
<div className="w-full rounded-xl border p-6 shadow-sm md:col-span-4 lg:col-span-3">
<h2 className="font-semibold">Latest Invoices</h2>
{lastFiveInvoices.map((invoice) => {
const customer = customers.find(
(customer) => customer.id === invoice.customerId,
);
return (
<div
key={invoice.id}
className="mt-8 flex flex-row items-center justify-between"
>
<div className="flex items-center">
<img
src={customer?.imageUrl || ""}
alt={customer?.name || ""}
className="mr-4 h-8 w-8 rounded-full"
/>
<div className="min-w-0">
<p className="truncate font-semibold">{customer?.name}</p>
<p className="hidden text-sm text-gray-500 sm:block">
{customer?.email}
</p>
</div>
</div>
<p className="truncate font-medium sm:text-lg">
+{" "}
{(invoice.amount / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}
</p>
</div>
);
})}
</div>
);
}

View File

@@ -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 <p className="mt-4 text-zinc-400">No data available.</p>;
}
return (
<div className="rounded-xl border p-6 shadow-sm md:col-span-5">
<h2 className="font-semibold">Revenue</h2>
<div className="sm:grid-cols-13 mt-4 grid grid-cols-12 items-end gap-2 md:gap-4">
{/* y-axis */}
<div
className="mb-6 hidden flex-col justify-between text-sm text-zinc-400 sm:flex"
style={{ height: `${chartHeight}px` }}
>
{yAxisLabels.map((label, index) => (
<p key={index}>{label}</p>
))}
</div>
{revenue.map((month, index) => (
<div key={index} className="flex flex-col items-center gap-2">
{/* bars */}
<div
className="w-full rounded-md bg-gradient-to-t from-blue-200 via-blue-300 to-blue-400"
style={{
height: `${(chartHeight / topLabel) * month.revenue}px`,
}}
></div>
{/* x-axis */}
<p className="-rotate-90 text-sm text-zinc-400 sm:rotate-0">
{month.month}
</p>
</div>
))}
</div>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -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;