Files
Epicure/apps/web/app/(app)/collections/page.tsx
T
Arnaud b1f745da66 fix: batch of collections/i18n/translate-button fixes (v0.53.1)
- Drag-reorder used verticalListSortingStrategy on a multi-column grid,
  which computes wrong transforms for grid reflow — swapped to
  rectSortingStrategy so cards actually animate live while dragging.
- Grip handle was rendered as a sibling of (not a descendant of) the
  `group` element its `group-hover:opacity-100` depended on, so it was
  permanently invisible. Fixed the DOM nesting and made it always
  partially visible instead of hover-only.
- common.edit was missing from both locales (not just French) —
  edit-collection-dialog.tsx was the first caller to hit it.
- Root cause of "generated in my language but Translate still shows":
  generate-meal, meal-plan/generate, and adapt never set recipes.language
  on the row they inserted, so the button's `!recipe.language || ...`
  check always fell back to "show it". Fixed at all three insert sites.
- Translate dialog was entirely hardcoded English (title, description,
  language names, buttons) despite i18n keys already existing for most of
  it — now uses them, plus new translated language-name keys.
- Recipe tags now render on the recipe detail page (previously grid-card
  only).
- Collection header actions converted to icon-only + tooltip, matching
  the recipe page's pattern instead of icon+label buttons.
- Collections list search now also matches recipe titles inside each
  collection, not just the collection's own name/description.
- Explore page links to /collections/explore next to its tabs.

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

86 lines
2.9 KiB
TypeScript

import type { Metadata } from "next";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db, collections, collectionRecipes, eq, and, or, ilike, sql } from "@epicure/db";
import { CollectionsPageContent } from "@/components/collections/collections-page-content";
import { getPublicUrl } from "@/lib/storage";
export const metadata: Metadata = {};
export default async function CollectionsPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return null;
const { q } = await searchParams;
const query = q?.trim();
const where = query
? and(
eq(collections.userId, session.user.id),
or(
ilike(collections.name, `%${query}%`),
ilike(collections.description, `%${query}%`),
sql`exists (
select 1 from collection_recipes cr
inner join recipes r on r.id = cr.recipe_id
where cr.collection_id = ${collections.id} and r.title ilike ${`%${query}%`}
)`
)
)
: eq(collections.userId, session.user.id);
const [userCollections, countRows] = await Promise.all([
db.query.collections.findMany({
where,
orderBy: (t, { desc }) => desc(t.updatedAt),
with: {
recipes: {
limit: 4,
orderBy: (t, { asc }) => asc(t.position),
with: { recipe: { with: { photos: true } } },
},
},
}),
// Separate grouped count — the `with: { recipes: { limit: 4 } }` above is
// capped for thumbnail previews, so `.recipes.length` off that relation
// would only ever report up to 4, never the real total.
db
.select({ collectionId: collectionRecipes.collectionId, count: sql<number>`count(*)::int` })
.from(collectionRecipes)
.innerJoin(collections, eq(collectionRecipes.collectionId, collections.id))
.where(eq(collections.userId, session.user.id))
.groupBy(collectionRecipes.collectionId),
]);
const countByCollection = new Map(countRows.map((r) => [r.collectionId, r.count]));
return (
<CollectionsPageContent
query={query ?? ""}
collections={userCollections.map((col) => ({
id: col.id,
name: col.name,
description: col.description,
tags: col.tags,
isPublic: col.isPublic,
recipeCount: countByCollection.get(col.id) ?? 0,
thumbnails: col.recipes.flatMap((r) => {
if (!r.recipe) return [];
const cover = r.recipe.photos.find((p) => p.isCover) ?? r.recipe.photos[0];
return [{
recipeId: r.recipe.id,
recipeType: r.recipe.recipeType,
coverIcon: r.recipe.coverIcon,
coverColor: r.recipe.coverColor,
photoUrl: cover ? getPublicUrl(cover.storageKey) : null,
}];
}),
}))}
/>
);
}