feat(recipes): full recipe CRUD with photos, print, version history

List, detail, new, edit pages. Server-side pagination, dietary tags, difficulty.
Photo upload to S3-compatible storage. Version history. Multi-select grid with
bulk delete/visibility. Print view. Delete confirmation dialog.
This commit is contained in:
Arnaud
2026-07-01 08:10:11 +02:00
parent fa2d797918
commit 84d6cfeb07
45 changed files with 5300 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
import type { Metadata } from "next";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db, recipes, recipeIngredients } from "@epicure/db";
import { eq, desc, and, ilike, or, sql } from "@epicure/db";
import { RecipesHeader } from "@/components/recipe/recipes-header";
import { RecipesEmptyState } from "@/components/recipe/recipes-empty-state";
import { RecipesGrid } from "@/components/recipe/recipes-grid";
export const metadata: Metadata = { title: "Recipes" };
type SearchParams = Promise<{ q?: string }>;
export default async function RecipesPage({ searchParams }: { searchParams: SearchParams }) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return null;
const { q } = await searchParams;
const query = q?.trim() ?? "";
const whereClause = query
? and(
eq(recipes.authorId, session.user.id),
or(
ilike(recipes.title, `%${query}%`),
ilike(recipes.description, `%${query}%`)
)
)
: eq(recipes.authorId, session.user.id);
const userRecipes = await db.query.recipes.findMany({
where: whereClause,
orderBy: desc(recipes.updatedAt),
with: { photos: true },
});
const totalCount = query ? undefined : userRecipes.length;
return (
<div className="space-y-6">
<RecipesHeader count={totalCount ?? userRecipes.length} initialQuery={query} />
<RecipesEmptyState query={query} count={userRecipes.length} />
<RecipesGrid recipes={userRecipes} />
</div>
);
}