Skip to content

Commit

Permalink
feat: add favorites to dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelblijleven committed Oct 23, 2023
1 parent b573bd9 commit c5f9a59
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 43 deletions.
106 changes: 68 additions & 38 deletions src/app/dashboard/components/stats-card-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import {type ReactNode} from "react";

import {getMetrics} from "@/lib/db/get-metrics";

import { StatsCard, StatsCardSkeleton } from "./stats-card";
import {FavoriteStatsCard, FavoriteStatsCardSkeleton, StatsCard, StatsCardSkeleton} from "./stats-card";


function Layout({children}: {children: ReactNode}) {
function Layout({children}: { children: ReactNode }) {
return (
<section className={"mt-12"}>
<h2 className={"text-xl font-bold"}>Stats</h2>
<div className={"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 md:gap-4"}>
{children}
</div>
{children}
</section>
)
}
Expand All @@ -31,46 +29,78 @@ export async function StatsCardSection() {

return (
<Layout>
<StatsCard
title={"Coffee"}
subtitle={"Bags of coffee"}
value={metrics.beansCount}
href={"/coffee"}
/>
<StatsCard
title={"Freezer"}
subtitle={"Frozen entries"}
value={metrics.freezerCount}
href={"/coffee/freeze"}
/>
<StatsCard
title={"Roasters"}
subtitle={"Unique roasters"}
value={metrics.roasterCount}
/>
<StatsCard
title={"Cafe brews"}
subtitle={"cafe brews logged"}
value={metrics.cafeBrewsCount}
href={"/brews/cafe"}
/>
<StatsCard
title={"Brews"}
subtitle={"brews logged"}
value={"Coming soon"}
/>
<div className={"space-y-2 md:space-y-4"}>
<div className={"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 md:gap-4"}>
<StatsCard
title={"Coffee"}
subtitle={"Bags of coffee"}
value={metrics.beansCount}
href={"/coffee"}
/>
<StatsCard
title={"Freezer"}
subtitle={"Frozen entries"}
value={metrics.freezerCount}
href={"/coffee/freeze"}
/>
<StatsCard
title={"Roasters"}
subtitle={"Unique roasters"}
value={metrics.roasterCount}
/>
<StatsCard
title={"Cafe brews"}
subtitle={"cafe brews logged"}
value={metrics.cafeBrewsCount}
href={"/brews/cafe"}
/>
<StatsCard
title={"Brews"}
subtitle={"brews logged"}
value={"Coming soon"}
/>
</div>
<div className={"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 md:gap-4"}>
<FavoriteStatsCard title={"Roasters"}
subtitle={"By bags"}
values={metrics.favoriteRoastersByBag}
/>
<FavoriteStatsCard title={"Roasters"}
subtitle={"By weight"}
values={metrics.favoriteRoastersByWeight}
/>
<FavoriteStatsCard title={"Varieties"}
subtitle={"By occurrence"}
values={metrics.favoriteVarieties}
/>
<FavoriteStatsCard title={"Origins"}
subtitle={"By occurrence"}
values={metrics.favoriteOrigins}
/>
</div>
</div>
</Layout>
)
}

export function StatsCardSectionSkeleton() {
return (
<Layout>
<StatsCardSkeleton />
<StatsCardSkeleton />
<StatsCardSkeleton />
<StatsCardSkeleton />
<StatsCardSkeleton />
<div className={"space-y-2 md:space-y-4"}>
<div className={"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 md:gap-4"}>
<StatsCardSkeleton/>
<StatsCardSkeleton/>
<StatsCardSkeleton/>
<StatsCardSkeleton/>
<StatsCardSkeleton/>
</div>
<div className={"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 md:gap-4"}>
<FavoriteStatsCardSkeleton />
<FavoriteStatsCardSkeleton />
<FavoriteStatsCardSkeleton />
<FavoriteStatsCardSkeleton />
</div>
</div>
</Layout>
)
}
76 changes: 73 additions & 3 deletions src/app/dashboard/components/stats-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Link from "next/link";

import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Skeleton} from "@/components/ui/skeleton";
import {type FavoritesResult} from "@/lib/db/get-metrics";

type StatsCardProps = {
title: string;
Expand All @@ -10,17 +11,50 @@ type StatsCardProps = {
href?: string;
}

type FavoriteStatsCardProps = {
title: string;
subtitle: string;
values: FavoritesResult;
}

export function StatsCardSkeleton() {
return (
<Card>
<CardHeader className={"space-y-0 pb-2"}>
<CardTitle className={"text-sm font-medium"}>
<Skeleton className={"h-4 my-1 w-3/4"} />
<Skeleton className={"h-4 my-1 w-3/4"}/>
</CardTitle>
</CardHeader>
<CardContent>
<Skeleton className={"h-6 w-1/2"}/>
<Skeleton className={"h-3 mt-2"}/>
</CardContent>
</Card>
)
}

export function FavoriteStatsCardSkeleton() {
return (
<Card className={"col-span-2"}>
<CardHeader className={"space-y-0 pb-2"}>
<CardTitle className={"text-sm font-medium"}>
<Skeleton className={"h-4 my-1 w-3/4"}/>
<Skeleton className={"h-3 mt-2"}/>
</CardTitle>
</CardHeader>
<CardContent>
<Skeleton className={"h-6 w-1/2"} />
<Skeleton className={"h-3 mt-2"} />
<div className={"text-base font-medium space-y-1"}>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
<Skeleton className={"h-6 w-full"}/>
</div>
</CardContent>
</Card>
)
Expand All @@ -42,3 +76,39 @@ export function StatsCard(props: StatsCardProps) {
</Card>
)
}

function FavoriteRow(props: { name: string, total: string }) {
return (
<div className={"flex justify-between items-center hover:bg-primary/20 p-1 px-1.5 cursor-default rounded-sm"}>
<div className={"grow shrink-0"}>{props.name}</div>
<div>{props.total}</div>
</div>
)
}

export function FavoriteStatsCard(props: FavoriteStatsCardProps) {
return (
<Card className={"col-span-2"}>
<CardHeader className={"space-y-0 pb-2"}>
<CardTitle className={"text-base font-medium"}>
{props.title}
<p className={"text-sm text-muted-foreground"}>{props.subtitle}</p>
</CardTitle>
</CardHeader>
<CardContent className={"h-full"}>
{!!props.values.length && (
<div className={"text-base font-medium space-y-1"}>
{props.values.map(value => (
<FavoriteRow key={`${props.title}-${props.subtitle}-${value.name}`} {...value} />
))}
</div>
)}
{!props.values.length && (
<div className={"flex flex-col text-base justify-center items-center h-full text-muted-foreground"}>
{`No ${props.title.toLowerCase()} yet`}
</div>
)}
</CardContent>
</Card>
)
}
59 changes: 57 additions & 2 deletions src/lib/db/get-metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import {eq, sql} from "drizzle-orm";
import {cache} from "react";

import {db} from "@/db";
import {beans, cafeBrews, freezeEntries, roasters} from "@/db/schema";
import {beans, beanVarieties, cafeBrews, freezeEntries, roasters} from "@/db/schema";

export type FavoritesResult = {name: string, total: string}[];

type Metrics = {
roasterCount: number;
beansCount: number;
freezerCount: number;
cafeBrewsCount: number;
favoriteRoastersByBag: FavoritesResult
favoriteRoastersByWeight: FavoritesResult,
favoriteVarieties: FavoritesResult,
favoriteOrigins: FavoritesResult,
}

export const revalidate = 900; // Revalidate every 15 minutes at most
Expand All @@ -19,6 +25,10 @@ export const getMetrics = cache(async (userId: number): Promise<Metrics> => {
beansCount: 0,
freezerCount: 0,
cafeBrewsCount: 0,
favoriteRoastersByBag: [],
favoriteRoastersByWeight: [],
favoriteVarieties: [],
favoriteOrigins: [],
}

const [roaster] = await db
Expand All @@ -36,12 +46,57 @@ export const getMetrics = cache(async (userId: number): Promise<Metrics> => {
const [freezer] = await db
.select({count: sql<number>`count(*)`})
.from(freezeEntries)
.where(eq(freezeEntries.userId, userId))
.where(eq(freezeEntries.userId, userId));

const favoriteRoasters = await db
.execute(sql`
SELECT ${roasters.name} name, count(*) total, sum(${beans.weight}) as total_weight
FROM ${roasters}
INNER JOIN ${beans} ON ${roasters.id} = ${beans.roasterId}
WHERE ${roasters.userId} = ${userId}
GROUP BY ${roasters.name}
ORDER BY total DESC
LIMIT 10;
`);

const favoriteVarieties = await db
.execute(sql`
SELECT ${beanVarieties.name} name, count(*) as total
FROM ${beanVarieties}
INNER JOIN ${beans} ON ${beanVarieties.beanId} = ${beans.id}
WHERE ${beans.userId} = ${userId} AND ${beanVarieties.name} IS NOT NULL
GROUP BY ${beanVarieties.name}
ORDER BY total DESC
LIMIT 10;
`);

const favoriteOrigins = await db
.execute(sql`
SELECT ${beanVarieties.country} name, count(*) as total
FROM ${beanVarieties}
INNER JOIN ${beans} ON ${beanVarieties.beanId} = ${beans.id}
WHERE ${beans.userId} = ${userId} AND ${beanVarieties.country} IS NOT NULL
GROUP BY ${beanVarieties.country}
ORDER BY total DESC
LIMIT 10;
`);

const favoriteRoastersByBag: FavoritesResult = [];
const favoriteRoastersByWeight: FavoritesResult = [];

for (const row of favoriteRoasters.rows as {name: string, total: string, total_weight: string}[]) {
favoriteRoastersByBag.push({name: row.name, total: row.total})
favoriteRoastersByWeight.push({name: row.name, total: row.total_weight})
}

return {
roasterCount: roaster.count,
beansCount: bean.count,
freezerCount: freezer.count,
cafeBrewsCount: cafeBrew.count,
favoriteRoastersByBag,
favoriteRoastersByWeight,
favoriteVarieties: favoriteVarieties.rows as FavoritesResult,
favoriteOrigins: favoriteOrigins.rows as FavoritesResult,
};
});

0 comments on commit c5f9a59

Please sign in to comment.