feat(search): full-text recipe search and explore page
Postgres full-text search with dietary/difficulty/time filters. Search page with real-time results. Explore page with trending and recent recipes.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import type { Metadata } from "next";
|
||||
import {
|
||||
db,
|
||||
recipes,
|
||||
users,
|
||||
favorites,
|
||||
eq,
|
||||
desc,
|
||||
sql,
|
||||
and,
|
||||
gte,
|
||||
count,
|
||||
} from "@epicure/db";
|
||||
import { ExplorePageContent } from "@/components/search/explore-page-content";
|
||||
|
||||
export const metadata: Metadata = { title: "Explore — Epicure" };
|
||||
|
||||
export type RecipeResult = {
|
||||
id: string;
|
||||
title: string;
|
||||
authorName: string | null;
|
||||
difficulty: string | null;
|
||||
prepMins: number | null;
|
||||
cookMins: number | null;
|
||||
};
|
||||
|
||||
export default async function ExplorePage() {
|
||||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Trending: public recipes ordered by favorite count in last 7 days
|
||||
const trendingRows = await db
|
||||
.select({
|
||||
id: recipes.id,
|
||||
title: recipes.title,
|
||||
authorName: users.name,
|
||||
difficulty: recipes.difficulty,
|
||||
prepMins: recipes.prepMins,
|
||||
cookMins: recipes.cookMins,
|
||||
favoriteCount: count(favorites.recipeId),
|
||||
})
|
||||
.from(recipes)
|
||||
.innerJoin(users, eq(recipes.authorId, users.id))
|
||||
.leftJoin(
|
||||
favorites,
|
||||
and(
|
||||
eq(favorites.recipeId, recipes.id),
|
||||
gte(favorites.createdAt, sevenDaysAgo)
|
||||
)
|
||||
)
|
||||
.where(eq(recipes.visibility, "public"))
|
||||
.groupBy(recipes.id, users.id)
|
||||
.orderBy(desc(sql`count(${favorites.recipeId})`))
|
||||
.limit(12);
|
||||
|
||||
// Recent: public recipes ordered by createdAt desc
|
||||
const recentRows = await db
|
||||
.select({
|
||||
id: recipes.id,
|
||||
title: recipes.title,
|
||||
authorName: users.name,
|
||||
difficulty: recipes.difficulty,
|
||||
prepMins: recipes.prepMins,
|
||||
cookMins: recipes.cookMins,
|
||||
})
|
||||
.from(recipes)
|
||||
.innerJoin(users, eq(recipes.authorId, users.id))
|
||||
.where(eq(recipes.visibility, "public"))
|
||||
.orderBy(desc(recipes.createdAt))
|
||||
.limit(12);
|
||||
|
||||
const trending: RecipeResult[] = trendingRows.map(({ favoriteCount: _fc, ...r }) => r);
|
||||
const recent: RecipeResult[] = recentRows;
|
||||
|
||||
return <ExplorePageContent trending={trending} recent={recent} />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SearchPageContent } from "@/components/search/search-page-content";
|
||||
|
||||
type Params = { searchParams: Promise<{ q?: string; difficulty?: string; dietary?: string }> };
|
||||
|
||||
export default async function SearchPage({ searchParams }: Params) {
|
||||
const { q } = await searchParams;
|
||||
return <SearchPageContent initialQuery={q ?? ""} />;
|
||||
}
|
||||
Reference in New Issue
Block a user