cba5d9c3ac
Sticky sidebar with back-to-app link. Overview: user counts, recipe photos, AI calls/month. User management with role/tier editing. Audit log (all admin actions). Storage page with photo breakdown by tier. AI config showing DB vs .env key source. Site settings page to override .env values at runtime (encrypted in DB).
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { db, recipes, users, eq, desc } from "@epicure/db";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import Link from "next/link";
|
|
|
|
export const metadata: Metadata = { title: "Recipe Moderation" };
|
|
|
|
const VISIBILITY_COLORS = {
|
|
public: "default",
|
|
unlisted: "outline",
|
|
private: "secondary",
|
|
} as const;
|
|
|
|
export default async function AdminRecipesPage() {
|
|
const publicRecipes = await db
|
|
.select({
|
|
id: recipes.id,
|
|
title: recipes.title,
|
|
visibility: recipes.visibility,
|
|
createdAt: recipes.createdAt,
|
|
authorName: users.name,
|
|
authorId: users.id,
|
|
})
|
|
.from(recipes)
|
|
.leftJoin(users, eq(recipes.authorId, users.id))
|
|
.where(eq(recipes.visibility, "public"))
|
|
.orderBy(desc(recipes.createdAt))
|
|
.limit(200);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Recipe Moderation</h1>
|
|
<p className="text-muted-foreground text-sm mt-1">
|
|
Public recipes sorted by newest. Review and decide manually.
|
|
</p>
|
|
</div>
|
|
|
|
<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">Title</th>
|
|
<th className="px-4 py-3 text-left font-medium">Author</th>
|
|
<th className="px-4 py-3 text-left font-medium">Visibility</th>
|
|
<th className="px-4 py-3 text-left font-medium">Created</th>
|
|
<th className="px-4 py-3 text-left font-medium">Link</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{publicRecipes.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">
|
|
No public recipes yet.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
publicRecipes.map((recipe) => (
|
|
<tr key={recipe.id} className="border-b last:border-0 hover:bg-muted/30">
|
|
<td className="px-4 py-3 font-medium">{recipe.title}</td>
|
|
<td className="px-4 py-3 text-muted-foreground">
|
|
{recipe.authorId ? (
|
|
<Link
|
|
href={`/admin/users/${recipe.authorId}`}
|
|
className="hover:underline underline-offset-4"
|
|
>
|
|
{recipe.authorName ?? "Unknown"}
|
|
</Link>
|
|
) : (
|
|
<span>{recipe.authorName ?? "Unknown"}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<Badge variant={VISIBILITY_COLORS[recipe.visibility]}>
|
|
{recipe.visibility}
|
|
</Badge>
|
|
</td>
|
|
<td className="px-4 py-3 text-muted-foreground">
|
|
{recipe.createdAt.toLocaleDateString()}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<Link
|
|
href={`/r/${recipe.id}`}
|
|
className="text-primary underline-offset-4 hover:underline text-xs"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
View
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|