Files
Epicure/apps/web/app/admin/storage/page.tsx
T
Arnaud 212d6f7335 fix: browser tab title should always just say "Epicure"
Root layout used title.template ("%s | Epicure") and 38 pages set
their own title, producing "Recipes | Epicure", "Messages — Epicure",
etc. Drop the template, set a flat "Epicure" default, and clear every
page's title override so they all inherit it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 14:58:03 +02:00

132 lines
5.1 KiB
TypeScript

import type { Metadata } from "next";
import { db, users, recipePhotos, recipes, eq, desc, sql, count } from "@epicure/db";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { HardDrive } from "lucide-react";
export const metadata: Metadata = {};
export default async function AdminStoragePage() {
const [totalPhotos, recipesWithPhotos, photosByTier, topUsers] = await Promise.all([
db.select({ count: count() }).from(recipePhotos).then((r) => r[0]),
db
.select({ count: count() })
.from(recipePhotos)
.where(eq(recipePhotos.isCover, true))
.then((r) => r[0]),
db
.select({
tier: users.tier,
photoCount: sql<string>`count(${recipePhotos.id})`,
})
.from(recipePhotos)
.innerJoin(recipes, eq(recipePhotos.recipeId, recipes.id))
.innerJoin(users, eq(recipes.authorId, users.id))
.groupBy(users.tier),
db
.select({
userId: users.id,
name: users.name,
email: users.email,
tier: users.tier,
photoCount: sql<string>`count(${recipePhotos.id})`,
})
.from(recipePhotos)
.innerJoin(recipes, eq(recipePhotos.recipeId, recipes.id))
.innerJoin(users, eq(recipes.authorId, users.id))
.groupBy(users.id, users.name, users.email, users.tier)
.orderBy(desc(sql`count(${recipePhotos.id})`))
.limit(10),
]);
const freePhotos = Number(photosByTier.find((r) => r.tier === "free")?.photoCount ?? 0);
const proPhotos = Number(photosByTier.find((r) => r.tier === "pro")?.photoCount ?? 0);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Storage Usage</h1>
<p className="text-muted-foreground text-sm mt-1">
Photo storage breakdown by tier and user.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Photos</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{(totalPhotos?.count ?? 0).toLocaleString()}</div>
<p className="text-xs text-muted-foreground mt-1">{recipesWithPhotos?.count ?? 0} cover photos</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Free Tier Photos</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{freePhotos.toLocaleString()}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Pro Tier Photos</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{proPhotos.toLocaleString()}</div>
</CardContent>
</Card>
</div>
<div>
<h2 className="text-lg font-semibold mb-3">Top 10 Users by Photo Count</h2>
<div className="rounded-md border">
<table className="w-full text-sm">
<thead className="border-b bg-muted/50">
<tr>
<th className="px-4 py-3 text-left font-medium">User</th>
<th className="px-4 py-3 text-left font-medium">Tier</th>
<th className="px-4 py-3 text-right font-medium">Photos</th>
</tr>
</thead>
<tbody>
{topUsers.length === 0 ? (
<tr>
<td colSpan={3} className="px-4 py-8 text-center text-muted-foreground">
No photos uploaded yet.
</td>
</tr>
) : (
topUsers.map((row) => (
<tr key={row.userId} className="border-b last:border-0 hover:bg-muted/30">
<td className="px-4 py-3">
<div className="font-medium">{row.name ?? "Unknown"}</div>
{row.email && (
<div className="text-xs text-muted-foreground">{row.email}</div>
)}
</td>
<td className="px-4 py-3">
<Badge variant={row.tier === "pro" ? "default" : "secondary"}>
{row.tier ?? "—"}
</Badge>
</td>
<td className="px-4 py-3 text-right font-mono">
{Number(row.photoCount).toLocaleString()}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}