From 3675a9470a6ef63aa9d7b8f2c7c255f8ff55f3e8 Mon Sep 17 00:00:00 2001 From: Ru Chern Chong Date: Mon, 8 Jul 2024 02:34:05 +0800 Subject: [PATCH] Update layout and design for cars dashboard --- app/cars/[type]/page.tsx | 2 + app/cars/page.tsx | 138 ++++++---------------------- app/components/DataTable.tsx | 11 ++- app/components/MonthSelect.tsx | 3 +- components/Leaderboard.tsx | 92 +++++++++++++++++++ utils/formatDateToMonthYear.test.ts | 9 ++ utils/formatDateToMonthYear.ts | 20 ++++ 7 files changed, 158 insertions(+), 117 deletions(-) create mode 100644 components/Leaderboard.tsx create mode 100644 utils/formatDateToMonthYear.test.ts create mode 100644 utils/formatDateToMonthYear.ts diff --git a/app/cars/[type]/page.tsx b/app/cars/[type]/page.tsx index cea441b..5dfbf1b 100644 --- a/app/cars/[type]/page.tsx +++ b/app/cars/[type]/page.tsx @@ -23,6 +23,8 @@ import { import { capitaliseWords } from "@/utils/capitaliseWords"; import { fetchApi } from "@/utils/fetchApi"; import { Car } from "@/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Typography from "@/components/Typography"; interface Props { params: { type: string }; diff --git a/app/cars/page.tsx b/app/cars/page.tsx index 40190c5..74b5364 100644 --- a/app/cars/page.tsx +++ b/app/cars/page.tsx @@ -11,29 +11,19 @@ import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; import { CarPieChart } from "@/components/CarPieChart"; -import { - API_URL, - FEATURE_FLAG_RELEASED, - FUEL_TYPE, - MEDAL_MAPPING, -} from "@/config"; +import { Leaderboard } from "@/components/Leaderboard"; +import { API_URL, FEATURE_FLAG_RELEASED, FUEL_TYPE } from "@/config"; import { fetchApi } from "@/utils/fetchApi"; +import { formatDateToMonthYear } from "@/utils/formatDateToMonthYear"; import { formatPercent } from "@/utils/formatPercent"; import type { Car } from "@/types"; interface CarsPageProps { - searchParams: { [key: string]: string | string[] }; -} - -interface PopularityListProps { - title: string; - data: Pick[]; + searchParams: { [key: string]: string }; } const VEHICLE_TYPE_MAP: Record = { @@ -113,61 +103,6 @@ const CarsPage = async ({ searchParams }: CarsPageProps) => { numberByVehicleType[vehicle_type] += number || 0; }); - const getPopularMakesByFuelType = (fuelType?: string) => { - let popularMakeByType: Record = {}; - const filteredCars = cars.filter(({ fuel_type }) => fuel_type === fuelType); - - if (!fuelType) { - cars.map(({ make, number }) => { - popularMakeByType[make] = - (popularMakeByType[make] || 0) + (number || 0); - }); - } else if (fuelType === "hybrid") { - const HYBRIDS = [ - "Petrol-Electric", - "Petrol-Electric (Plug-In)", - "Diesel-Electric", - ]; - cars - .filter(({ fuel_type }) => HYBRIDS.includes(fuel_type)) - .map(({ make, number }) => { - popularMakeByType[make] = - (popularMakeByType[make] || 0) + (number || 0); - }); - } else { - filteredCars.map(({ make, number }) => { - popularMakeByType[make] = - (popularMakeByType[make] || 0) + (number || 0); - }); - } - - const popularMakes = Object.entries(popularMakeByType) - .map(([make, number]) => ({ make, number })) - .sort((a, b) => b.number - a.number) - .slice(0, 3); - - return popularMakes; - }; - - const PopularityList = ({ title, data }: PopularityListProps) => ( - <> -
{title}
-
    - {data.map(({ make, number }, index) => ( -
  1. - - {/*TODO: Switch this to the brand logos instead*/} - {MEDAL_MAPPING[index + 1]} - {make} - - {number} -
  2. - ))} -
- - - ); - return (
{FEATURE_FLAG_RELEASED && ( @@ -186,25 +121,31 @@ const CarsPage = async ({ searchParams }: CarsPageProps) => { )}

- Dashboard + Car Registrations for {formatDateToMonthYear(month)}

-
    -
  • Add links to the respective fuel type
  • -
  • Yearly OR YTD if not a full year metrics
  • -
  • Show trending Petrol/Electric/Hybrid makes
  • -
-
+
- Total - Last 4 weeks + Total Registrations -
{total}
+

{total}

-
+ {FEATURE_FLAG_RELEASED && ( + + + Top Fuel Type + + +

+ Petrol Electric +

+

Highest adoption rate

+
+
+ )}
@@ -219,43 +160,16 @@ const CarsPage = async ({ searchParams }: CarsPageProps) => { total={total} />
- {/*TODO: Interim solution*/}
- Popularity - For the month of {month} + Leaderboard + + For {formatDateToMonthYear(month)} + -
- - - - - - {/*TODO: Interim solution*/} - {Object.keys(numberByFuelType).includes(FUEL_TYPE.OTHERS) && ( - - )} -
+
diff --git a/app/components/DataTable.tsx b/app/components/DataTable.tsx index 2f1cc38..1d58f5a 100644 --- a/app/components/DataTable.tsx +++ b/app/components/DataTable.tsx @@ -15,6 +15,7 @@ import { import { MEDAL_MAPPING } from "@/config"; import { useGlobalState } from "@/context/GlobalStateContext"; import { capitaliseWords } from "@/utils/capitaliseWords"; +import { formatDateToMonthYear } from "@/utils/formatDateToMonthYear"; interface DataTableProps { data: any[]; @@ -45,10 +46,12 @@ export const DataTable = ({ data, fuelType }: DataTableProps) => { return ( - - {capitaliseWords(fuelType)} cars registration in Singapore for{" "} - {selectedMonth} - + {selectedMonth && ( + + {capitaliseWords(fuelType)} cars registration in Singapore for{" "} + {formatDateToMonthYear(selectedMonth)} + + )} # diff --git a/app/components/MonthSelect.tsx b/app/components/MonthSelect.tsx index f146797..441660d 100644 --- a/app/components/MonthSelect.tsx +++ b/app/components/MonthSelect.tsx @@ -11,6 +11,7 @@ import { SelectValue, } from "@/components/ui/select"; import { useGlobalState } from "@/context/GlobalStateContext"; +import { formatDateToMonthYear } from "@/utils/formatDateToMonthYear"; import { groupByYear } from "@/utils/groupByYear"; interface MonthSelectProps { @@ -57,7 +58,7 @@ export const MonthSelect = ({ months, selectedMonth }: MonthSelectProps) => { return ( - {date} + {formatDateToMonthYear(date)} ); })} diff --git a/components/Leaderboard.tsx b/components/Leaderboard.tsx new file mode 100644 index 0000000..e06945d --- /dev/null +++ b/components/Leaderboard.tsx @@ -0,0 +1,92 @@ +import { Fragment, ReactNode } from "react"; +import Link from "next/link"; +import { BatteryCharging, Droplet, Fuel, Trophy, Zap } from "lucide-react"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import Typography from "@/components/Typography"; +import type { Car } from "@/types"; + +interface Category { + title: string; + icon: ReactNode; +} + +type PopularMake = Pick; + +interface LeaderboardProps { + cars: Car[]; +} + +const CATEGORIES: Category[] = [ + { + title: "Overall", + icon: , + }, + { + title: "Petrol", + icon: , + }, + { + title: "Hybrid", + icon: , + }, + { + title: "Electric", + icon: , + }, + { + title: "Diesel", + icon: , + }, +]; + +const HYBRID_TYPES: string[] = [ + "Petrol-Electric", + "Petrol-Electric (Plug-In)", + "Diesel-Electric", +]; + +const getPopularMakes = (cars: Car[], fuelType: string): PopularMake[] => { + const makeCount: Record = {}; + + cars.forEach(({ make, number, fuel_type }) => { + if ( + fuelType === "Overall" || + (fuelType === "Hybrid" && HYBRID_TYPES.includes(fuel_type)) || + fuel_type === fuelType + ) { + makeCount[make] = (makeCount[make] || 0) + (number || 0); + } + }); + + return Object.entries(makeCount) + .map(([make, number]) => ({ make, number })) + .sort((a, b) => b.number - a.number) + .slice(0, 3); +}; + +export const Leaderboard = ({ cars }: LeaderboardProps) => { + return ( +
+ {CATEGORIES.map(({ title, icon }) => ( + + + {icon} + {title} + + +
    + {getPopularMakes(cars, title).map(({ make, number }) => ( +
  1. + + {make} + + {number} +
  2. + ))} +
+
+
+ ))} +
+ ); +}; diff --git a/utils/formatDateToMonthYear.test.ts b/utils/formatDateToMonthYear.test.ts new file mode 100644 index 0000000..e9a8493 --- /dev/null +++ b/utils/formatDateToMonthYear.test.ts @@ -0,0 +1,9 @@ +import { formatDateToMonthYear } from "./formatDateToMonthYear"; + +describe("formatDateToMonthYear", () => { + it("should return the formatted dates correctly", () => { + expect(formatDateToMonthYear("2024-01")).toBe("Jan 2024"); + expect(formatDateToMonthYear("2023-12")).toBe("Dec 2023"); + expect(formatDateToMonthYear("2025-06")).toBe("Jun 2025"); + }); +}); diff --git a/utils/formatDateToMonthYear.ts b/utils/formatDateToMonthYear.ts new file mode 100644 index 0000000..79b801f --- /dev/null +++ b/utils/formatDateToMonthYear.ts @@ -0,0 +1,20 @@ +export const MONTHS: string[] = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +] as const; + +export const formatDateToMonthYear = (dateString: string): string => { + // After splitting the year and month, convert them to numbers right away + const [year, month] = dateString.split("-").map(Number); + return `${MONTHS[month - 1]} ${year}`; +};