mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-28 15:14:15 +00:00
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:
committed by
GitHub
parent
5225fc3b54
commit
acc531b370
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
52
dashboard/15-final/app/ui/latest-invoices.tsx
Normal file
52
dashboard/15-final/app/ui/latest-invoices.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
dashboard/15-final/app/ui/revenue-chart.tsx
Normal file
49
dashboard/15-final/app/ui/revenue-chart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
dashboard/15-final/public/customers/ada-lovelace.png
Normal file
BIN
dashboard/15-final/public/customers/ada-lovelace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
dashboard/15-final/public/customers/grace-hopper.png
Normal file
BIN
dashboard/15-final/public/customers/grace-hopper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
dashboard/15-final/public/customers/hedy-lammar.png
Normal file
BIN
dashboard/15-final/public/customers/hedy-lammar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
dashboard/15-final/public/customers/margaret-hamilton.png
Normal file
BIN
dashboard/15-final/public/customers/margaret-hamilton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user