b1f745da66
- 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>
86 lines
2.9 KiB
TypeScript
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,
|
|
}];
|
|
}),
|
|
}))}
|
|
/>
|
|
);
|
|
}
|