Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"mounts": [
"source=${localEnv:SSH_AUTH_SOCK},target=/ssh-agent,type=bind"
],
"remoteEnv": { "VSCODE_AGENT_FOLDER": "/home/node/.vscode-server" },
"remoteEnv": {
"SSH_AUTH_SOCK": "/ssh-agent",
"VSCODE_AGENT_FOLDER": "/home/node/.vscode-server"
},
"runArgs": ["--memory=6g", "--shm-size=1g"],

"customizations": {
Expand Down
25 changes: 25 additions & 0 deletions src/app/api/recipes/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { db } from '@/lib/db';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
request: NextRequest,
{ params }: { params: { slug: string } }
) {
try {
const { slug } = params;
const recipe = await db.recipe.findFirst({
where: { slug },
orderBy: { version: 'desc' },
include: { ingredients: true, steps: true, components: true, media: true, book: true }
});

if (!recipe) {
return NextResponse.json({ error: 'Recipe not found' }, { status: 404 });
}

return NextResponse.json(recipe);
} catch (error) {
console.error('Error fetching recipe:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
55 changes: 55 additions & 0 deletions src/app/api/recipes/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { db } from '@/lib/db';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q')?.trim();
const limit = parseInt(searchParams.get('limit') || '50');

if (!query) {
// If no query, return latest recipes
const recipes = await db.recipe.findMany({
orderBy: [{ slug: 'asc' }, { version: 'desc' }],
take: limit,
include: { ingredients: true }
});

const latestBySlug = new Map<string, typeof recipes[number]>();
for (const r of recipes) if (!latestBySlug.has(r.slug)) latestBySlug.set(r.slug, r);

return NextResponse.json(Array.from(latestBySlug.values()));
}

// Search recipes by title, slug, description, and ingredients
const recipes = await db.recipe.findMany({
where: {
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ slug: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ subtitle: { contains: query, mode: 'insensitive' } },
{
ingredients: {
some: {
item: { contains: query, mode: 'insensitive' }
}
}
}
]
},
orderBy: [{ slug: 'asc' }, { version: 'desc' }],
take: limit,
include: { ingredients: true }
});

// Get latest version of each recipe
const latestBySlug = new Map<string, typeof recipes[number]>();
for (const r of recipes) if (!latestBySlug.has(r.slug)) latestBySlug.set(r.slug, r);

return NextResponse.json(Array.from(latestBySlug.values()));
} catch (error) {
console.error('Error searching recipes:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
81 changes: 72 additions & 9 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,89 @@
import { db } from '@/lib/db';
'use client';

import { useState, useEffect } from 'react';
import Link from 'next/link';

export default async function HomePage(){
const recipes = await db.recipe.findMany({ orderBy: [{ slug: 'asc' }, { version: 'desc' }] });
const latestBySlug = new Map<string, typeof recipes[number]>();
for (const r of recipes) if (!latestBySlug.has(r.slug)) latestBySlug.set(r.slug, r);
export default function HomePage(){
const [recipes, setRecipes] = useState<any[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false);
const [debouncedQuery, setDebouncedQuery] = useState('');

// Debounce search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(searchQuery);
}, 300);

return () => clearTimeout(timer);
}, [searchQuery]);

// Fetch recipes based on search query
useEffect(() => {
async function fetchRecipes() {
setLoading(true);
try {
const params = new URLSearchParams();
if (debouncedQuery) {
params.append('q', debouncedQuery);
}
const response = await fetch(`/api/recipes/search?${params}`);
if (response.ok) {
const data = await response.json();
setRecipes(data);
}
} catch (error) {
console.error('Error fetching recipes:', error);
} finally {
setLoading(false);
}
}

fetchRecipes();
}, [debouncedQuery]);

return (
<main className="space-y-4">
<h2 className="text-xl font-semibold">All Recipes (latest)</h2>
<main className="space-y-6">
<div className="space-y-4">
<h2 className="text-xl font-semibold">All Recipes (latest)</h2>

<div>
<input
type="text"
placeholder="Search recipes by title, ingredients, or description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="input w-full"
/>
</div>

{loading && (
<div className="text-center text-gray-500">Searching...</div>
)}
</div>

<div className="grid gap-3">
{Array.from(latestBySlug.values()).map(r => (
<Link key={r.id} href={`/recipes/${r.slug}`} className="card">
{recipes.map(r => (
<Link key={r.id} href={`/recipes/${r.slug}`} className="card hover:shadow-md transition-shadow">
<div className="flex items-center justify-between">
<div>
<div className="font-medium">{r.title}</div>
<div className="text-sm text-gray-500">{r.slug} — v{r.version}</div>
{r.description && (
<div className="text-sm text-gray-600 mt-1 line-clamp-2">{r.description}</div>
)}
</div>
<div className="text-sm text-gray-500">{r.yieldQty ? `${r.yieldQty} ${r.yieldUnit ?? ''}` : ''}</div>
</div>
</Link>
))}
</div>

{!loading && recipes.length === 0 && debouncedQuery && (
<div className="text-center text-gray-500">
No recipes found for "{debouncedQuery}"
</div>
)}
</main>
);
}
5 changes: 4 additions & 1 deletion src/app/recipes/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export default async function RecipePage({ params }: { params: { slug: string }
<h2 className="text-2xl font-semibold">{recipe.title}</h2>
<div className="text-sm text-gray-500">{recipe.slug} — v{recipe.version}</div>
</div>
<div className="text-sm text-gray-600">{recipe.yieldQty ? `${recipe.yieldQty} ${recipe.yieldUnit ?? ''}` : ''}</div>
<div className="flex items-center gap-4">
<div className="text-sm text-gray-600">{recipe.yieldQty ? `${recipe.yieldQty} ${recipe.yieldUnit ?? ''}` : ''}</div>
<Link href={`/recipes/${recipe.slug}/print`} className="btn">Print</Link>
</div>
</div>

<section className="grid gap-6 md:grid-cols-2">
Expand Down
Loading
Loading