Initial commit: json-render crepes demo
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

This commit is contained in:
2026-02-09 07:30:54 +01:00
commit 9b750238c2
35 changed files with 9237 additions and 0 deletions

68
lib/catalog.ts Normal file
View File

@@ -0,0 +1,68 @@
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react";
import { z } from "zod";
export const catalog = defineCatalog(schema, {
components: {
RecipeHeader: {
props: z.object({
title: z.string(),
chef: z.string(),
origin: z.string(),
prepTime: z.string(),
servings: z.number(),
}),
description: "Header with recipe title, chef name, origin, and key info",
},
ChefInfo: {
props: z.object({
name: z.string(),
bio: z.string(),
sourceUrl: z.string(),
imageUrl: z.string().optional(),
}),
description: "Chef information card with bio and source link",
},
IngredientList: {
props: z.object({
title: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
quantity: z.string(),
})
),
}),
description: "List of ingredients with quantities",
},
InstructionSteps: {
props: z.object({
steps: z.array(
z.object({
number: z.number(),
instruction: z.string(),
})
),
}),
description: "Step-by-step cooking instructions",
},
TipCard: {
props: z.object({
tip: z.string(),
author: z.string(),
}),
description: "Chef's tip or pro advice",
},
RecipeCard: {
props: z.object({
title: z.string(),
description: z.string(),
imageUrl: z.string().optional(),
}),
description: "Card container for a recipe",
},
},
actions: {},
});
export type Catalog = typeof catalog;

172
lib/components.tsx Normal file
View File

@@ -0,0 +1,172 @@
import React from "react";
export interface RecipeHeaderProps {
title: string;
chef: string;
origin: string;
prepTime: string;
servings: number;
}
export function RecipeHeader({ title, chef, origin, prepTime, servings }: RecipeHeaderProps) {
return (
<div className="bg-gradient-to-r from-amber-50 to-orange-50 p-8 rounded-2xl shadow-lg mb-8 border-2 border-amber-200">
<h2 className="text-4xl font-bold text-gray-900 mb-3">{title}</h2>
<div className="flex flex-wrap gap-4 text-lg">
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
👨🍳 <strong>Chef:</strong> {chef}
</span>
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
🇫🇷 <strong>Origin:</strong> {origin}
</span>
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
<strong>Prep:</strong> {prepTime}
</span>
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
🍽 <strong>Serves:</strong> {servings}
</span>
</div>
</div>
);
}
export interface ChefInfoProps {
name: string;
bio: string;
sourceUrl: string;
imageUrl?: string;
}
export function ChefInfo({ name, bio, sourceUrl, imageUrl }: ChefInfoProps) {
return (
<div className="bg-blue-50 p-6 rounded-xl shadow-md mb-8 border-l-4 border-blue-500">
<div className="flex items-start gap-4">
{imageUrl && (
<img
src={imageUrl}
alt={name}
className="w-24 h-24 rounded-full object-cover shadow-lg"
/>
)}
<div className="flex-1">
<h3 className="text-2xl font-bold text-gray-900 mb-2">{name}</h3>
<p className="text-gray-700 mb-3">{bio}</p>
<a
href={sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 font-semibold underline"
>
📖 View Original Recipe
</a>
</div>
</div>
</div>
);
}
export interface IngredientListProps {
title: string;
ingredients: Array<{ quantity: string; name: string }>;
}
export function IngredientList({ title, ingredients }: IngredientListProps) {
return (
<div className="bg-green-50 p-6 rounded-xl shadow-md mb-6 border-l-4 border-green-500">
<h4 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
🥚 {title}
</h4>
<ul className="space-y-2">
{ingredients.map((ingredient, index) => (
<li key={index} className="flex items-baseline gap-3 text-lg">
<span className="text-green-600 font-bold"></span>
<span className="font-semibold text-gray-900 min-w-[120px]">
{ingredient.quantity}
</span>
<span className="text-gray-700">{ingredient.name}</span>
</li>
))}
</ul>
</div>
);
}
export interface InstructionStepsProps {
steps: Array<{ number: number; instruction: string }>;
}
export function InstructionSteps({ steps }: InstructionStepsProps) {
return (
<div className="bg-purple-50 p-6 rounded-xl shadow-md mb-6 border-l-4 border-purple-500">
<h4 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
📝 Instructions
</h4>
<ol className="space-y-4">
{steps.map((step) => (
<li key={step.number} className="flex gap-4">
<span className="flex-shrink-0 w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold">
{step.number}
</span>
<p className="text-gray-700 text-lg pt-1">{step.instruction}</p>
</li>
))}
</ol>
</div>
);
}
export interface TipCardProps {
tip: string;
author: string;
}
export function TipCard({ tip, author }: TipCardProps) {
return (
<div className="bg-yellow-50 p-6 rounded-xl shadow-md mb-6 border-l-4 border-yellow-500">
<div className="flex items-start gap-3">
<span className="text-3xl">💡</span>
<div>
<p className="text-gray-800 text-lg mb-2 italic">&ldquo;{tip}&rdquo;</p>
<p className="text-gray-600 font-semibold"> {author}</p>
</div>
</div>
</div>
);
}
export interface Recipe {
title: string;
description: string;
imageUrl?: string;
chef: ChefInfoProps;
header: RecipeHeaderProps;
ingredientLists: IngredientListProps[];
steps: InstructionStepsProps;
tip: TipCardProps;
}
export function RecipeCard({ recipe }: { recipe: Recipe }) {
return (
<div className="bg-white p-8 rounded-2xl shadow-xl mb-8 border border-gray-200">
{recipe.imageUrl && (
<img
src={recipe.imageUrl}
alt={recipe.title}
className="w-full h-64 object-cover rounded-xl mb-6 shadow-md"
/>
)}
<h2 className="text-3xl font-bold text-gray-900 mb-3">{recipe.title}</h2>
<p className="text-gray-600 text-lg mb-6">{recipe.description}</p>
<ChefInfo {...recipe.chef} />
<RecipeHeader {...recipe.header} />
{recipe.ingredientLists.map((list, index) => (
<IngredientList key={index} {...list} />
))}
<InstructionSteps {...recipe.steps} />
<TipCard {...recipe.tip} />
</div>
);
}

274
lib/recipes-simple.ts Normal file
View File

@@ -0,0 +1,274 @@
import { Recipe } from "./components";
export const recipes: Recipe[] = [
{
title: "Crêpes Suzette",
description: "The iconic flambéed orange crêpe dessert, a timeless French classic",
imageUrl: "https://images.unsplash.com/photo-1576284805570-ca5c04e3aa2c?w=800",
chef: {
name: "Auguste Escoffier",
bio: "The legendary French chef who codified French cuisine and created many classic dishes. His influence on modern French cooking is immeasurable.",
sourceUrl: "https://www.escoffier.edu/blog/culinary-arts/the-legendary-auguste-escoffier/",
},
header: {
title: "Crêpes Suzette",
chef: "Auguste Escoffier",
origin: "Monte Carlo, 1895",
prepTime: "45 minutes",
servings: 4,
},
ingredientLists: [
{
title: "For the Crêpes",
ingredients: [
{ quantity: "200g", name: "All-purpose flour" },
{ quantity: "4", name: "Large eggs" },
{ quantity: "500ml", name: "Whole milk" },
{ quantity: "50g", name: "Melted butter" },
{ quantity: "1 pinch", name: "Salt" },
{ quantity: "2 tbsp", name: "Sugar" },
],
},
{
title: "For the Suzette Sauce",
ingredients: [
{ quantity: "100g", name: "Unsalted butter" },
{ quantity: "100g", name: "Sugar" },
{ quantity: "Zest of 2", name: "Oranges" },
{ quantity: "150ml", name: "Fresh orange juice" },
{ quantity: "50ml", name: "Grand Marnier" },
{ quantity: "50ml", name: "Cognac" },
],
},
],
steps: {
steps: [
{
number: 1,
instruction: "Whisk together flour, eggs, milk, melted butter, salt, and sugar until smooth. Let the batter rest for 30 minutes.",
},
{
number: 2,
instruction: "Heat a non-stick pan over medium heat. Pour a thin layer of batter and swirl to coat. Cook for 1-2 minutes per side until golden.",
},
{
number: 3,
instruction: "For the sauce: In a large pan, melt butter with sugar until caramelized. Add orange zest and juice, simmer for 2 minutes.",
},
{
number: 4,
instruction: "Fold each crêpe into quarters and arrange in the orange sauce. Heat through gently.",
},
{
number: 5,
instruction: "Add Grand Marnier and Cognac, ignite carefully to flambé. Serve immediately with the sauce.",
},
],
},
tip: {
tip: "The flambé should be done tableside for maximum theatrical effect. Ensure your pan is hot before adding the alcohol for proper ignition.",
author: "Auguste Escoffier",
},
},
{
title: "Crêpes Bretonnes au Sarrasin",
description: "Traditional Breton buckwheat crêpes with ham, egg, and Gruyère",
imageUrl: "https://images.unsplash.com/photo-1505253468034-514d2507d914?w=800",
chef: {
name: "Olivier Roellinger",
bio: "Three-Michelin-starred chef from Brittany, known for his innovative use of spices and celebration of Breton culinary traditions.",
sourceUrl: "https://www.maisons-de-bricourt.com/en/",
},
header: {
title: "Crêpes Bretonnes au Sarrasin",
chef: "Olivier Roellinger",
origin: "Brittany, France",
prepTime: "30 minutes",
servings: 6,
},
ingredientLists: [
{
title: "Ingredients",
ingredients: [
{ quantity: "250g", name: "Buckwheat flour" },
{ quantity: "1", name: "Large egg" },
{ quantity: "500ml", name: "Water" },
{ quantity: "1 tsp", name: "Salt" },
{ quantity: "30g", name: "Melted butter" },
{ quantity: "6 slices", name: "Jambon de Paris (quality ham)" },
{ quantity: "6", name: "Fresh eggs" },
{ quantity: "200g", name: "Grated Gruyère cheese" },
],
},
],
steps: {
steps: [
{
number: 1,
instruction: "Mix buckwheat flour with salt. Gradually whisk in water and egg until smooth. Add melted butter. Let rest 1 hour.",
},
{
number: 2,
instruction: "Heat a large crêpe pan or flat griddle. Lightly oil the surface.",
},
{
number: 3,
instruction: "Pour batter to form a thin crêpe. Let it set for 30 seconds.",
},
{
number: 4,
instruction: "Place a slice of ham in the center, crack an egg on top, and sprinkle with Gruyère. Season with pepper.",
},
{
number: 5,
instruction: "Fold the edges inward to form a square, leaving the egg yolk visible. Cook until egg white is set and yolk is still runny.",
},
{
number: 6,
instruction: "Serve immediately with a glass of Breton cider.",
},
],
},
tip: {
tip: "The buckwheat flour gives these galettes their distinctive nutty flavor. Use genuine buckwheat from Brittany if possible, and never skip the resting time for the batter.",
author: "Olivier Roellinger",
},
},
{
title: "Crêpes with Salted Caramel",
description: "Modern French crêpes with homemade salted butter caramel sauce",
imageUrl: "https://images.unsplash.com/photo-1519676867240-f03562e64548?w=800",
chef: {
name: "Pierre Hermé",
bio: "Renowned pastry chef dubbed the 'Picasso of Pastry' by Vogue. Master of combining unexpected flavors and textures in French desserts.",
sourceUrl: "https://www.pierreherme.com/",
},
header: {
title: "Crêpes au Caramel Beurre Salé",
chef: "Pierre Hermé",
origin: "Paris, France",
prepTime: "40 minutes",
servings: 8,
},
ingredientLists: [
{
title: "For the Crêpes",
ingredients: [
{ quantity: "300g", name: "All-purpose flour" },
{ quantity: "3", name: "Large eggs" },
{ quantity: "600ml", name: "Whole milk" },
{ quantity: "40g", name: "Melted butter" },
{ quantity: "30g", name: "Sugar" },
{ quantity: "1 pinch", name: "Fleur de sel" },
],
},
{
title: "For the Salted Caramel",
ingredients: [
{ quantity: "200g", name: "Sugar" },
{ quantity: "100g", name: "Salted butter (Breton preferred)" },
{ quantity: "200ml", name: "Heavy cream" },
{ quantity: "1 tsp", name: "Fleur de sel" },
],
},
],
steps: {
steps: [
{
number: 1,
instruction: "Prepare crêpe batter: Whisk flour, eggs, milk, melted butter, sugar, and fleur de sel until smooth. Rest for 30 minutes.",
},
{
number: 2,
instruction: "For the caramel: In a heavy saucepan, melt sugar over medium heat without stirring until it becomes deep amber.",
},
{
number: 3,
instruction: "Remove from heat and carefully whisk in butter, then cream (it will bubble vigorously). Return to low heat and stir until smooth.",
},
{
number: 4,
instruction: "Add fleur de sel to taste. Let caramel cool slightly.",
},
{
number: 5,
instruction: "Cook crêpes in a hot pan until lightly golden on both sides.",
},
{
number: 6,
instruction: "Fold crêpes into quarters, drizzle generously with warm salted caramel. Garnish with extra fleur de sel.",
},
],
},
tip: {
tip: "The key to perfect caramel is patience. Don't rush the caramelization, and use quality salted butter from Brittany. The contrast between sweet and salty creates an unforgettable experience.",
author: "Pierre Hermé",
},
},
{
title: "Crêpes Parmentier",
description: "Savory potato and bacon crêpes, rustic and comforting",
imageUrl: "https://images.unsplash.com/photo-1525184782196-8e2ded604bf7?w=800",
chef: {
name: "Joël Robuchon",
bio: "Holder of the most Michelin stars in the world (31 total). Known for elevating simple ingredients into extraordinary dishes through perfect technique.",
sourceUrl: "https://www.joel-robuchon.com/",
},
header: {
title: "Crêpes Parmentier",
chef: "Joël Robuchon",
origin: "France (modern bistro)",
prepTime: "50 minutes",
servings: 4,
},
ingredientLists: [
{
title: "Ingredients",
ingredients: [
{ quantity: "200g", name: "All-purpose flour" },
{ quantity: "3", name: "Large eggs" },
{ quantity: "400ml", name: "Whole milk" },
{ quantity: "50g", name: "Melted butter" },
{ quantity: "400g", name: "Yukon Gold potatoes" },
{ quantity: "200g", name: "Quality bacon lardons" },
{ quantity: "100g", name: "Comté cheese, grated" },
{ quantity: "2", name: "Shallots, finely chopped" },
{ quantity: "100ml", name: "Crème fraîche" },
{ quantity: "To taste", name: "Salt, pepper, nutmeg" },
],
},
],
steps: {
steps: [
{
number: 1,
instruction: "Make crêpe batter with flour, eggs, milk, and melted butter. Season lightly and rest for 30 minutes.",
},
{
number: 2,
instruction: "Peel and dice potatoes into small cubes. Boil in salted water until just tender, about 8 minutes. Drain thoroughly.",
},
{
number: 3,
instruction: "In a large pan, cook bacon lardons until crispy. Add shallots and cook until soft.",
},
{
number: 4,
instruction: "Add potatoes to the pan, season with salt, pepper, and a grating of nutmeg. Stir in crème fraîche.",
},
{
number: 5,
instruction: "Cook crêpes until golden. Fill each with the potato-bacon mixture and fold or roll.",
},
{
number: 6,
instruction: "Place filled crêpes in a buttered baking dish, top with Comté cheese. Bake at 200°C for 10 minutes until bubbly and golden.",
},
],
},
tip: {
tip: "The potatoes should be cooked al dente, not mushy. This is the Robuchon way—respect the texture of every ingredient. A quality Comté adds depth that's essential to the dish.",
author: "Joël Robuchon",
},
},
];

392
lib/recipes.ts Normal file
View File

@@ -0,0 +1,392 @@
export const recipes = [
{
root: "root",
elements: {
root: {
type: "RecipeCard" as const,
props: {
title: "Crêpes Suzette",
description: "The iconic flambéed orange crêpe dessert, a timeless French classic",
imageUrl: "https://images.unsplash.com/photo-1576284805570-ca5c04e3aa2c?w=800",
},
children: [
"chef",
"header",
"ingredients1",
"ingredients2",
"steps",
"tip",
],
},
chef: {
type: "ChefInfo" as const,
props: {
name: "Auguste Escoffier",
bio: "The legendary French chef who codified French cuisine and created many classic dishes. His influence on modern French cooking is immeasurable.",
sourceUrl: "https://www.escoffier.edu/blog/culinary-arts/the-legendary-auguste-escoffier/",
},
},
header: {
type: "RecipeHeader" as const,
props: {
title: "Crêpes Suzette",
chef: "Auguste Escoffier",
origin: "Monte Carlo, 1895",
prepTime: "45 minutes",
servings: 4,
},
},
ingredients1: {
type: "IngredientList" as const,
props: {
title: "For the Crêpes",
ingredients: [
{ quantity: "200g", name: "All-purpose flour" },
{ quantity: "4", name: "Large eggs" },
{ quantity: "500ml", name: "Whole milk" },
{ quantity: "50g", name: "Melted butter" },
{ quantity: "1 pinch", name: "Salt" },
{ quantity: "2 tbsp", name: "Sugar" },
],
},
},
ingredients2: {
type: "IngredientList" as const,
props: {
title: "For the Suzette Sauce",
ingredients: [
{ quantity: "100g", name: "Unsalted butter" },
{ quantity: "100g", name: "Sugar" },
{ quantity: "Zest of 2", name: "Oranges" },
{ quantity: "150ml", name: "Fresh orange juice" },
{ quantity: "50ml", name: "Grand Marnier" },
{ quantity: "50ml", name: "Cognac" },
],
},
},
steps: {
type: "InstructionSteps" as const,
props: {
steps: [
{
number: 1,
instruction: "Whisk together flour, eggs, milk, melted butter, salt, and sugar until smooth. Let the batter rest for 30 minutes.",
},
{
number: 2,
instruction: "Heat a non-stick pan over medium heat. Pour a thin layer of batter and swirl to coat. Cook for 1-2 minutes per side until golden.",
},
{
number: 3,
instruction: "For the sauce: In a large pan, melt butter with sugar until caramelized. Add orange zest and juice, simmer for 2 minutes.",
},
{
number: 4,
instruction: "Fold each crêpe into quarters and arrange in the orange sauce. Heat through gently.",
},
{
number: 5,
instruction: "Add Grand Marnier and Cognac, ignite carefully to flambé. Serve immediately with the sauce.",
},
],
},
},
tip: {
type: "TipCard" as const,
props: {
tip: "The flambé should be done tableside for maximum theatrical effect. Ensure your pan is hot before adding the alcohol for proper ignition.",
author: "Auguste Escoffier",
},
},
},
},
{
root: "root",
elements: {
root: {
type: "RecipeCard" as const,
props: {
title: "Crêpes Bretonnes au Sarrasin",
description: "Traditional Breton buckwheat crêpes with ham, egg, and Gruyère",
imageUrl: "https://images.unsplash.com/photo-1505253468034-514d2507d914?w=800",
},
children: [
"chef",
"header",
"ingredients",
"steps",
"tip",
],
},
chef: {
type: "ChefInfo" as const,
props: {
name: "Olivier Roellinger",
bio: "Three-Michelin-starred chef from Brittany, known for his innovative use of spices and celebration of Breton culinary traditions.",
sourceUrl: "https://www.maisons-de-bricourt.com/en/",
},
},
header: {
type: "RecipeHeader" as const,
props: {
title: "Crêpes Bretonnes au Sarrasin",
chef: "Olivier Roellinger",
origin: "Brittany, France",
prepTime: "30 minutes",
servings: 6,
},
},
ingredients: {
type: "IngredientList" as const,
props: {
title: "Ingredients",
ingredients: [
{ quantity: "250g", name: "Buckwheat flour" },
{ quantity: "1", name: "Large egg" },
{ quantity: "500ml", name: "Water" },
{ quantity: "1 tsp", name: "Salt" },
{ quantity: "30g", name: "Melted butter" },
{ quantity: "6 slices", name: "Jambon de Paris (quality ham)" },
{ quantity: "6", name: "Fresh eggs" },
{ quantity: "200g", name: "Grated Gruyère cheese" },
],
},
},
steps: {
type: "InstructionSteps" as const,
props: {
steps: [
{
number: 1,
instruction: "Mix buckwheat flour with salt. Gradually whisk in water and egg until smooth. Add melted butter. Let rest 1 hour.",
},
{
number: 2,
instruction: "Heat a large crêpe pan or flat griddle. Lightly oil the surface.",
},
{
number: 3,
instruction: "Pour batter to form a thin crêpe. Let it set for 30 seconds.",
},
{
number: 4,
instruction: "Place a slice of ham in the center, crack an egg on top, and sprinkle with Gruyère. Season with pepper.",
},
{
number: 5,
instruction: "Fold the edges inward to form a square, leaving the egg yolk visible. Cook until egg white is set and yolk is still runny.",
},
{
number: 6,
instruction: "Serve immediately with a glass of Breton cider.",
},
],
},
},
tip: {
type: "TipCard" as const,
props: {
tip: "The buckwheat flour gives these galettes their distinctive nutty flavor. Use genuine buckwheat from Brittany if possible, and never skip the resting time for the batter.",
author: "Olivier Roellinger",
},
},
},
},
{
root: "root",
elements: {
root: {
type: "RecipeCard" as const,
props: {
title: "Crêpes with Salted Caramel",
description: "Modern French crêpes with homemade salted butter caramel sauce",
imageUrl: "https://images.unsplash.com/photo-1519676867240-f03562e64548?w=800",
},
children: [
"chef",
"header",
"ingredients1",
"ingredients2",
"steps",
"tip",
],
},
chef: {
type: "ChefInfo" as const,
props: {
name: "Pierre Hermé",
bio: "Renowned pastry chef dubbed the 'Picasso of Pastry' by Vogue. Master of combining unexpected flavors and textures in French desserts.",
sourceUrl: "https://www.pierreherme.com/",
},
},
header: {
type: "RecipeHeader" as const,
props: {
title: "Crêpes au Caramel Beurre Salé",
chef: "Pierre Hermé",
origin: "Paris, France",
prepTime: "40 minutes",
servings: 8,
},
},
ingredients1: {
type: "IngredientList" as const,
props: {
title: "For the Crêpes",
ingredients: [
{ quantity: "300g", name: "All-purpose flour" },
{ quantity: "3", name: "Large eggs" },
{ quantity: "600ml", name: "Whole milk" },
{ quantity: "40g", name: "Melted butter" },
{ quantity: "30g", name: "Sugar" },
{ quantity: "1 pinch", name: "Fleur de sel" },
],
},
},
ingredients2: {
type: "IngredientList" as const,
props: {
title: "For the Salted Caramel",
ingredients: [
{ quantity: "200g", name: "Sugar" },
{ quantity: "100g", name: "Salted butter (Breton preferred)" },
{ quantity: "200ml", name: "Heavy cream" },
{ quantity: "1 tsp", name: "Fleur de sel" },
],
},
},
steps: {
type: "InstructionSteps" as const,
props: {
steps: [
{
number: 1,
instruction: "Prepare crêpe batter: Whisk flour, eggs, milk, melted butter, sugar, and fleur de sel until smooth. Rest for 30 minutes.",
},
{
number: 2,
instruction: "For the caramel: In a heavy saucepan, melt sugar over medium heat without stirring until it becomes deep amber.",
},
{
number: 3,
instruction: "Remove from heat and carefully whisk in butter, then cream (it will bubble vigorously). Return to low heat and stir until smooth.",
},
{
number: 4,
instruction: "Add fleur de sel to taste. Let caramel cool slightly.",
},
{
number: 5,
instruction: "Cook crêpes in a hot pan until lightly golden on both sides.",
},
{
number: 6,
instruction: "Fold crêpes into quarters, drizzle generously with warm salted caramel. Garnish with extra fleur de sel.",
},
],
},
},
tip: {
type: "TipCard" as const,
props: {
tip: "The key to perfect caramel is patience. Don't rush the caramelization, and use quality salted butter from Brittany. The contrast between sweet and salty creates an unforgettable experience.",
author: "Pierre Hermé",
},
},
},
},
{
root: "root",
elements: {
root: {
type: "RecipeCard" as const,
props: {
title: "Crêpes Parmentier",
description: "Savory potato and bacon crêpes, rustic and comforting",
imageUrl: "https://images.unsplash.com/photo-1525184782196-8e2ded604bf7?w=800",
},
children: [
"chef",
"header",
"ingredients",
"steps",
"tip",
],
},
chef: {
type: "ChefInfo" as const,
props: {
name: "Joël Robuchon",
bio: "Holder of the most Michelin stars in the world (31 total). Known for elevating simple ingredients into extraordinary dishes through perfect technique.",
sourceUrl: "https://www.joel-robuchon.com/",
},
},
header: {
type: "RecipeHeader" as const,
props: {
title: "Crêpes Parmentier",
chef: "Joël Robuchon",
origin: "France (modern bistro)",
prepTime: "50 minutes",
servings: 4,
},
},
ingredients: {
type: "IngredientList" as const,
props: {
title: "Ingredients",
ingredients: [
{ quantity: "200g", name: "All-purpose flour" },
{ quantity: "3", name: "Large eggs" },
{ quantity: "400ml", name: "Whole milk" },
{ quantity: "50g", name: "Melted butter" },
{ quantity: "400g", name: "Yukon Gold potatoes" },
{ quantity: "200g", name: "Quality bacon lardons" },
{ quantity: "100g", name: "Comté cheese, grated" },
{ quantity: "2", name: "Shallots, finely chopped" },
{ quantity: "100ml", name: "Crème fraîche" },
{ quantity: "To taste", name: "Salt, pepper, nutmeg" },
],
},
},
steps: {
type: "InstructionSteps" as const,
props: {
steps: [
{
number: 1,
instruction: "Make crêpe batter with flour, eggs, milk, and melted butter. Season lightly and rest for 30 minutes.",
},
{
number: 2,
instruction: "Peel and dice potatoes into small cubes. Boil in salted water until just tender, about 8 minutes. Drain thoroughly.",
},
{
number: 3,
instruction: "In a large pan, cook bacon lardons until crispy. Add shallots and cook until soft.",
},
{
number: 4,
instruction: "Add potatoes to the pan, season with salt, pepper, and a grating of nutmeg. Stir in crème fraîche.",
},
{
number: 5,
instruction: "Cook crêpes until golden. Fill each with the potato-bacon mixture and fold or roll.",
},
{
number: 6,
instruction: "Place filled crêpes in a buttered baking dish, top with Comté cheese. Bake at 200°C for 10 minutes until bubbly and golden.",
},
],
},
},
tip: {
type: "TipCard" as const,
props: {
tip: "The potatoes should be cooked al dente, not mushy. This is the Robuchon way—respect the texture of every ingredient. A quality Comté adds depth that's essential to the dish.",
author: "Joël Robuchon",
},
},
},
},
];

127
lib/registry.tsx Normal file
View File

@@ -0,0 +1,127 @@
"use client";
import { defineRegistry, DataProvider, VisibilityProvider, ActionProvider } from "@json-render/react";
import { catalog } from "./catalog";
import { ReactNode } from "react";
export const { registry } = defineRegistry(catalog, {
components: {
RecipeHeader: ({ props }) => (
<div className="bg-gradient-to-r from-amber-50 to-orange-50 p-8 rounded-2xl shadow-lg mb-8 border-2 border-amber-200">
<h1 className="text-4xl font-bold text-gray-900 mb-3">{props.title}</h1>
<div className="flex flex-wrap gap-4 text-lg">
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
👨🍳 <strong>Chef:</strong> {props.chef}
</span>
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
🇫🇷 <strong>Origin:</strong> {props.origin}
</span>
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
<strong>Prep:</strong> {props.prepTime}
</span>
<span className="bg-white px-4 py-2 rounded-full shadow-sm">
🍽 <strong>Serves:</strong> {props.servings}
</span>
</div>
</div>
),
ChefInfo: ({ props }) => (
<div className="bg-blue-50 p-6 rounded-xl shadow-md mb-8 border-l-4 border-blue-500">
<div className="flex items-start gap-4">
{props.imageUrl && (
<img
src={props.imageUrl}
alt={props.name}
className="w-24 h-24 rounded-full object-cover shadow-lg"
/>
)}
<div className="flex-1">
<h2 className="text-2xl font-bold text-gray-900 mb-2">{props.name}</h2>
<p className="text-gray-700 mb-3">{props.bio}</p>
<a
href={props.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 font-semibold underline"
>
📖 View Original Recipe
</a>
</div>
</div>
</div>
),
IngredientList: ({ props }) => (
<div className="bg-green-50 p-6 rounded-xl shadow-md mb-6 border-l-4 border-green-500">
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
🥚 {props.title}
</h3>
<ul className="space-y-2">
{props.ingredients.map((ingredient: any, index: number) => (
<li key={index} className="flex items-baseline gap-3 text-lg">
<span className="text-green-600 font-bold"></span>
<span className="font-semibold text-gray-900 min-w-[120px]">
{ingredient.quantity}
</span>
<span className="text-gray-700">{ingredient.name}</span>
</li>
))}
</ul>
</div>
),
InstructionSteps: ({ props }) => (
<div className="bg-purple-50 p-6 rounded-xl shadow-md mb-6 border-l-4 border-purple-500">
<h3 className="text-2xl font-bold text-gray-900 mb-4 flex items-center gap-2">
📝 Instructions
</h3>
<ol className="space-y-4">
{props.steps.map((step: any) => (
<li key={step.number} className="flex gap-4">
<span className="flex-shrink-0 w-8 h-8 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold">
{step.number}
</span>
<p className="text-gray-700 text-lg pt-1">{step.instruction}</p>
</li>
))}
</ol>
</div>
),
TipCard: ({ props }) => (
<div className="bg-yellow-50 p-6 rounded-xl shadow-md mb-6 border-l-4 border-yellow-500">
<div className="flex items-start gap-3">
<span className="text-3xl">💡</span>
<div>
<p className="text-gray-800 text-lg mb-2 italic">&ldquo;{props.tip}&rdquo;</p>
<p className="text-gray-600 font-semibold"> {props.author}</p>
</div>
</div>
</div>
),
RecipeCard: ({ props, children }) => (
<div className="bg-white p-8 rounded-2xl shadow-xl mb-8 border border-gray-200">
{props.imageUrl && (
<img
src={props.imageUrl}
alt={props.title}
className="w-full h-64 object-cover rounded-xl mb-6 shadow-md"
/>
)}
<h2 className="text-3xl font-bold text-gray-900 mb-3">{props.title}</h2>
<p className="text-gray-600 text-lg mb-6">{props.description}</p>
{children}
</div>
),
},
});
// Provider wrapper for all required contexts
export function Provider({ children }: { children: ReactNode }) {
return (
<ActionProvider>
<DataProvider>
<VisibilityProvider>
{children}
</VisibilityProvider>
</DataProvider>
</ActionProvider>
);
}