Initial commit: json-render crepes demo
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
This commit is contained in:
68
lib/catalog.ts
Normal file
68
lib/catalog.ts
Normal 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
172
lib/components.tsx
Normal 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">“{tip}”</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
274
lib/recipes-simple.ts
Normal 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
392
lib/recipes.ts
Normal 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
127
lib/registry.tsx
Normal 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">“{props.tip}”</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user