212d6f7335
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>
132 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|