Skip to content

Commit

Permalink
Update layout and design for cars dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
ruchernchong committed Jul 7, 2024
1 parent e1050dc commit 2bafbb1
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 117 deletions.
2 changes: 2 additions & 0 deletions app/cars/[type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
136 changes: 24 additions & 112 deletions app/cars/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Car, "make" | "number">[];
searchParams: { [key: string]: string };
}

const VEHICLE_TYPE_MAP: Record<string, string> = {
Expand Down Expand Up @@ -113,61 +103,6 @@ const CarsPage = async ({ searchParams }: CarsPageProps) => {
numberByVehicleType[vehicle_type] += number || 0;
});

const getPopularMakesByFuelType = (fuelType?: string) => {
let popularMakeByType: Record<string, number> = {};
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) => (
<>
<div className="font-semibold">{title}</div>
<ol className="grid gap-2">
{data.map(({ make, number }, index) => (
<li key={make} className="flex items-center justify-between">
<span className="flex gap-x-2 text-muted-foreground">
{/*TODO: Switch this to the brand logos instead*/}
<span>{MEDAL_MAPPING[index + 1]}</span>
<Link href={`/make/${make}?month=${month}`}>{make}</Link>
</span>
<span>{number}</span>
</li>
))}
</ol>
<Separator className="my-2 last:hidden" />
</>
);

return (
<div className="flex flex-col gap-8">
{FEATURE_FLAG_RELEASED && (
Expand All @@ -186,24 +121,28 @@ const CarsPage = async ({ searchParams }: CarsPageProps) => {
</Breadcrumb>
)}
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
Dashboard
Car Registrations for {formatDateToMonthYear(month)}
</h1>
<ul>
<li>Add links to the respective fuel type</li>
<li>Yearly OR YTD if not a full year metrics</li>
<li>Show trending Petrol/Electric/Hybrid makes</li>
</ul>
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<div className="grid grid-cols-1 gap-4 xl:grid-cols-4">
<Card>
<CardHeader>
<CardTitle>Total</CardTitle>
<CardDescription>Last 4 weeks</CardDescription>
<CardTitle>Total Registrations</CardTitle>
</CardHeader>
<CardContent>
<div className="text-lg font-semibold text-primary">{total}</div>
<p className="text-4xl font-bold text-blue-600">{total}</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Top Fuel Type</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-green-600">
Petrol Electric
</p>
<p className="text-gray-600">Highest adoption rate</p>
</CardContent>
<CardFooter></CardFooter>
</Card>
</div>
<div className="grid gap-4 lg:grid-cols-4">
Expand All @@ -219,43 +158,16 @@ const CarsPage = async ({ searchParams }: CarsPageProps) => {
total={total}
/>
</div>
{/*TODO: Interim solution*/}
<div className="grid gap-4 lg:col-span-2 xl:col-span-1">
<Card>
<CardHeader>
<CardTitle>Popularity</CardTitle>
<CardDescription>For the month of {month}</CardDescription>
<CardTitle>Leaderboard</CardTitle>
<CardDescription>
For {formatDateToMonthYear(month)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-2">
<PopularityList
title="Overall"
data={getPopularMakesByFuelType()}
/>
<PopularityList
title="Petrol"
data={getPopularMakesByFuelType(FUEL_TYPE.PETROL)}
/>
<PopularityList
title="Hybrid"
data={getPopularMakesByFuelType("hybrid")}
/>
<PopularityList
title="Electric"
data={getPopularMakesByFuelType(FUEL_TYPE.ELECTRIC)}
/>
<PopularityList
title="Diesel"
data={getPopularMakesByFuelType(FUEL_TYPE.DIESEL)}
/>
{/*TODO: Interim solution*/}
{Object.keys(numberByFuelType).includes(FUEL_TYPE.OTHERS) && (
<PopularityList
title="Others"
data={getPopularMakesByFuelType(FUEL_TYPE.OTHERS)}
/>
)}
</div>
<Leaderboard cars={cars} />
</CardContent>
</Card>
</div>
Expand Down
11 changes: 7 additions & 4 deletions app/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -45,10 +46,12 @@ export const DataTable = ({ data, fuelType }: DataTableProps) => {

return (
<Table>
<TableCaption>
{capitaliseWords(fuelType)} cars registration in Singapore for{" "}
{selectedMonth}
</TableCaption>
{selectedMonth && (
<TableCaption>
{capitaliseWords(fuelType)} cars registration in Singapore for{" "}
{formatDateToMonthYear(selectedMonth)}
</TableCaption>
)}
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
Expand Down
3 changes: 2 additions & 1 deletion app/components/MonthSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -57,7 +58,7 @@ export const MonthSelect = ({ months, selectedMonth }: MonthSelectProps) => {

return (
<SelectItem key={month} value={date}>
{date}
{formatDateToMonthYear(date)}
</SelectItem>
);
})}
Expand Down
92 changes: 92 additions & 0 deletions components/Leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -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<Car, "make" | "number">;

interface LeaderboardProps {
cars: Car[];
}

const CATEGORIES: Category[] = [
{
title: "Overall",
icon: <Trophy className="h-6 w-6 text-yellow-600" />,
},
{
title: "Petrol",
icon: <Fuel className="h-6 w-6 text-red-600" />,
},
{
title: "Hybrid",
icon: <Zap className="h-6 w-6 text-blue-600" />,
},
{
title: "Electric",
icon: <BatteryCharging className="h-6 w-6 text-green-600" />,
},
{
title: "Diesel",
icon: <Droplet className="h-6 w-6 text-gray-600" />,
},
];

const HYBRID_TYPES: string[] = [
"Petrol-Electric",
"Petrol-Electric (Plug-In)",
"Diesel-Electric",
];

const getPopularMakes = (cars: Car[], fuelType: string): PopularMake[] => {
const makeCount: Record<string, number> = {};

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 (
<div className="grid grid-cols-1 gap-4">
{CATEGORIES.map(({ title, icon }) => (
<Card key={title}>
<CardHeader className="flex items-center">
{icon}
<Typography.H3>{title}</Typography.H3>
</CardHeader>
<CardContent>
<ol className="list-decimal">
{getPopularMakes(cars, title).map(({ make, number }) => (
<li key={make} className="flex items-center justify-between">
<span className="flex items-center gap-x-2">
<Link href={`/make/${make}`}>{make}</Link>
</span>
<span className="font-semibold">{number}</span>
</li>
))}
</ol>
</CardContent>
</Card>
))}
</div>
);
};
9 changes: 9 additions & 0 deletions utils/formatDateToMonthYear.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
20 changes: 20 additions & 0 deletions utils/formatDateToMonthYear.ts
Original file line number Diff line number Diff line change
@@ -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}`;
};

0 comments on commit 2bafbb1

Please sign in to comment.