130 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			130 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { Handlers, PageProps } from "$fresh/server.ts";
 | |
| import { IngredientsList } from "@islands/IngredientsList.tsx";
 | |
| import { MainLayout } from "@components/layouts/main.tsx";
 | |
| import Counter from "@islands/Counter.tsx";
 | |
| import { Signal, useSignal } from "@preact/signals";
 | |
| import { Recipe } from "@lib/recipeSchema.ts";
 | |
| import { RedirectSearchHandler } from "@islands/Search.tsx";
 | |
| import { KMenu } from "@islands/KMenu.tsx";
 | |
| import PageHero from "@components/PageHero.tsx";
 | |
| import { Star } from "@components/Stars.tsx";
 | |
| import { renderMarkdown } from "@lib/documents.ts";
 | |
| import { isValidRecipe } from "@lib/recipeSchema.ts";
 | |
| import { MetaTags } from "@components/MetaTags.tsx";
 | |
| import { fetchResource } from "@lib/resources.ts";
 | |
| 
 | |
| export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
 | |
|   async GET(_, ctx) {
 | |
|     try {
 | |
|       const recipe = await fetchResource(`recipes/${ctx.params.name}.md`);
 | |
|       if (!recipe) {
 | |
|         return ctx.renderNotFound();
 | |
|       }
 | |
|       return ctx.render({ recipe, session: ctx.state.session });
 | |
|     } catch (_e) {
 | |
|       return ctx.renderNotFound();
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| function ValidRecipe({
 | |
|   recipe,
 | |
|   amount,
 | |
|   portion,
 | |
| }: { recipe: Recipe; amount: Signal<number>; portion: number }) {
 | |
|   return (
 | |
|     <>
 | |
|       <div class="flex items-center gap-8">
 | |
|         <h3 class="text-3xl my-5">Ingredients</h3>
 | |
|         {portion && <Counter count={amount} />}
 | |
|       </div>
 | |
|       <IngredientsList
 | |
|         ingredients={recipe.content.recipeIngredient}
 | |
|         amount={amount}
 | |
|         portion={portion}
 | |
|       />
 | |
|       <h3 class="text-3xl my-5">Preparation</h3>
 | |
|       <div class="pl-2">
 | |
|         <ol class="list-decimal grid gap-4">
 | |
|           {recipe.content.recipeInstructions &&
 | |
|             (recipe.content.recipeInstructions.filter((inst) => !!inst?.length)
 | |
|               .map((instruction) => {
 | |
|                 return (
 | |
|                   <li
 | |
|                     dangerouslySetInnerHTML={{
 | |
|                       __html: renderMarkdown(instruction),
 | |
|                     }}
 | |
|                   />
 | |
|                 );
 | |
|               }))}
 | |
|         </ol>
 | |
|       </div>
 | |
|     </>
 | |
|   );
 | |
| }
 | |
| 
 | |
| export default function Page(
 | |
|   props: PageProps<{ recipe: Recipe; session: Record<string, string> }>,
 | |
| ) {
 | |
|   const { recipe, session } = props.data;
 | |
| 
 | |
|   const portion = recipe.recipeYield;
 | |
|   const amount = useSignal(portion || 1);
 | |
| 
 | |
|   const subline = [
 | |
|     recipe?.content?.prepTime && `Duration ${recipe?.content?.prepTime}`,
 | |
|   ].filter(Boolean) as string[];
 | |
| 
 | |
|   return (
 | |
|     <MainLayout
 | |
|       url={props.url}
 | |
|       title={`Recipes > ${recipe.content?.name}`}
 | |
|       context={recipe}
 | |
|     >
 | |
|       <RedirectSearchHandler />
 | |
|       <KMenu type="main" context={recipe} />
 | |
|       <MetaTags resource={recipe} />
 | |
| 
 | |
|       <PageHero
 | |
|         image={recipe.content?.image}
 | |
|         thumbnail={recipe.content?.thumbnail}
 | |
|       >
 | |
|         <PageHero.Header>
 | |
|           <PageHero.BackLink href="/recipes" />
 | |
|           {session && (
 | |
|             <PageHero.EditLink
 | |
|               href={`https://notes.max-richter.dev/resources/recipes/${recipe.name}`}
 | |
|             />
 | |
|           )}
 | |
|         </PageHero.Header>
 | |
|         <PageHero.Footer>
 | |
|           <PageHero.Title link={recipe.content?.link}>
 | |
|             {recipe.content.name}
 | |
|           </PageHero.Title>
 | |
|           <PageHero.Subline
 | |
|             entries={subline}
 | |
|           >
 | |
|             {recipe.meta?.rating && <Star rating={recipe.meta?.rating} />}
 | |
|           </PageHero.Subline>
 | |
|         </PageHero.Footer>
 | |
|       </PageHero>
 | |
| 
 | |
|       <div class="px-8 text-white mt-10">
 | |
|         {isValidRecipe(recipe)
 | |
|           ? (
 | |
|             <ValidRecipe
 | |
|               recipe={recipe}
 | |
|               amount={amount}
 | |
|               portion={portion || 1}
 | |
|             />
 | |
|           )
 | |
|           : (
 | |
|             <div class="whitespace-break-spaces markdown-body">
 | |
|               {JSON.stringify(recipe)}
 | |
|             </div>
 | |
|           )}
 | |
|       </div>
 | |
|     </MainLayout>
 | |
|   );
 | |
| }
 |