diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 14e270f..efa8525 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,12 @@ "Bash(npm --version:*)", "Bash(where:*)", "Bash(npm install:*)", - "Bash(npm run build:*)" + "Bash(npm run build:*)", + "Bash(node scripts/copy-recipe-assets.js:*)", + "Bash(dir:*)", + "Bash(findstr:*)", + "Bash(timeout 10 npm run dev:*)", + "Bash(grep:*)" ] } } diff --git a/app/globals.css b/app/globals.css index 13d40b8..410f84e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,4 +24,16 @@ body { .text-balance { text-wrap: balance; } + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } } diff --git a/app/recipes/[category]/[slug]/page.tsx b/app/recipes/[category]/[slug]/page.tsx index d54c904..2d807e8 100644 --- a/app/recipes/[category]/[slug]/page.tsx +++ b/app/recipes/[category]/[slug]/page.tsx @@ -1,6 +1,8 @@ import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; import { getRecipeByCategoryAndSlug, getAllRecipePaths } from '@/lib/recipes'; +import { parseRecipeSections } from '@/lib/parseRecipe'; +import RecipePageClient from '@/components/RecipePageClient'; interface RecipePageProps { params: Promise<{ @@ -47,86 +49,12 @@ export default async function RecipePage({ params }: RecipePageProps) { notFound(); } + const sections = parseRecipeSections(recipe.content); + return ( -
-
-
- {recipe.category} - - - {recipe.lastUpdated !== recipe.date && ( - <> - - - - )} -
- -

- {recipe.title} -

- -

- {recipe.description} -

- -
-
- Prep: - {recipe.prepTime} min -
-
- Cook: - {recipe.cookTime} min -
-
- Total: - {recipe.totalTime} min -
-
- Servings: - {recipe.servings} -
-
- Difficulty: - {recipe.difficulty} -
-
- -
- {recipe.tags.map((tag) => ( - - {tag} - - ))} -
-
- -
-
-

- Recipe content will be rendered here from MDX -

-
-            {recipe.content.substring(0, 500)}...
-          
-
-
-
+ ); } diff --git a/components/RecipeCard.tsx b/components/RecipeCard.tsx index 3ff8733..8a366de 100644 --- a/components/RecipeCard.tsx +++ b/components/RecipeCard.tsx @@ -7,22 +7,21 @@ interface RecipeCardProps { } export default function RecipeCard({ recipe }: RecipeCardProps) { - const imageRelativePath = recipe.displayPhoto.startsWith('./') - ? `${recipe.folderPath}/${recipe.displayPhoto.replace('./', '')}` + // Convert relative path to public URL + const imageSrc = recipe.displayPhoto.startsWith('./') + ? `/recipes/${recipe.folderPath}/${recipe.displayPhoto.replace('./', '')}`.replace(/\\/g, '/') : recipe.displayPhoto; - const normalizedPath = imageRelativePath.replace(/\\/g, '/'); - const imageSrc = `/api/recipe-image?path=${encodeURIComponent(normalizedPath)}`; - return (
{recipe.title} -
- - ⏱️ {recipe.totalTime} min +
+ + + Total time: + {recipe.totalTime} min - - 👥 {recipe.servings} servings + + + Servings: + {recipe.servings} servings - - {recipe.difficulty === 'easy' && '⭐'} - {recipe.difficulty === 'medium' && '⭐⭐'} - {recipe.difficulty === 'hard' && '⭐⭐⭐'} + +
-
+
{recipe.tags.slice(0, 3).map((tag) => ( {tag} ))} {recipe.tags.length > 3 && ( - - +{recipe.tags.length - 3} + + +{recipe.tags.length - 3} more )}
diff --git a/components/RecipesClient.tsx b/components/RecipesClient.tsx index c31dd0f..640838e 100644 --- a/components/RecipesClient.tsx +++ b/components/RecipesClient.tsx @@ -1,7 +1,8 @@ 'use client'; import { useState, useMemo } from 'react'; -import RecipesSidebar, { FilterState } from './RecipesSidebar'; +import RecipeLayout from './RecipeLayout'; +import { FilterState } from './RecipesSidebar'; import RecipeCard from './RecipeCard'; import type { Recipe } from '@/lib/recipes'; @@ -17,7 +18,6 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie category: '', selectedTags: [], }); - const [sidebarOpen, setSidebarOpen] = useState(false); const filteredRecipes = useMemo(() => { return recipes.filter((recipe) => { @@ -44,42 +44,35 @@ export default function RecipesClient({ recipes, categories, tags }: RecipesClie }, [recipes, filters]); return ( -
-
- setSidebarOpen(!sidebarOpen)} - /> + +
+ {filteredRecipes.length === recipes.length + ? `Showing all ${recipes.length} recipes` + : `Showing ${filteredRecipes.length} of ${recipes.length} recipes`}
-
-
- {filteredRecipes.length === recipes.length - ? `Showing all ${recipes.length} recipes` - : `Showing ${filteredRecipes.length} of ${recipes.length} recipes`} + {filteredRecipes.length > 0 ? ( +
+ {filteredRecipes.map((recipe) => ( + + ))}
- - {filteredRecipes.length > 0 ? ( -
- {filteredRecipes.map((recipe) => ( - - ))} -
- ) : ( -
-
🔍
-

- No recipes found -

-

- Try adjusting your filters or search terms -

-
- )} -
-
+ ) : ( +
+
🔍
+

+ No recipes found +

+

+ Try adjusting your filters or search terms +

+
+ )} +
); } diff --git a/components/RecipesSidebar.tsx b/components/RecipesSidebar.tsx index c4ff514..5f1af95 100644 --- a/components/RecipesSidebar.tsx +++ b/components/RecipesSidebar.tsx @@ -1,13 +1,13 @@ 'use client'; -import { useState, useMemo } from 'react'; +import { useState, useEffect } from 'react'; +import SelectedTags from './SelectedTags'; +import TagSelector from './TagSelector'; interface RecipesSidebarProps { categories: string[]; tags: string[]; onFilterChange: (filters: FilterState) => void; - isOpen: boolean; - onToggle: () => void; } export interface FilterState { @@ -16,13 +16,12 @@ export interface FilterState { selectedTags: string[]; } -export default function RecipesSidebar({ categories, tags, onFilterChange, isOpen, onToggle }: RecipesSidebarProps) { +export default function RecipesSidebar({ categories, tags, onFilterChange }: RecipesSidebarProps) { const [search, setSearch] = useState(''); const [category, setCategory] = useState(''); const [selectedTags, setSelectedTags] = useState([]); - const [showAllTags, setShowAllTags] = useState(false); - useMemo(() => { + useEffect(() => { onFilterChange({ search, category, selectedTags }); }, [search, category, selectedTags, onFilterChange]); @@ -32,6 +31,14 @@ export default function RecipesSidebar({ categories, tags, onFilterChange, isOpe ); }; + const handleRemoveTag = (tag: string) => { + setSelectedTags((prev) => prev.filter((t) => t !== tag)); + }; + + const handleClearTags = () => { + setSelectedTags([]); + }; + const handleClearFilters = () => { setSearch(''); setCategory(''); @@ -39,18 +46,10 @@ export default function RecipesSidebar({ categories, tags, onFilterChange, isOpe }; const hasActiveFilters = search || category || selectedTags.length > 0; - const displayedTags = showAllTags ? tags : tags.slice(0, 10); return ( - + ); } diff --git a/data/README.md b/data/README.md deleted file mode 100644 index 05aff1a..0000000 --- a/data/README.md +++ /dev/null @@ -1,140 +0,0 @@ -# Data Directory - -This directory contains all content and reference data for the cooking website. - -## Directory Structure - -``` -data/ -├── README.md # This file -├── authors.json # Author profiles and information -├── taxonomy.json # Categories, tags, and classification system -└── recipes/ # Recipe content - ├── README.md - ├── examples/ - ├── appetizers/ - ├── mains/ - ├── desserts/ - ├── sides/ - ├── beverages/ - └── breads/ -``` - -## Reference Files - -### authors.json - -Contains author profiles with biographical information, social links, and specialties. - -**Structure:** -```json -{ - "authors": [ - { - "id": "pws", - "name": "PWS", - "fullName": "Paul Wilson Smith", - "bio": "Short biography", - "email": "email@example.com", - "website": "https://...", - "avatar": "/images/authors/pws.jpg", - "social": { - "github": "username" - }, - "joinDate": "2026-01-01", - "specialties": ["Italian cuisine", "Baking"], - "favoriteIngredient": "Garlic" - } - ] -} -``` - -**Usage in recipes:** -- Reference the author by their `id` in the recipe frontmatter -- Example: `author: "pws"` -- The site will look up full author details from this file - -### taxonomy.json - -Defines the complete taxonomy system for organizing recipes. - -**Contains:** -1. **Categories** - Main recipe classifications (appetizers, mains, desserts, etc.) -2. **Tags** - Organized by type: - - `cuisine` - Italian, Mexican, Asian, etc. - - `protein` - Chicken, beef, vegetarian, etc. - - `dietary` - Gluten-free, vegan, keto, etc. - - `meal-type` - Breakfast, lunch, dinner, etc. - - `occasion` - Weeknight, holiday, party, etc. - - `cooking-method` - Baking, grilling, slow-cooker, etc. - - `speed` - Quick-meals, one-pot, make-ahead, etc. - - `flavor-profile` - Spicy, sweet, savory, etc. - - `special` - Chocolate, pasta, soup, etc. -3. **Difficulty** - Easy, medium, hard with descriptions - -**Usage in recipes:** -- Use tag IDs from this file in recipe frontmatter -- Example: `tags: ["italian", "chicken", "comfort-food"]` -- Example: `category: "mains"` -- Example: `difficulty: "medium"` -- The site can validate tags against this taxonomy - -## Benefits of Reference Files - -### Consistency -- Ensures uniform spelling and naming across all recipes -- Prevents duplicate tags with slight variations -- Maintains standardized author information - -### Validation -- Can validate recipe frontmatter against these schemas -- Catch typos and invalid values during build -- Provide helpful error messages - -### UI Generation -- Generate filter dropdowns from taxonomy -- Display tag descriptions and icons -- Show author bios and links automatically - -### Scalability -- Easy to add new authors, tags, or categories -- Central place to update descriptions and metadata -- Supports multiple authors and contributors - -### SEO & Discovery -- Structured data for search engines -- Consistent taxonomy improves findability -- Tag descriptions enhance metadata - -## Adding New Entries - -### Adding an Author - -1. Open `data/authors.json` -2. Add a new author object to the `authors` array -3. Ensure the `id` is unique and lowercase-hyphenated -4. Add avatar image to `/public/images/authors/` -5. Reference the author ID in recipe frontmatter - -### Adding a Tag - -1. Open `data/taxonomy.json` -2. Find the appropriate tag category (cuisine, protein, etc.) -3. Add a new tag object with `id`, `name`, and `description` -4. Use the tag ID in recipe frontmatter - -### Adding a Category - -1. Open `data/taxonomy.json` -2. Add a new category to the `categories` array -3. Create the corresponding folder in `data/recipes/` -4. Use the category ID in recipe frontmatter - -## Best Practices - -- Always reference tags/categories/authors by their `id` -- Keep IDs lowercase with hyphens (kebab-case) -- Write clear, helpful descriptions -- Add emojis to categories for visual interest -- Update both reference files and recipes together -- Validate changes before committing diff --git a/data/authors.json b/data/authors.json deleted file mode 100644 index e8282ad..0000000 --- a/data/authors.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "authors": [ - { - "id": "pws", - "name": "PWS", - "fullName": "Paul Wilson Smith", - "bio": "Home cook and food enthusiast sharing tested recipes and cooking techniques.", - "email": "pws@example.com", - "website": "https://github.com/runyanjake/cooking", - "avatar": "/images/authors/pws.jpg", - "social": { - "github": "runyanjake" - }, - "joinDate": "2026-01-01", - "specialties": ["Italian cuisine", "Baking", "Comfort food"], - "favoriteIngredient": "Garlic" - } - ] -} diff --git a/data/recipes/README.md b/data/recipes/README.md deleted file mode 100644 index a46cd40..0000000 --- a/data/recipes/README.md +++ /dev/null @@ -1,212 +0,0 @@ -# Recipe Content Structure - -This directory contains all recipe content for the cooking website. - -## Folder Organization - -``` -data/recipes/ -├── appetizers/ -├── mains/ -├── desserts/ -├── sides/ -├── beverages/ -└── breads/ -``` - -Each recipe follows this structure: -``` -category/ -└── YYYY.MM.DD-recipe-slug/ - ├── YYYY.MM.DD-recipe-slug.mdx - └── assets/ - ├── hero.jpg - ├── step1.jpg - └── ... -``` - -## Recipe Format - -### MDX File Structure - -The `.mdx` file contains all recipe metadata and content in one place. - -#### Frontmatter (Required) - -All recipe metadata is stored in YAML frontmatter at the top of the MDX file: - -```yaml ---- -title: "Recipe Title" -slug: "recipe-slug" -date: "YYYY-MM-DD" -lastUpdated: "YYYY-MM-DD" -category: "mains" -tags: ["tag1", "tag2", "tag3"] -dietary: ["vegetarian", "gluten-free"] -cookTime: 45 -prepTime: 20 -totalTime: 65 -difficulty: "easy" -servings: 4 -author: "Author Name" -description: "Short description for SEO and previews" -featured: true -display: true -displayPhoto: "./assets/hero.jpg" ---- -``` - -**Frontmatter Fields:** -- `title` - Display title of the recipe -- `slug` - URL-friendly identifier -- `date` - Publication date (YYYY-MM-DD) -- `lastUpdated` - Last modification date (YYYY-MM-DD) -- `category` - Main category ID (references `../taxonomy.json`) -- `tags` - Array of tag IDs (references `../taxonomy.json`) -- `dietary` - Array of dietary tag IDs (references `../taxonomy.json`) -- `cookTime` - Active cooking time in minutes -- `prepTime` - Preparation time in minutes -- `totalTime` - Total time in minutes -- `difficulty` - Difficulty ID: easy, medium, or hard (references `../taxonomy.json`) -- `servings` - Number of servings -- `author` - Author ID (references `../authors.json`) -- `description` - Brief description for SEO and cards -- `featured` - Boolean for homepage featuring -- `display` - Boolean to control visibility (set to false to hide recipe) -- `displayPhoto` - Relative path to display photo (e.g., "./assets/hero.jpg") - -**Note:** Use IDs from the reference files (`data/authors.json` and `data/taxonomy.json`) to ensure consistency and enable validation. - -#### Content Sections - -The following `## ` (h2) sections are parsed into tabs in the UI: - -1. **## Photos** - Recipe images with captions -2. **## Ingredients** - Lists of ingredients (can use h3 subsections) -3. **## Instructions** - Step-by-step cooking instructions -4. **## Notes** - Tips, variations, storage info (optional) -5. **## References** - Sources, inspirations, credits (optional) - -#### Example MDX Structure - -```mdx ---- -title: "Recipe Name" -slug: "recipe-name" -date: "2026-02-08" -lastUpdated: "2026-02-08" -category: "mains" -tags: ["italian", "chicken"] -dietary: ["gluten-free-option"] -cookTime: 45 -prepTime: 20 -totalTime: 65 -difficulty: "medium" -servings: 4 -author: "PWS" -description: "Short description for SEO and previews" -featured: false -display: true -displayPhoto: "./assets/hero.jpg" ---- - -# Recipe Name - -Introduction paragraph about the recipe. - -## Photos - -![Hero image](./assets/hero.jpg) -*Caption describing the image* - -![Step photo](./assets/step1.jpg) -*Another helpful image* - -## Ingredients - -### For the Main Component -- 2 cups ingredient one -- 1 tablespoon ingredient two -- Salt and pepper to taste - -### For the Sauce -- 1 cup sauce base -- Seasonings - -## Instructions - -### Preparation -1. **Step name**: Detailed instruction with technique. -2. **Another step**: More details here. - -### Cooking -3. **Heat and cook**: Continue with numbered steps. -4. **Finish**: Final steps. - -## Notes - -### Tips for Success -- Helpful tip one -- Helpful tip two - -### Storage -- How to store leftovers -- Freezing instructions - -### Variations -- How to adapt the recipe - -## References - -- Source credits -- Inspiration mentions -- Cookbook references -``` - -## Content Guidelines - -### Writing Style -- Use clear, conversational language -- Include helpful tips and context -- Explain techniques for beginners -- Add timing and temperature details - -### Photography -- Include hero shot (main finished dish) -- Add process shots for complex steps -- Use descriptive alt text for accessibility -- Optimize images (web-friendly sizes) - -### Tags -Choose from these categories: -- **Cuisine**: italian, mexican, asian, american, mediterranean, etc. -- **Protein**: chicken, beef, pork, seafood, vegetarian, vegan -- **Meal Type**: breakfast, lunch, dinner, snack, appetizer -- **Occasion**: weeknight, holiday, party, comfort-food -- **Dietary**: gluten-free, dairy-free, low-carb, keto, paleo -- **Cooking Method**: baking, grilling, slow-cooker, instant-pot -- **Speed**: quick-meals, one-pot, make-ahead, no-cook - -### Difficulty Levels -- **easy**: Beginner-friendly, simple techniques, common ingredients -- **medium**: Some cooking experience needed, multiple steps -- **hard**: Advanced techniques, precise timing, specialty equipment - -## Adding New Recipes - -1. Create folder: `data/recipes/[category]/YYYY.MM.DD-recipe-name/` -2. Create `YYYY.MM.DD-recipe-name.mdx` with frontmatter and content -3. Add images to `assets/` folder -4. Build locally to verify rendering -5. Commit and push - -## Best Practices - -- Use consistent date formatting (YYYY.MM.DD) -- Keep slugs URL-friendly (lowercase, hyphens) -- Optimize images before adding (compress, resize) -- Test recipes before publishing -- Include metric and imperial measurements when possible -- Credit sources and inspirations -- Update featured flag sparingly (limit to 3-5 recipes) diff --git a/data/taxonomy.json b/data/taxonomy.json deleted file mode 100644 index b9242b1..0000000 --- a/data/taxonomy.json +++ /dev/null @@ -1,187 +0,0 @@ -{ - "categories": [ - { - "id": "appetizers", - "name": "Appetizers", - "slug": "appetizers", - "description": "Small dishes to start your meal", - "icon": "🥖" - }, - { - "id": "mains", - "name": "Main Dishes", - "slug": "mains", - "description": "Hearty entrees and main courses", - "icon": "🍽️" - }, - { - "id": "desserts", - "name": "Desserts", - "slug": "desserts", - "description": "Sweet treats and baked goods", - "icon": "🍰" - }, - { - "id": "sides", - "name": "Side Dishes", - "slug": "sides", - "description": "Complementary dishes and accompaniments", - "icon": "🥗" - }, - { - "id": "beverages", - "name": "Beverages", - "slug": "beverages", - "description": "Drinks, smoothies, and cocktails", - "icon": "🥤" - }, - { - "id": "breads", - "name": "Breads & Baking", - "slug": "breads", - "description": "Homemade breads, rolls, and baked goods", - "icon": "🍞" - } - ], - "tags": { - "cuisine": { - "name": "Cuisine", - "description": "Regional and cultural cooking styles", - "tags": [ - { "id": "italian", "name": "Italian", "description": "Italian cuisine and cooking traditions" }, - { "id": "mexican", "name": "Mexican", "description": "Mexican and Latin American dishes" }, - { "id": "asian", "name": "Asian", "description": "Asian cuisine including Chinese, Japanese, Thai, etc." }, - { "id": "american", "name": "American", "description": "Classic American cooking" }, - { "id": "mediterranean", "name": "Mediterranean", "description": "Mediterranean diet and cuisine" }, - { "id": "french", "name": "French", "description": "French culinary techniques and dishes" }, - { "id": "indian", "name": "Indian", "description": "Indian spices and cooking methods" }, - { "id": "middle-eastern", "name": "Middle Eastern", "description": "Middle Eastern flavors and dishes" } - ] - }, - "protein": { - "name": "Protein", - "description": "Main protein sources", - "tags": [ - { "id": "chicken", "name": "Chicken", "description": "Chicken-based recipes" }, - { "id": "beef", "name": "Beef", "description": "Beef and red meat dishes" }, - { "id": "pork", "name": "Pork", "description": "Pork and ham recipes" }, - { "id": "seafood", "name": "Seafood", "description": "Fish and shellfish dishes" }, - { "id": "lamb", "name": "Lamb", "description": "Lamb-based recipes" }, - { "id": "vegetarian", "name": "Vegetarian", "description": "No meat, may include dairy and eggs" }, - { "id": "vegan", "name": "Vegan", "description": "Plant-based, no animal products" } - ] - }, - "dietary": { - "name": "Dietary", - "description": "Special dietary considerations", - "tags": [ - { "id": "gluten-free", "name": "Gluten-Free", "description": "No gluten-containing ingredients" }, - { "id": "gluten-free-option", "name": "Gluten-Free Option", "description": "Can be made gluten-free with substitutions" }, - { "id": "dairy-free", "name": "Dairy-Free", "description": "No dairy products" }, - { "id": "low-carb", "name": "Low-Carb", "description": "Reduced carbohydrate content" }, - { "id": "keto", "name": "Keto", "description": "Ketogenic diet friendly" }, - { "id": "paleo", "name": "Paleo", "description": "Paleo diet compliant" }, - { "id": "whole30", "name": "Whole30", "description": "Whole30 program approved" }, - { "id": "nut-free", "name": "Nut-Free", "description": "No nuts or nut products" } - ] - }, - "meal-type": { - "name": "Meal Type", - "description": "When to serve this dish", - "tags": [ - { "id": "breakfast", "name": "Breakfast", "description": "Morning meals and brunch" }, - { "id": "lunch", "name": "Lunch", "description": "Midday meals" }, - { "id": "dinner", "name": "Dinner", "description": "Evening meals" }, - { "id": "snack", "name": "Snack", "description": "Small bites between meals" }, - { "id": "brunch", "name": "Brunch", "description": "Late morning combination meal" } - ] - }, - "occasion": { - "name": "Occasion", - "description": "Special events and settings", - "tags": [ - { "id": "weeknight", "name": "Weeknight", "description": "Quick meals for busy evenings" }, - { "id": "weekend", "name": "Weekend", "description": "Recipes with more time investment" }, - { "id": "holiday", "name": "Holiday", "description": "Special holiday dishes" }, - { "id": "party", "name": "Party", "description": "Entertaining and gatherings" }, - { "id": "comfort-food", "name": "Comfort Food", "description": "Cozy, satisfying dishes" }, - { "id": "family-friendly", "name": "Family-Friendly", "description": "Kid-approved recipes" }, - { "id": "date-night", "name": "Date Night", "description": "Romantic or impressive dishes" } - ] - }, - "cooking-method": { - "name": "Cooking Method", - "description": "Primary cooking technique", - "tags": [ - { "id": "baking", "name": "Baking", "description": "Oven-baked dishes" }, - { "id": "grilling", "name": "Grilling", "description": "Outdoor or indoor grilling" }, - { "id": "slow-cooker", "name": "Slow Cooker", "description": "Crock-pot and slow cooker meals" }, - { "id": "instant-pot", "name": "Instant Pot", "description": "Pressure cooker recipes" }, - { "id": "stovetop", "name": "Stovetop", "description": "Cooked on the stove" }, - { "id": "no-cook", "name": "No-Cook", "description": "No cooking required" }, - { "id": "air-fryer", "name": "Air Fryer", "description": "Air fryer recipes" }, - { "id": "sous-vide", "name": "Sous Vide", "description": "Precision cooking method" } - ] - }, - "speed": { - "name": "Speed & Convenience", - "description": "Time and effort indicators", - "tags": [ - { "id": "quick-meals", "name": "Quick Meals", "description": "30 minutes or less" }, - { "id": "one-pot", "name": "One-Pot", "description": "Minimal cleanup required" }, - { "id": "make-ahead", "name": "Make-Ahead", "description": "Can be prepared in advance" }, - { "id": "meal-prep", "name": "Meal Prep", "description": "Great for batch cooking" }, - { "id": "leftovers", "name": "Leftovers", "description": "Tastes great reheated" }, - { "id": "freezer-friendly", "name": "Freezer-Friendly", "description": "Can be frozen" } - ] - }, - "flavor-profile": { - "name": "Flavor Profile", - "description": "Dominant tastes and characteristics", - "tags": [ - { "id": "spicy", "name": "Spicy", "description": "Hot and spicy flavors" }, - { "id": "sweet", "name": "Sweet", "description": "Sweet or dessert-like" }, - { "id": "savory", "name": "Savory", "description": "Rich savory flavors" }, - { "id": "tangy", "name": "Tangy", "description": "Acidic or citrus notes" }, - { "id": "rich", "name": "Rich", "description": "Indulgent and decadent" }, - { "id": "light", "name": "Light", "description": "Fresh and light dishes" }, - { "id": "smoky", "name": "Smoky", "description": "Smoky flavors" }, - { "id": "umami", "name": "Umami", "description": "Deep savory umami taste" } - ] - }, - "special": { - "name": "Special Features", - "description": "Notable characteristics", - "tags": [ - { "id": "chocolate", "name": "Chocolate", "description": "Contains chocolate" }, - { "id": "cookies", "name": "Cookies", "description": "Cookie recipes" }, - { "id": "pasta", "name": "Pasta", "description": "Pasta dishes" }, - { "id": "soup", "name": "Soup", "description": "Soups and stews" }, - { "id": "salad", "name": "Salad", "description": "Salads and fresh dishes" }, - { "id": "sandwich", "name": "Sandwich", "description": "Sandwiches and wraps" }, - { "id": "pizza", "name": "Pizza", "description": "Pizza recipes" }, - { "id": "bbq", "name": "BBQ", "description": "Barbecue style" } - ] - } - }, - "difficulty": [ - { - "id": "easy", - "name": "Easy", - "description": "Beginner-friendly, simple techniques, common ingredients", - "icon": "⭐" - }, - { - "id": "medium", - "name": "Medium", - "description": "Some cooking experience needed, multiple steps", - "icon": "⭐⭐" - }, - { - "id": "hard", - "name": "Hard", - "description": "Advanced techniques, precise timing, specialty equipment", - "icon": "⭐⭐⭐" - } - ] -} diff --git a/lib/recipes.ts b/lib/recipes.ts index d3d11b6..2da737a 100644 --- a/lib/recipes.ts +++ b/lib/recipes.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; -const recipesDirectory = path.join(process.cwd(), 'data/recipes'); +const recipesDirectory = path.join(process.cwd(), 'public/recipes'); // Cache to avoid repeated file system walks let recipesCache: Recipe[] | null = null; diff --git a/next.config.ts b/next.config.ts index 363f8bd..95a6154 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,20 +4,6 @@ import createMDX from '@next/mdx'; const nextConfig: NextConfig = { reactStrictMode: true, pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], - images: { - remotePatterns: [ - { - protocol: 'http', - hostname: 'localhost', - pathname: '/api/recipe-image**', - }, - ], - localPatterns: [ - { - pathname: '/api/recipe-image**', - }, - ], - }, }; const withMDX = createMDX({ diff --git a/package-lock.json b/package-lock.json index 339b81f..b6e701b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "gray-matter": "^4.0.3", "next": "^15.1.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@types/node": "^20", @@ -4033,6 +4035,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4844,6 +4856,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4854,6 +4876,34 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", @@ -4878,6 +4928,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", @@ -5103,6 +5254,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -6415,6 +6687,33 @@ "dev": true, "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6564,6 +6863,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", @@ -6611,6 +6928,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", diff --git a/package.json b/package.json index d35a9b2..a09b372 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "gray-matter": "^4.0.3", "next": "^15.1.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@types/node": "^20",