diff --git a/.claude/settings.local.json b/.claude/settings.local.json index efa8525..6406045 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -11,7 +11,9 @@ "Bash(dir:*)", "Bash(findstr:*)", "Bash(timeout 10 npm run dev:*)", - "Bash(grep:*)" + "Bash(grep:*)", + "Read(//c/Users/runya/Documents/repositories/recipes/recipes/docs/**)", + "Bash(node scripts/import-recipes.js:*)" ] } } diff --git a/app/page.tsx b/app/page.tsx index 9b1969d..2a619ec 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,4 @@ +import Image from 'next/image'; import Link from 'next/link'; export default function Home() { @@ -26,27 +27,65 @@ export default function Home() { -
-
-
🍳
-

Easy Recipes

-

- Step-by-step instructions for home cooks of all skill levels -

+
+
+
+ Person deciding what to cook +
+
+

Easy Recipes

+

+ Step-by-step instructions for home cooks of all skill levels. Whether you're a beginner or an experienced cook, every recipe is written with clarity in mind. +

+ + Browse Recipes + +
-
-
📸
-

Beautiful Photos

-

- High-quality images to inspire your cooking journey -

+ +
+
+ Online cookbook illustration +
+
+

Beautiful Photos

+

+ High-quality images accompany every recipe to inspire your cooking journey. See exactly what you're making before you start. +

+
-
-
🏷️
-

Organized

-

- Find recipes by tags, ingredients, and dietary preferences -

+ +
+
+ Organized recipe collection +
+
+

No Nonsense

+

+ Just recipes. Find what you need quickly by browsing categories, filtering by tags, or searching by ingredient. +

+
diff --git a/app/recipes/page.tsx b/app/recipes/page.tsx index b36a75c..8039ad2 100644 --- a/app/recipes/page.tsx +++ b/app/recipes/page.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react'; import type { Metadata } from 'next'; import { getAllRecipes, getAllCategories, getAllTags } from '@/lib/recipes'; import RecipesClient from '@/components/RecipesClient'; @@ -23,7 +24,13 @@ export default function RecipesPage() {

- + + Loading recipes... + + }> + + ); } diff --git a/components/RecipeLayout.tsx b/components/RecipeLayout.tsx index 275a197..33f2f44 100644 --- a/components/RecipeLayout.tsx +++ b/components/RecipeLayout.tsx @@ -1,12 +1,14 @@ 'use client'; import { useState, useCallback, ReactNode } from 'react'; -import RecipesSidebar, { type FilterState } from './RecipesSidebar'; +import RecipesSidebar from './RecipesSidebar'; +import type { FilterState } from '@/lib/types'; interface RecipeLayoutProps { children: ReactNode; categories: string[]; tags: string[]; + filters?: FilterState; onFilterChange?: (filters: FilterState) => void; showFilters?: boolean; } @@ -15,14 +17,15 @@ export default function RecipeLayout({ children, categories, tags, + filters, onFilterChange, showFilters = true }: RecipeLayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(false); - const handleFilterChange = useCallback((filters: FilterState) => { + const handleFilterChange = useCallback((newFilters: FilterState) => { if (onFilterChange) { - onFilterChange(filters); + onFilterChange(newFilters); } }, [onFilterChange]); @@ -76,10 +79,11 @@ export default function RecipeLayout({ {/* Sidebar content */} - {showFilters && ( + {showFilters && filters && ( )} diff --git a/components/RecipePageClient.tsx b/components/RecipePageClient.tsx index f2edfc7..23cd6e7 100644 --- a/components/RecipePageClient.tsx +++ b/components/RecipePageClient.tsx @@ -28,7 +28,12 @@ export default function RecipePageClient({ recipe, sections }: RecipePageClientP
- {recipe.category} + + {recipe.category} +
diff --git a/components/RecipeTabs.tsx b/components/RecipeTabs.tsx index a43b904..342baee 100644 --- a/components/RecipeTabs.tsx +++ b/components/RecipeTabs.tsx @@ -127,7 +127,6 @@ export default function RecipeTabs({ sections, folderPath }: RecipeTabsProps) { {children} ), - // Don't override

- let ReactMarkdown handle it to avoid hydration errors code: ({ inline, children, ...props }: any) => { if (inline) { return ( diff --git a/components/RecipesClient.tsx b/components/RecipesClient.tsx index 640838e..981fb39 100644 --- a/components/RecipesClient.tsx +++ b/components/RecipesClient.tsx @@ -1,10 +1,11 @@ 'use client'; -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; +import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import RecipeLayout from './RecipeLayout'; -import { FilterState } from './RecipesSidebar'; import RecipeCard from './RecipeCard'; import type { Recipe } from '@/lib/recipes'; +import type { FilterState } from '@/lib/types'; interface RecipesClientProps { recipes: Recipe[]; @@ -12,12 +13,52 @@ interface RecipesClientProps { tags: string[]; } +function parseFiltersFromParams(searchParams: URLSearchParams): FilterState { + return { + search: searchParams.get('search') || '', + category: searchParams.get('category') || '', + selectedTags: searchParams.get('tags') + ? [...new Set(searchParams.get('tags')!.split(',').filter(Boolean))] + : [], + }; +} + +function buildQueryString(filters: FilterState): string { + const params = new URLSearchParams(); + if (filters.search) params.set('search', filters.search); + if (filters.category) params.set('category', filters.category); + if (filters.selectedTags.length > 0) params.set('tags', filters.selectedTags.join(',')); + const qs = params.toString(); + return qs ? `?${qs}` : ''; +} + export default function RecipesClient({ recipes, categories, tags }: RecipesClientProps) { - const [filters, setFilters] = useState({ - search: '', - category: '', - selectedTags: [], - }); + const searchParams = useSearchParams(); + const router = useRouter(); + const pathname = usePathname(); + + const [filters, setFilters] = useState(() => + parseFiltersFromParams(searchParams) + ); + + // Track internal updates to avoid reacting to our own URL changes + const isInternalUpdate = useRef(false); + + // Sync URL → state on browser back/forward + useEffect(() => { + if (isInternalUpdate.current) { + isInternalUpdate.current = false; + return; + } + setFilters(parseFiltersFromParams(searchParams)); + }, [searchParams]); + + // Update filters and sync to URL + const updateFilters = useCallback((newFilters: FilterState) => { + isInternalUpdate.current = true; + setFilters(newFilters); + router.replace(`${pathname}${buildQueryString(newFilters)}`, { scroll: false }); + }, [router, pathname]); const filteredRecipes = useMemo(() => { return recipes.filter((recipe) => { @@ -47,7 +88,8 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie

diff --git a/components/RecipesSidebar.tsx b/components/RecipesSidebar.tsx index 5f1af95..3e16aab 100644 --- a/components/RecipesSidebar.tsx +++ b/components/RecipesSidebar.tsx @@ -3,49 +3,54 @@ import { useState, useEffect } from 'react'; import SelectedTags from './SelectedTags'; import TagSelector from './TagSelector'; +import type { FilterState } from '@/lib/types'; interface RecipesSidebarProps { categories: string[]; tags: string[]; + filters: FilterState; onFilterChange: (filters: FilterState) => void; } -export interface FilterState { - search: string; - category: string; - selectedTags: string[]; -} - -export default function RecipesSidebar({ categories, tags, onFilterChange }: RecipesSidebarProps) { - const [search, setSearch] = useState(''); - const [category, setCategory] = useState(''); - const [selectedTags, setSelectedTags] = useState([]); +export default function RecipesSidebar({ categories, tags, filters, onFilterChange }: RecipesSidebarProps) { + const [searchInput, setSearchInput] = useState(filters.search); + // Sync external search changes (e.g. "Clear All" or URL-driven) to local input useEffect(() => { - onFilterChange({ search, category, selectedTags }); - }, [search, category, selectedTags, onFilterChange]); + setSearchInput(filters.search); + }, [filters.search]); + + // Debounce local search input → parent + useEffect(() => { + const timer = setTimeout(() => { + if (searchInput !== filters.search) { + onFilterChange({ ...filters, search: searchInput }); + } + }, 300); + return () => clearTimeout(timer); + }, [searchInput, filters, onFilterChange]); const handleTagToggle = (tag: string) => { - setSelectedTags((prev) => - prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] - ); + const selectedTags = filters.selectedTags.includes(tag) + ? filters.selectedTags.filter((t) => t !== tag) + : [...filters.selectedTags, tag]; + onFilterChange({ ...filters, selectedTags }); }; const handleRemoveTag = (tag: string) => { - setSelectedTags((prev) => prev.filter((t) => t !== tag)); + onFilterChange({ ...filters, selectedTags: filters.selectedTags.filter((t) => t !== tag) }); }; const handleClearTags = () => { - setSelectedTags([]); + onFilterChange({ ...filters, selectedTags: [] }); }; const handleClearFilters = () => { - setSearch(''); - setCategory(''); - setSelectedTags([]); + setSearchInput(''); + onFilterChange({ search: '', category: '', selectedTags: [] }); }; - const hasActiveFilters = search || category || selectedTags.length > 0; + const hasActiveFilters = searchInput || filters.category || filters.selectedTags.length > 0; return (
@@ -57,8 +62,8 @@ export default function RecipesSidebar({ categories, tags, onFilterChange }: Rec setSearch(e.target.value)} + value={searchInput} + onChange={(e) => setSearchInput(e.target.value)} placeholder="Search recipes..." className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" /> @@ -70,8 +75,8 @@ export default function RecipesSidebar({ categories, tags, onFilterChange }: Rec