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?.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
zcmV
caY
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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
zcmV;a4p#ArP)^5PyI;Q72-=gaHv1F$c^!W6n9}czx=A
z@1{0)m`e=ty{m68^3=2NRFuk;DU&K!uAEAjE}cGp{Fpv`_>kX6>+>A<
z@(llZrfk`=cI}^k{;`8|9h`Ho=2%O6YG1C|n*4#ltifi?L;TmTUyT^fMWC1g@%-vP
z1lFrpFEwk{ELE>wJx@4utOKLoy?bZR1N!C5mv*p^_7$`Eg+A6`o?q4xqs3&=CFb*t
z0r!)-7^S5DIu}F4CC)kcU!_Ww)Vg)+)UaVgyB8pzK7F!zz#xG~j~>~9Gy&VEPoLDe
zbLVvZ`gH@!Iy~q1&Y}aL9*oyQVJId+FCBlgi1(cH4B?#l3Kc4(7A;zsr0?FnYec?z
z^Cn%rdNn(=Z>w#zG9;G^;#F{
z_j5T9A`bt6jw~j~R@${|mu}v?X@sIqH*VZW4<0-)wW?pgetP@%ZF>Lyy#WL)*N`sH
zK7an)j*1m4rb?A6rKU}rntCHuqzp(7X^aQwk4Sq78q=ye5rV}Slqi2L{Nn`$C;t^;=e{{8k0&&qpxW^)L56pA?~k3Q>$crH8CZ};xq?K*b0
zYu7HbIiv%@+LU{%R;`-q)~%a5cI=p*J$q&ZBRSNV{TnrEWY_s$yLRoo{r(PPCUG25
z^XAP>8ZM#;_sg0gH+uNz|Bu
zBf<%BFDlDCwud%gr>HWviCXdtBZ$Q?i2W2Qq@q!J!eaDBdn+^~J=tBSPMvH+h}^Pe
zOFlV(c#g)68=KT18e4=2qy~YkfiwWD_$G-17-z%UTmw8z;<$M6Vj4JbU@?ayaD{wF
zu22k#!>b!aup#OL!5o|es9(Q+seSwQ`7FZ+X$BxU=UyLXkTBkoIi!Y!&zw19>oT}@
z?b@}}qel;WR?bFiM2{~lIS(R&^C7{S%679z<3g@#&ZdFITQys#B+q&28AQ
z!2NsGK-@(Gq~;W?pS{4nQ{H
zz`zOMY=$Es(_mgU77728G_!_JrXjLt&z_bYp|)JZtG;;g!pgUpU3rz&D2j_`0Yx{!(e@?6M0Vdgu$q(cP?IIbo^0*kswr&V2nElF!sPa
zIir;&|0V$cuhg=#r$S(HUOuotdGf?k!7TNU+EPze!nZVE57#LI?4Z^eu)7&$ztvN(F`&Jz)U}@*f*`_3BlAA*2dGRsa6^xM?9K
z#ynDUnwN8vDF6@#qeXE5x;
zhnD5}^T6aq3po~DS5HJXKsG_C~3JBNsRE
zTxBtI9sq!2QGN;XO5eDjxQN;^l!b^nsIVN6hEy6ABd)O?_sE`=baTz;vflYA@rZOs
zMi$>W6^}#SqcR`{H?8g(8()B|inh>%?>A*TLA
zy&woxg*eX1QSrJk1ws|a;5-rlGz>a@`m|{Vo)U?(9`{k@3y|!a|LjTN7j3Z@S}eq+
z4E59vYUuj>&i5k$h|S@6R;^lP-c?N^=4b_S$lj2y*9@{DidcS+9zAO70v0u$JbAKd
zhAJ4C!TzZ7lqpk+(d+-mn461Y_FxrQ;kX{=!0u2hRFCV_g2biq+|AN?6s{~Su!CQO)Mod6tO3==U;Gk`kr9czEHT)yfwEy^Z<
z=b-SVl5f(aiOscY)ynQyZK$4%hJrDGkrHbl753qrbv&^ufMFV{>?;WmFbiafFi|KU
z6hTN1J40n~?+{N#k81i%-q&|os-tz)G}gM{gDNIBs5PK_8-ISj3JdG^8EhdxoA}H(sUvtq|A=3&}
zT?SxmftaSQnD5=N0dWzKK1UScSy_x%OAdf2Yf(RkYidSiWnX?2c|iVumeXP2L~t$u
zqVQxR2*8QxouZcn(p5I%*Y^Npk}av}RhwSG$o9A{0eIT8z!pWR`6H`hCKzLfI2qhI
z*C0aOFQ0X&w1FXUt(Yx&r{?GNa#R3W6Q;#CPFWKCtBM*2f@;A{q;=`irTm6^p($q|
z&r23Vj8rW3MEJ@eD;HLPXATF1b0J=G&TE1!C!;m}c`j?h-{9ZiFd&$Xh{A`@wQJX=g$oy^9XocUWy_YOxpU{Hy?ghXxf89a7>lx8tq05dcisoGh&&mIF;Zk6X~12$4sl%LeLM~Vm69qdNkGb|G6^~NsPa&(
zq(YGiaKEa2A9sDjS(=eg8MA<7Nlt?vrF35lLl8BevKa_QTC53hUPH#rw5l~hJ$xO|B9eG-aYREZ*$B!Fu;0wh53OBtm8
zrYZ}FR1zS7*cZeKoN7qXqI(HF3jT$nK#7W8zrIZJZ3_k)P|on-4j&OHgTYJkLWU|H
z=}C$t$ULh~IGz-ss3zbP%#}na2Sws4r$mp;h`2js$dD5B#uPJegIVh60mpe%W8AoL
zCNV;Q>;`vEyP?_dw?YsmJM;F<0dTbeoWfa|CvaT3l`AL=zWVAL6p#OjdU!8D{t)uIf>?@#SH
zeE9GZb)Of6*Sbt6<2R39)H4B9n>1-snl)>dRrN^Vz<~qSS3wF4h%EFemd=&t(MCkg
zX-}{=16%YUSWkUff%5S$773+i&Ybz5sJp&3u3|H)fVjkBoHr9s8}_C>m1y?c*Z;!~~siN9t<(!b$ZoJ7c}2nn5(9|B0iG4$
z@}hv{9)RJPBsr<_=FOWkLCM=HW8vDNLx;?caeS;N`ZK>5_1yAO!oIKpTsUAzehCvu
zDtcSO1}cZ@0YpGOKr$Gvc@q#2BtqEn!96nE$58g?i;sEp=B2S?$J*YQM$W6FS5f~T
zTL28T99;6f7oW@e=Ku`|l5;@n6`Q^XMK!%s@M^{LR4Tp`Alr@fdC9T{85K!63C8T%
zv;X&gFiK8pqKA+M--LHdR*GW4ZW{D@UVE?v6R?1*A`
z`}XZNKXvL<+ZV6wdpO=H`n6=24CsN$IiJV*P2uK}B}?+Pm=^beUMiLKJc9~DRljfD
z`@xzkSFTJ44<0lItXQ$a)*3x}w1H(U(zTsCcczIGCt9lm`FdnrTX`qL447-KImTbU`hMjfquzPvo#AE6mNnP%<;(luKJNQobImo|
z;J{#;Gj~q?o-koTTexsx`}Nmf+fP6J)JBgU-R8}k*S6VaoA%dVf3*)j_@HgP@y2bv
z_10@Y|NL_s9v*J9XU}d!Lql!TO*gH%tBhWywNb5q-I
z*kFUU)>>;7f!465v7XJGIkUc3S!IeI+TL=-PhB5aLhwwlD{IfP&d+oJLdkC|R4~7ZB97HZyu%NBJ`s$@!o6@-PH0ilL
zjEOK@o^841mhGT}4r>2A>Yr`G#0l-#V~;Hwe*E#rT7yt8zWCzy;)^fVobSH-t{rj2
z5hYyvu)HA;EX?fY%%0Q6tv{}`V}OCPX3i=`-LvT2xpPYh2v}{k)rt}K5r|;~_>H+-
znQueiu?`o8e*5jWzL^rBy{&JZnzLktp4VT0y*>Z@^KGMzHfmQ~aYYH%yu%JVtbOy%
zHzhQ5_?X9%1~0kflF0}_w81VWVewyn`K7<9Z@z&Tzi9-GOM07|PwSYaz09#8?I12D
z<=YsHM&rg=BPJe`#>mvEQ)>+Z`|7K&$`_|hnbO9N9or^NnpAu4u)_|;(BX$4-o}m@
z+XjaQ%fj$5kVS1Wal*uUzywW4*EbEHaw{%1Ds
z2Z;&m_uqdnrV#9dN%xHp5E<(k!P>(!On&|K*KOLgX(h-lw%DTP(il1)c;JCT$_F2O
zu>J7E59L0X0(>{#cw?FKz&h)!Q$(?I-yre{`2y>rfL&-JP7y%L+FcrkmIDuH_
z0h;fgOIWZs^TbR5#(K<~K+ys{G0#PI-+lLD^0UuAE1wikgYqshmF0};Nom+k43dD&
zO`vjiPDv}r%r7@tTLQ_L!UHvfE*@!ce#V!J==*@7-o5PwJv
znv&LY&pq3oc;boDSYY23%_ULTYU{1)_v4R0-d=g-mC_(VSjU1(`RAYC7j)vO?@_%Chb<7)A}LHx
zpFX`z1Y^%W`)s97q??$QY{y*L$-D2qTW0e9`|sB`{~;vAU~t0?H!O3N!XYwdh!M9zZn)uw
z(snUu0ShtFNgIu12GH$qb}a25al4U}YWZ-*n2$dCs6RGflT-_!s2qeHah66gxcAK5!2;<8K&pefsIAMY}y*1cb?d|L?!szy9Z6CDfrL))0wQ
zsXzH?GMV0gZD7b8^UXJ}q?j-P9BnXhxhCk3
z&{Ad*NN8a}=1H(Az@Fv?_@r~t__o__8;OH#mqe(Wq!CUM2rlidnAoi>QsdE796H(+
zv(7y8%wkZs&$o?Z&a{ddjADGo^chtpq%=}3JoeaQ?dF?r?&AS^Fv0h%M~j&ByALEQ
z0MB>QO!MqLKm!271cM-=2h{6Xpgcj85Tr~bYav7=Io^Kz?b~IST~?U|Vp8+bns~)`
zX@~H>{PN4TblK9snYH~$Ux}33lrQlwa|i}V1$`s*&__V0pMHA9Nfrq^h77Xt&O7g1
zOtEMF5R-_6N?Qv@v?a!gRp#ZnIM=H!3-JOhsT44epyaCVGt(sR5t0vcMx=0q4s$UF
z83||Wt+y_`ps5Jw?6c2qLxV$Y-~IM2YeWNIEd`M>b3g#q`yX=BsL4sOm=-|#&jbId
zq&q<1J`Ejx^wGs+q!|X|&xFA`Xfg$B3ezwbQ;QF)9+HS7oS73?a09y1u#my`WHSUJ
zep*8^Fod8*ixw9FBxQijEs|u|2NTr(<&-b`{+R-o*A&1o75(-TDzV+5yOIQ+4e_Jp%SXb2$u_ciS`Yyk
z<}2Q%#6d)!#aGSgG8I$xp{-n?-+C#LGM+%=j|7~ShkIy>KPTUW1KV!9ZS8Gb%veSP
z!TD~PXcdN?fEz-ysb@5h0yZ@r^K{7(gJ55L@kI&Bx+(@f6F`V2!gSU$Hi
zj#$vjOwvu8lVk81g$;m0#y`X-4|bcsTia!+N4?c
z%|)L%>7NdbvJ%`->;!yZ7FE+XWX~P_*p2>#miYt9XVG$V{X+7)(N|#$uwMe)ehAhNLX`r@0ei
z)AYp<=CmYaL+L#GA46%ENaUww+D6m|dBW$rxbwf@N%&dfii!U@pLteoV?K68>ezFMZRtA<*$G`D|K*DQm_QrT~*YQde3_4$D08
zQ(X!6@1g7-vxgpfXd#k;eW+bw7BS0Y4%Nx-eG`NaL1~)@FpS3x0@C1hx#zb8A?aQi
zNn6Am*3nJ>vVXz`KMVlbn~M?JQ%^lrm4z@gBpBO3vMLi0AZa66OA>(Y$qyePVVXe<
zX)XA3L1`sHI2C?Qm!O(8NB7PJXV0EhzPW7qvcH{4U@j#UAtSa6XkBfl6NiR`C*cNY
zilgSAbka#}0MaC@Hz@Hii=-=-6Cod}$IM6!U0ATc;I9P*LODC^CIci*kkY9C{`Zk~
z=bd-SG
zO`W=_9myfaMk`km>4B1}qF8yOlO4gvWNDc72v3+q1K|*QlK0S*PJ8y#PXa=}{n|VI
z8Xn1Kxy?`zN!kN7Z35kxAtdsgG*)J+TknxPqM#@g=Ip5w%opfAL$tO9Lee74L8y;hbb7_;X4)p;MYYX8i#O1hLu38G2Y_SJEm0wdZGc866o^bK6CL|!X$`P`YE-nQT42eQXmzy
zP8CxkZD4^25>5Tph4QW@2_dOlG7=1{&$RIqGLj@@4;c%>x)y;ri8z4>Q9^i|Xn(Nk!xiG-A%n5iHrv!r1DVxGLmjE{1b9%%H
z(lO)YuIa|1kpgnuKx;`n%)?k3QZeBn{)Yf&s$w|hlv7HI_uO;OiWdz=$`iyf!}v+s
z6^TtQQWU^_n2U|Afk~~r_;T73-T0PXibNxOMo<|W|3VAF!w7Laud!G|&^IQBKZ8Bh
zPo{pp=wya;4Wc}1DUhohnG+|R9%R%
zWHguxIHf8{iibeHt5jn>e27owyq_@AzKm<2htQII`&Lvdvl8Z#IFza9@KfAzrr^X#
zZxKZ25F}FU=7P!UyI|uRamK(^pjX4JZ9+`Q+F>yiLV6l66aWv
zS3+1#t^iL=I-LS29bXiJlGf$*t-J_WuX53zt`{{sz7n7I+H0>O>X~Pr>6=GdiQUpA
z7Xs6m&8bk{cIj2OTaF6!^5zFYq#xQXwNl)21p;IdsVVbzM!MevS6y{g
znNa#GOeSwgSbzyg0Qg2)5i!d&fD0l4(
zCVM+J4I_hTFmWg^9XO3ufk(per!E0dkclC>Uf$~w>yWeSgE8CmRO=1FUR=vL;povr
zWi8wx8tQfyQikP~ue`OW^5Bo2r88cNxbn&?`$yN{nm=57?X~Tun{Mi7E>gLaGx4b)
zV5FThg#bX8`IuUKDu~E5?HAzjjgGU(AUzXL{7$bl
eVAg$4;Qs-mYa){4I$O^G0000PtG|9|ML{S&HC!>
zg9Xj*?n-ODD_8Gj8#iwJ=WuJ(sp^MeNuP6SpUas|-(T*0hTKjxTYj@W;c}bzC^Gy5cr=R*WiDH5TFS_WWJ|4p)jA@Ly_U+r(cJ12L83Bs909mtUO}pWS
z8`?eh+|vOV!^_C%Xet$y$;FM1V??GdRMpo!YWdxF-}U!Y5%DBJ)i8C}U3ay!&N^#i
zfa77O>({Sud-v{bd-m+<^GMKT%@05Pu)X!xTOF9X_kV?$(2EvYGCLYFi%_d>J
z_+xwf?YBFjBys%l$4@p#B{Ay#V~;)7F1h5AZfm5=9sv;5Cv_Jrx)o
zbOgOJ9svMqHU`Lwz|j!OSjfNh(o3Cyc;SE$qN${fc*-lUyt3OHFB;o3&pMi4stO!o
z#X6XM=bd+^=TW;Vy(7&_!bBZhQK(WKjroHQK4_nR{&{DFJYJOHfAGrQR=53
zvl=xN>(;GX*Chi8(!-p44CyyM=#wJb#9(3_JN)dk&w3a+_uO-rIJy9#<-XE8OXg5*
zF0FdXiV!Bg{`%|o`|rO`j>Q_#w+Z_pMUsj78G{+lhEx%%ALGE%B6Yv)*FM&dQ<6B?
zV^KOcN_NU*77beDXlkEL-h1!8PBbF=LWl23!ZV^1^DwNBnhFb;3?L+^-*~?ql{s-X
z*7)O(KYAZ2e)G*Y?Y{f&>uW_)k@TW?I8_s4=a>>z=~;Nys#Wc)ufFQzk_hMCIE;#a
zRF?givvTFi&ipmkT+=ST_~M@7Ty@n|{nvO5u)`mI_@OgQ(xi_WeeQYHUw{4eh+tox
zOtdbsa6Wn@9o@Z_gm+|AXo#)#jQwAUP|M_*8nrTR`SRuc_m*33>5e8+M>MIKldvF8
z0^{0iukGtvU!UMl1H{`~Z@qP*HEm2vCVs{+5DC^iur}&LdSg8n+n`pPH*fCG7^G?>
za`VkMcgZ1Ih)9fNZ|w2XOE2wt==IlM-@0aEXM;eEis@%hVk4kv
z1j!kv5GG~n%3ESaU1@I;r22rRzHvZf(mm#*^N4xW<+SRVFv0=Y5JN6Va|}7A@Jpqr
zE@@D)6tcW8FR7m@8V>}_DvXs#a#nLNednEb_VK{7rw{`WAX8uVil#-0chq#sWUAES
zNk_+n7>>hl0(876RWb*&7$5}m?GiKd*qQvI%p)GVwoD=efQnn
z{_OGSqmTAHG)Z{sNcph7Dx5J2%obJdA$&}Ms6{~F8B+shLG}{QmfRAfwMI+{B}EHZ
z?2;F)c`7xWGxg%&p*|JH5S%b5~8S=jo3xhG15sQo%5Ff
zf%qI*Al4P+g#!J;LO`@;(l&bl$l(*yaBuv(@4nj|%b$P#+1JsR(yIdPc{-noz18ex
zJUlgqD^EF;u?WUsY|%wB?2dg@)imBZB#*(&(-7pDT_$<32T;thkF}|DoENFfNyn6N
zEEZ<9NUR2$0YL|9VKz>r9<6(}Ibui#gPz$_)YZH=8;o*3?%Cw~@4w&I0AfIQhLgks
zaMs|(#mXn1c%nVR(m6Xq-)jaqhgs`YeZoL
zgE`{YM~av;UOeQ4q9=$tSz5M|)z&i_VZRyX45C+F?}31jIv9BLPIPKQ23`
z(hxSru|k|lGM3mgsRDsL8-qDvKp7>FbNz{PvJ{%Y&OZ6%lkQ~9rJ7OsFjpC>A9W@t
zGwSeoJ_FgPSTYyV!&tmtLWRXK1$7%5mqXpCA0QO2q@=I~7OCHV|NR}P-+ue8ufbM5
zi+}FTOO04VS@j*GP0lOFi)@jKP*L{?G#+CZ^4`7oIV>^`Auf{US*cT0I`yC?xEoKh
z7&S&dN}ApSiR#Kpd2rfMtA;9&@e5V@=cC@Hr95&aOUZi#Cp`<}h@-NkmAWtoM*4VE
z>tIeCyZ7FEdj|8$E3b5$5(8tq2`~0mRd}yHeCnyEmI$BYuke+}3`3|~uYVI7t
zWX-8afBNaC|5s6n9T_N_hkmjWi$VwkImjguYKd9Dkt}=4AriGq<*qN^D`*5pF_sUb
zIYLAWhI}oKKR^Q@6GC!q8TxDf9DAJCpqK%v$HX5CCWS%}`-oa1E{8?=_Fx^rAV{Si
z^@|O&BLI=ENQH`lVKCnujI$X@#OvkIC&{-9p*b89zoK%8%7K-BuAh`JE0`lV`L8}Ui&^uj
zrOZcaXHWeOiZiEbV5HYovQr3nB{$rJ$dwU~;A7JwULj`fnJEjqZKkLp>ATu3GMCBvYaDJYGxDRnl6;d|#E
z0r45H9ZEmplRCqX3<=r?DmDg(r(g5XVa#uD3#q!MD{
zu-pSIan(H9P$g{*puPV3>r*8lORAcVUfso3E=$gVK)>#M-Q1eb5TpPg!DyFXs><0&<7!SFux9l;TRM2d6HSeR9ggI-RM5uU$6
znwSith7-)@QySv)uce#>Ac`d8F%g%KGpRT28K$$g5R(|3Ba&1lsX=Rog)Vt(5`{>7
z7U_6lGt4<#Dr4mEcp`xXW<)wP8+1i6>ltniFnO?k+licw7JZMdZoe$I63hq
z(IgD9B?KFj&(fi%dhq<$G?`QW4VBY8YkPLIneY)+oyCz@5Obu2a0uHpzp3hzwa|Sz
zDCW{q5JV&t>wjUOXdKBUMN5qrSC8=ki&^`mB9HS*1(8#PWLA+R^-Qgb
zR?g;y7hY%&Km73YZ>w4K6_TlzF$b08ggIRkxE;QdaE#g6rT@N=B#;oBgDNVM03FX4
zZ;g3s7gbICi$jU}d7tDVPA*|5M;q}G7^EF9zx;9s$br`X0r2SHchA?MK>z>%07*qo
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;