feat: init
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | # dotenv environment variable files | ||||||
|  | .env | ||||||
|  | .env.development.local | ||||||
|  | .env.test.local | ||||||
|  | .env.production.local | ||||||
|  | .env.local | ||||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | # Fresh project | ||||||
|  |  | ||||||
|  | Your new Fresh project is ready to go. You can follow the Fresh "Getting | ||||||
|  | Started" guide here: https://fresh.deno.dev/docs/getting-started | ||||||
|  |  | ||||||
|  | ### Usage | ||||||
|  |  | ||||||
|  | Make sure to install Deno: https://deno.land/manual/getting_started/installation | ||||||
|  |  | ||||||
|  | Then start the project: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | deno task start | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This will watch the project directory and restart as necessary. | ||||||
							
								
								
									
										12
									
								
								components/Button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								components/Button.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import { JSX } from "preact"; | ||||||
|  | import { IS_BROWSER } from "$fresh/runtime.ts"; | ||||||
|  |  | ||||||
|  | export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) { | ||||||
|  |   return ( | ||||||
|  |     <button | ||||||
|  |       {...props} | ||||||
|  |       disabled={!IS_BROWSER || props.disabled} | ||||||
|  |       class="px-2 py-1 border-gray-500 border-2 rounded bg-white hover:bg-gray-200 transition-colors" | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								components/IngredientsList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								components/IngredientsList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import type { Ingredient, Ingredients } from "../lib/recipes.ts"; | ||||||
|  |  | ||||||
|  | type IngredientsProps = { | ||||||
|  |   ingredients: Ingredients; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function formatIngredient(ingredient: Ingredient) { | ||||||
|  |   return `${ | ||||||
|  |     ingredient.amount && ingredient.unit && | ||||||
|  |     ` - ${ingredient.amount} ${ingredient.unit}` | ||||||
|  |   } ${ingredient.type}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const IngredientsList = ({ ingredients }: IngredientsProps) => { | ||||||
|  |   return ( | ||||||
|  |     <div> | ||||||
|  |       {ingredients.map((item, index) => ( | ||||||
|  |         <div key={index} class="mb-4"> | ||||||
|  |           {"type" in item && formatIngredient(item)} | ||||||
|  |           {"ingredients" in item && Array.isArray(item.ingredients) && ( | ||||||
|  |             <ul class="pl-4 list-disc"> | ||||||
|  |               {item.ingredients.map((ingredient, idx) => ( | ||||||
|  |                 <li key={idx}> | ||||||
|  |                   {formatIngredient(ingredient)} | ||||||
|  |                 </li> | ||||||
|  |               ))} | ||||||
|  |             </ul> | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
|  |       ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										25
									
								
								components/RecipeCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								components/RecipeCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import { Document } from "../lib/documents.ts"; | ||||||
|  | import { Recipe } from "../lib/recipes.ts"; | ||||||
|  |  | ||||||
|  | export function RecipeCard({ recipe }: { recipe: Recipe }) { | ||||||
|  |   return ( | ||||||
|  |     <a | ||||||
|  |       href={`/recipes/${recipe.id}`} | ||||||
|  |       style={{ | ||||||
|  |         backgroundImage: `url(${recipe?.meta?.image})`, | ||||||
|  |         backgroundSize: "cover", | ||||||
|  |       }} | ||||||
|  |       class="bg-gray-900 text-white rounded-3xl shadow-md p-4  | ||||||
|  | flex flex-col justify-between | ||||||
|  |       lg:w-56 lg:h-56  | ||||||
|  |       sm:w-40 sm:h-40  | ||||||
|  |       w-32 h-32" | ||||||
|  |     > | ||||||
|  |       <div> | ||||||
|  |       </div> | ||||||
|  |       <div class="mt-2 "> | ||||||
|  |         {recipe.name} | ||||||
|  |       </div> | ||||||
|  |     </a> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								components/RecipeHero.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								components/RecipeHero.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import { Recipe } from "../lib/recipes.ts"; | ||||||
|  |  | ||||||
|  | export function RecipeHero({ recipe }: { recipe: Recipe }) { | ||||||
|  |   return ( | ||||||
|  |     <div class="relative w-full h-[400px] rounded-3xl overflow-hidden bg-black"> | ||||||
|  |       <img | ||||||
|  |         src={recipe?.meta?.image} | ||||||
|  |         alt="Recipe Banner" | ||||||
|  |         class="object-cover w-full h-full" | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |       <div class="absolute top-4 left-4"> | ||||||
|  |         <a | ||||||
|  |           class="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg" | ||||||
|  |           href="/recipes" | ||||||
|  |         > | ||||||
|  |           Back | ||||||
|  |         </a> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div | ||||||
|  |         class="absolute inset-x-0 bottom-0 py-4 px-12 py-8" | ||||||
|  |         style={{ background: "linear-gradient(0deg, #fffe, #fff0)" }} | ||||||
|  |       > | ||||||
|  |         <h2 class="text-4xl font-bold mt-4" style={{ color: "#1F1F1F" }}> | ||||||
|  |           {recipe.name} | ||||||
|  |         </h2> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								components/layouts/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								components/layouts/main.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import { ComponentChildren } from "preact"; | ||||||
|  |  | ||||||
|  | export type Props = { | ||||||
|  |   children: ComponentChildren; | ||||||
|  |   title?: string; | ||||||
|  |   name?: string; | ||||||
|  |   description?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const MainLayout = ({ children }: Props) => { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <main | ||||||
|  |         class="max-w-2xl mx-auto lg:max-w-4xl py-5" | ||||||
|  |         style={{ fontFamily: "Work Sans" }} | ||||||
|  |       > | ||||||
|  |         {children} | ||||||
|  |       </main> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										30
									
								
								deno.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								deno.json
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | { | ||||||
|  |   "lock": false, | ||||||
|  |   "tasks": { | ||||||
|  |     "start": "deno run -A --watch=static/,routes/ dev.ts", | ||||||
|  |     "update": "deno run -A -r https://fresh.deno.dev/update ." | ||||||
|  |   }, | ||||||
|  |   "lint": { | ||||||
|  |     "rules": { | ||||||
|  |       "tags": [ | ||||||
|  |         "fresh", | ||||||
|  |         "recommended" | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "imports": { | ||||||
|  |     "$fresh/": "https://deno.land/x/fresh@1.3.1/", | ||||||
|  |     "preact": "https://esm.sh/preact@10.15.1", | ||||||
|  |     "preact/": "https://esm.sh/preact@10.15.1/", | ||||||
|  |     "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", | ||||||
|  |     "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", | ||||||
|  |     "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3", | ||||||
|  |     "twind": "https://esm.sh/twind@0.16.19", | ||||||
|  |     "twind/": "https://esm.sh/twind@0.16.19/", | ||||||
|  |     "$std/": "https://deno.land/std@0.193.0/" | ||||||
|  |   }, | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "jsx": "react-jsx", | ||||||
|  |     "jsxImportSource": "preact" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								dev.ts
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								dev.ts
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | #!/usr/bin/env -S deno run -A --watch=static/,routes/ | ||||||
|  |  | ||||||
|  | import dev from "$fresh/dev.ts"; | ||||||
|  |  | ||||||
|  | await dev(import.meta.url, "./main.ts"); | ||||||
							
								
								
									
										34
									
								
								fresh.gen.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								fresh.gen.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | // DO NOT EDIT. This file is generated by fresh. | ||||||
|  | // This file SHOULD be checked into source version control. | ||||||
|  | // This file is automatically updated during development when running `dev.ts`. | ||||||
|  |  | ||||||
|  | import * as $0 from "./routes/_404.tsx"; | ||||||
|  | import * as $1 from "./routes/_app.tsx"; | ||||||
|  | import * as $2 from "./routes/api/index.ts"; | ||||||
|  | import * as $3 from "./routes/api/recipes/[name].ts"; | ||||||
|  | import * as $4 from "./routes/api/recipes/images/[image].ts"; | ||||||
|  | import * as $5 from "./routes/api/recipes/index.ts"; | ||||||
|  | import * as $6 from "./routes/index.tsx"; | ||||||
|  | import * as $7 from "./routes/recipes/[name].tsx"; | ||||||
|  | import * as $8 from "./routes/recipes/index.tsx"; | ||||||
|  | import * as $$0 from "./islands/Counter.tsx"; | ||||||
|  |  | ||||||
|  | const manifest = { | ||||||
|  |   routes: { | ||||||
|  |     "./routes/_404.tsx": $0, | ||||||
|  |     "./routes/_app.tsx": $1, | ||||||
|  |     "./routes/api/index.ts": $2, | ||||||
|  |     "./routes/api/recipes/[name].ts": $3, | ||||||
|  |     "./routes/api/recipes/images/[image].ts": $4, | ||||||
|  |     "./routes/api/recipes/index.ts": $5, | ||||||
|  |     "./routes/index.tsx": $6, | ||||||
|  |     "./routes/recipes/[name].tsx": $7, | ||||||
|  |     "./routes/recipes/index.tsx": $8, | ||||||
|  |   }, | ||||||
|  |   islands: { | ||||||
|  |     "./islands/Counter.tsx": $$0, | ||||||
|  |   }, | ||||||
|  |   baseUrl: import.meta.url, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default manifest; | ||||||
							
								
								
									
										16
									
								
								islands/Counter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								islands/Counter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import type { Signal } from "@preact/signals"; | ||||||
|  | import { Button } from "../components/Button.tsx"; | ||||||
|  |  | ||||||
|  | interface CounterProps { | ||||||
|  |   count: Signal<number>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function Counter(props: CounterProps) { | ||||||
|  |   return ( | ||||||
|  |     <div class="flex gap-8 py-6"> | ||||||
|  |       <Button onClick={() => props.count.value -= 1}>-1</Button> | ||||||
|  |       <p class="text-3xl">{props.count}</p> | ||||||
|  |       <Button onClick={() => props.count.value += 1}>+1</Button> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								lib/documents.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								lib/documents.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | import { unified } from "npm:unified"; | ||||||
|  | import remarkParse from "npm:remark-parse"; | ||||||
|  | import remarkFrontmatter from "https://esm.sh/remark-frontmatter@4"; | ||||||
|  | import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts"; | ||||||
|  |  | ||||||
|  | export type Document = { | ||||||
|  |   name: string; | ||||||
|  |   lastModified: number; | ||||||
|  |   contentType: string; | ||||||
|  |   size: number; | ||||||
|  |   perm: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function parseFrontmatter(yaml: string) { | ||||||
|  |   return parse(yaml); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function getDocuments(): Promise<Document[]> { | ||||||
|  |   const headers = new Headers(); | ||||||
|  |   headers.append("Accept", "application/json"); | ||||||
|  |  | ||||||
|  |   const response = await fetch("http://192.168.178.56:3007/index.json", { | ||||||
|  |     headers: headers, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return response.json(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function getDocument(name: string): Promise<string> { | ||||||
|  |   const response = await fetch("http://192.168.178.56:3007/" + name); | ||||||
|  |   return await response.text(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function parseDocument(doc: string) { | ||||||
|  |   return unified() | ||||||
|  |     .use(remarkParse).use(remarkFrontmatter, ["yaml", "toml"]) | ||||||
|  |     .parse(doc); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export type ParsedDocument = ReturnType<typeof parseDocument>; | ||||||
|  | export type DocumentChild = ParsedDocument["children"][number]; | ||||||
|  |  | ||||||
|  | export function findRangeOfChildren(children: DocumentChild[]) { | ||||||
|  |   const firstChild = children[0]; | ||||||
|  |   const lastChild = children.length > 1 | ||||||
|  |     ? children[children.length - 1] | ||||||
|  |     : firstChild; | ||||||
|  |  | ||||||
|  |   const start = firstChild.position?.start.offset; | ||||||
|  |   const end = lastChild.position?.end.offset; | ||||||
|  |  | ||||||
|  |   if (typeof start !== "number" || typeof end !== "number") return; | ||||||
|  |  | ||||||
|  |   return [start, end]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getTextOfRange(children: DocumentChild[], text: string) { | ||||||
|  |   if (!children || children.length === 0) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const range = findRangeOfChildren(children); | ||||||
|  |   if (!range) return; | ||||||
|  |   return text.substring(range[0], range[1]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getTextOfChild(child: DocumentChild): string | undefined { | ||||||
|  |   if ("value" in child) return child.value; | ||||||
|  |   if ("children" in child) { | ||||||
|  |     return getTextOfChild(child.children[0]); | ||||||
|  |   } | ||||||
|  |   return; | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								lib/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from "./documents.ts"; | ||||||
							
								
								
									
										173
									
								
								lib/recipes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								lib/recipes.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | |||||||
|  | import { | ||||||
|  |   type DocumentChild, | ||||||
|  |   getTextOfChild, | ||||||
|  |   getTextOfRange, | ||||||
|  |   parseDocument, | ||||||
|  |   parseFrontmatter, | ||||||
|  | } from "./documents.ts"; | ||||||
|  |  | ||||||
|  | import { parseIngredient } from "npm:parse-ingredient"; | ||||||
|  |  | ||||||
|  | export type IngredientGroup = { | ||||||
|  |   name: string; | ||||||
|  |   ingredients: Ingredient[]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type Ingredient = { | ||||||
|  |   type: string; | ||||||
|  |   unit?: string; | ||||||
|  |   amount?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type Ingredients = (Ingredient | IngredientGroup)[]; | ||||||
|  |  | ||||||
|  | export type Recipe = { | ||||||
|  |   id: string; | ||||||
|  |   meta?: { | ||||||
|  |     link?: string; | ||||||
|  |     image?: string; | ||||||
|  |     rating?: number; | ||||||
|  |     portion?: number; | ||||||
|  |   }; | ||||||
|  |   name: string; | ||||||
|  |   description?: string; | ||||||
|  |   ingredients: Ingredients; | ||||||
|  |   preparation?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function parseIngredientItem(listItem: DocumentChild): Ingredient | undefined { | ||||||
|  |   if (listItem.type === "listItem") { | ||||||
|  |     const children: DocumentChild[] = listItem.children[0]?.children || | ||||||
|  |       listItem.children; | ||||||
|  |  | ||||||
|  |     const text = children.map((c) => getTextOfChild(c)).join(" ").trim(); | ||||||
|  |  | ||||||
|  |     const ing = parseIngredient(text, { | ||||||
|  |       additionalUOMs: { | ||||||
|  |         tableSpoon: { | ||||||
|  |           short: "EL", | ||||||
|  |           plural: "Table Spoons", | ||||||
|  |           alternates: ["el", "EL"], | ||||||
|  |         }, | ||||||
|  |         teaSpoon: { | ||||||
|  |           short: "TL", | ||||||
|  |           plural: "Tea Spoon", | ||||||
|  |           alternates: ["tl", "TL"], | ||||||
|  |         }, | ||||||
|  |         litre: { | ||||||
|  |           short: "L", | ||||||
|  |           plural: "liters", | ||||||
|  |           alternates: ["L", "l"], | ||||||
|  |         }, | ||||||
|  |         paket: { | ||||||
|  |           short: "Paket", | ||||||
|  |           plural: "Pakets", | ||||||
|  |           alternates: ["Paket", "paket"], | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       type: ing[0].description, | ||||||
|  |       unit: ing[0].unitOfMeasure, | ||||||
|  |       amount: ing[0].quantity, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   return; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const isIngredient = (item: Ingredient | undefined): item is Ingredient => { | ||||||
|  |   return !!item; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | function parseIngredientsList(list: DocumentChild): Ingredient[] { | ||||||
|  |   if (list.type === "list" && "children" in list) { | ||||||
|  |     return list.children.map((listItem) => { | ||||||
|  |       return parseIngredientItem(listItem); | ||||||
|  |     }).filter(isIngredient); | ||||||
|  |   } | ||||||
|  |   return []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function parseIngredients(children: DocumentChild[]): Recipe["ingredients"] { | ||||||
|  |   const ingredients: (Ingredient | IngredientGroup)[] = []; | ||||||
|  |   if (!children) return []; | ||||||
|  |   let skip = false; | ||||||
|  |   for (let i = 0; i < children.length; i++) { | ||||||
|  |     if (skip) { | ||||||
|  |       skip = false; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     const child = children[i]; | ||||||
|  |  | ||||||
|  |     if (child.type === "paragraph") { | ||||||
|  |       const nextChild = children[i + 1]; | ||||||
|  |  | ||||||
|  |       if (nextChild.type !== "list") continue; | ||||||
|  |  | ||||||
|  |       ingredients.push({ | ||||||
|  |         name: getTextOfChild(child) || "", | ||||||
|  |         ingredients: parseIngredientsList(nextChild), | ||||||
|  |       }); | ||||||
|  |       skip = true; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (child.type === "list") { | ||||||
|  |       ingredients.push(...parseIngredientsList(child)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ingredients; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function parseRecipe(original: string, id: string): Recipe { | ||||||
|  |   const doc = parseDocument(original); | ||||||
|  |  | ||||||
|  |   let name = ""; | ||||||
|  |   let meta: Recipe["meta"] = {}; | ||||||
|  |  | ||||||
|  |   const groups: DocumentChild[][] = []; | ||||||
|  |   let group: DocumentChild[] = []; | ||||||
|  |   for (const child of doc.children) { | ||||||
|  |     if (child.type === "yaml") { | ||||||
|  |       meta = parseFrontmatter(child.value) as Recipe["meta"]; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       child.type === "heading" && child.depth === 1 && !name && | ||||||
|  |       child.children.length === 1 && child.children[0].type === "text" | ||||||
|  |     ) { | ||||||
|  |       name = child.children[0].value; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     if (child.type === "thematicBreak") { | ||||||
|  |       groups.push(group); | ||||||
|  |       group = []; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |     group.push(child); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (group.length) { | ||||||
|  |     groups.push(group); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const description = getTextOfRange(groups[0], original); | ||||||
|  |  | ||||||
|  |   const ingredients = parseIngredients(groups[1]); | ||||||
|  |  | ||||||
|  |   const preparation = getTextOfRange(groups[2], original); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     id, | ||||||
|  |     meta: { | ||||||
|  |       ...meta, | ||||||
|  |       image: meta?.image?.replace(/^Recipes\/images/, "/api/recipes/images"), | ||||||
|  |     }, | ||||||
|  |     name, | ||||||
|  |     description, | ||||||
|  |     ingredients, | ||||||
|  |     preparation, | ||||||
|  |   }; | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								main.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | /// <reference no-default-lib="true" /> | ||||||
|  | /// <reference lib="dom" /> | ||||||
|  | /// <reference lib="dom.iterable" /> | ||||||
|  | /// <reference lib="dom.asynciterable" /> | ||||||
|  | /// <reference lib="deno.ns" /> | ||||||
|  |  | ||||||
|  | import "$std/dotenv/load.ts"; | ||||||
|  |  | ||||||
|  | import { start } from "$fresh/server.ts"; | ||||||
|  | import manifest from "./fresh.gen.ts"; | ||||||
|  |  | ||||||
|  | import twindPlugin from "$fresh/plugins/twind.ts"; | ||||||
|  | import twindConfig from "./twind.config.ts"; | ||||||
|  |  | ||||||
|  | await start(manifest, { plugins: [twindPlugin(twindConfig)] }); | ||||||
							
								
								
									
										28
									
								
								routes/_404.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								routes/_404.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  |  | ||||||
|  | import { Head } from "$fresh/runtime.ts"; | ||||||
|  |  | ||||||
|  | export default function Error404() { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Head> | ||||||
|  |         <title>404 - Page not found</title> | ||||||
|  |       </Head> | ||||||
|  |       <div class="px-4 py-8 mx-auto bg-[#86efac]"> | ||||||
|  |         <div class="max-w-screen-md mx-auto flex flex-col items-center justify-center"> | ||||||
|  |           <img | ||||||
|  |             class="my-6" | ||||||
|  |             src="/logo.svg" | ||||||
|  |             width="128" | ||||||
|  |             height="128" | ||||||
|  |             alt="the fresh logo: a sliced lemon dripping with juice" | ||||||
|  |           /> | ||||||
|  |           <h1 class="text-4xl font-bold">404 - Page not found</h1> | ||||||
|  |           <p class="my-4"> | ||||||
|  |             The page you were looking for doesn't exist. | ||||||
|  |           </p> | ||||||
|  |           <a href="/" class="underline">Go back home</a> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								routes/_app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								routes/_app.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import { AppProps } from "$fresh/server.ts"; | ||||||
|  |  | ||||||
|  | import { Head } from "$fresh/runtime.ts"; | ||||||
|  | export default function App({ Component }: AppProps) { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Head> | ||||||
|  |         <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||||||
|  |         <link | ||||||
|  |           rel="preconnect" | ||||||
|  |           href="https://fonts.gstatic.com" | ||||||
|  |           crossOrigin="" | ||||||
|  |         /> | ||||||
|  |         <link | ||||||
|  |           href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@100;400;700&display=swap" | ||||||
|  |           rel="stylesheet" | ||||||
|  |         /> | ||||||
|  |         <link href="/global.css" rel="stylesheet" /> | ||||||
|  |       </Head> | ||||||
|  |       <Component /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								routes/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								routes/api/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | import { HandlerContext } from "$fresh/server.ts"; | ||||||
|  | import { getDocuments } from "../../lib/documents.ts"; | ||||||
|  |  | ||||||
|  | export const handler = async ( | ||||||
|  |   _req: Request, | ||||||
|  |   _ctx: HandlerContext, | ||||||
|  | ): Promise<Response> => { | ||||||
|  |   const documents = await getDocuments(); | ||||||
|  |   const response = new Response(JSON.stringify(documents)); | ||||||
|  |   return response; | ||||||
|  | }; | ||||||
							
								
								
									
										23
									
								
								routes/api/recipes/[name].ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								routes/api/recipes/[name].ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import { HandlerContext } from "$fresh/server.ts"; | ||||||
|  | import { getDocument } from "../../../lib/documents.ts"; | ||||||
|  | import { parseRecipe } from "../../../lib/recipes.ts"; | ||||||
|  |  | ||||||
|  | export async function getRecipe(name: string) { | ||||||
|  |   const document = await getDocument(`Recipes/${name}.md`); | ||||||
|  |  | ||||||
|  |   const recipe = parseRecipe(document, name); | ||||||
|  |  | ||||||
|  |   return recipe; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const handler = async ( | ||||||
|  |   _req: Request, | ||||||
|  |   _ctx: HandlerContext, | ||||||
|  | ): Promise<Response> => { | ||||||
|  |   const recipe = await getRecipe(_ctx.params.name); | ||||||
|  |  | ||||||
|  |   const headers = new Headers(); | ||||||
|  |   headers.append("Content-Type", "application/json"); | ||||||
|  |  | ||||||
|  |   return new Response(JSON.stringify(recipe)); | ||||||
|  | }; | ||||||
							
								
								
									
										26
									
								
								routes/api/recipes/images/[image].ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								routes/api/recipes/images/[image].ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import { HandlerContext } from "$fresh/server.ts"; | ||||||
|  |  | ||||||
|  | function copyHeader(headerName: string, to: Headers, from: Headers) { | ||||||
|  |   const hdrVal = from.get(headerName); | ||||||
|  |   if (hdrVal) { | ||||||
|  |     to.set(headerName, hdrVal); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const handler = async ( | ||||||
|  |   _req: Request, | ||||||
|  |   _ctx: HandlerContext, | ||||||
|  | ): Promise<Response> => { | ||||||
|  |   const proxyRes = await fetch( | ||||||
|  |     "http://192.168.178.56:3007/Recipes/images/" + _ctx.params.image, | ||||||
|  |   ); | ||||||
|  |   console.log({ params: _ctx.params }); | ||||||
|  |   const headers = new Headers(); | ||||||
|  |   copyHeader("content-length", headers, proxyRes.headers); | ||||||
|  |   copyHeader("content-type", headers, proxyRes.headers); | ||||||
|  |   copyHeader("content-disposition", headers, proxyRes.headers); | ||||||
|  |   return new Response(proxyRes.body, { | ||||||
|  |     status: proxyRes.status, | ||||||
|  |     headers, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										34
									
								
								routes/api/recipes/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								routes/api/recipes/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | import { HandlerContext } from "$fresh/server.ts"; | ||||||
|  | import { getDocument, getDocuments } from "../../../lib/documents.ts"; | ||||||
|  | import { parseRecipe } from "../../../lib/recipes.ts"; | ||||||
|  |  | ||||||
|  | export async function getRecipes() { | ||||||
|  |   const documents = await getDocuments(); | ||||||
|  |  | ||||||
|  |   return Promise.all( | ||||||
|  |     documents.filter((d) => { | ||||||
|  |       return d.name.startsWith("Recipes/") && | ||||||
|  |         d.contentType === "text/markdown" && | ||||||
|  |         !d.name.endsWith("index.md"); | ||||||
|  |     }).map(async (doc) => { | ||||||
|  |       const document = await getDocument(doc.name); | ||||||
|  |       const recipe = parseRecipe(document, doc.name); | ||||||
|  |       return { | ||||||
|  |         ...recipe, | ||||||
|  |         id: recipe.id.replace(/^Recipes\//, "").replace(/\.md$/, ""), | ||||||
|  |       }; | ||||||
|  |     }), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const handler = async ( | ||||||
|  |   _req: Request, | ||||||
|  |   _ctx: HandlerContext, | ||||||
|  | ): Promise<Response> => { | ||||||
|  |   const headers = new Headers(); | ||||||
|  |   headers.append("Content-Type", "application/json"); | ||||||
|  |  | ||||||
|  |   const recipes = await getRecipes(); | ||||||
|  |  | ||||||
|  |   return new Response(JSON.stringify(recipes), { headers }); | ||||||
|  | }; | ||||||
							
								
								
									
										31
									
								
								routes/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								routes/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import { Head } from "$fresh/runtime.ts"; | ||||||
|  | import { useSignal } from "@preact/signals"; | ||||||
|  | import Counter from "../islands/Counter.tsx"; | ||||||
|  |  | ||||||
|  | export default function Home() { | ||||||
|  |   const count = useSignal(3); | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <Head> | ||||||
|  |         <title>app</title> | ||||||
|  |       </Head> | ||||||
|  |       <div class="px-4 py-8 mx-auto bg-[#86efac]"> | ||||||
|  |         <div class="max-w-screen-md mx-auto flex flex-col items-center justify-center"> | ||||||
|  |           <img | ||||||
|  |             class="my-6" | ||||||
|  |             src="/logo.svg" | ||||||
|  |             width="128" | ||||||
|  |             height="128" | ||||||
|  |             alt="the fresh logo: a sliced lemon dripping with juice" | ||||||
|  |           /> | ||||||
|  |           <h1 class="text-4xl font-bold">Welcome to fresh</h1> | ||||||
|  |           <p class="my-4"> | ||||||
|  |             Try updating this message in the | ||||||
|  |             <code class="mx-2">./routes/index.tsx</code> file, and refresh. | ||||||
|  |           </p> | ||||||
|  |           <Counter count={count} /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								routes/recipes/[name].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								routes/recipes/[name].tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import { Handlers, PageProps } from "$fresh/server.ts"; | ||||||
|  | import { IngredientsList } from "../../components/IngredientsList.tsx"; | ||||||
|  | import { RecipeHero } from "../../components/RecipeHero.tsx"; | ||||||
|  | import { MainLayout } from "../../components/layouts/main.tsx"; | ||||||
|  | import { Recipe } from "../../lib/recipes.ts"; | ||||||
|  | import { getRecipe } from "../api/recipes/[name].ts"; | ||||||
|  |  | ||||||
|  | export const handler: Handlers<Recipe | null> = { | ||||||
|  |   async GET(_, ctx) { | ||||||
|  |     const recipe = await getRecipe(ctx.params.name); | ||||||
|  |     return ctx.render(recipe); | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function Greet(props: PageProps<Recipe>) { | ||||||
|  |   return ( | ||||||
|  |     <MainLayout> | ||||||
|  |       <RecipeHero recipe={props.data} /> | ||||||
|  |       <div class="px-12 text-white mt-10"> | ||||||
|  |         <h3 class="text-3xl">Ingredients</h3> | ||||||
|  |         <IngredientsList ingredients={props.data.ingredients} /> | ||||||
|  |         <h3 class="text-3xl">Preperation</h3> | ||||||
|  |       </div> | ||||||
|  |     </MainLayout> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								routes/recipes/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								routes/recipes/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import { Handlers, PageProps } from "$fresh/server.ts"; | ||||||
|  | import { RecipeCard } from "../../components/RecipeCard.tsx"; | ||||||
|  | import { MainLayout } from "../../components/layouts/main.tsx"; | ||||||
|  | import type { Document } from "../../lib/documents.ts"; | ||||||
|  | import { Recipe } from "../../lib/recipes.ts"; | ||||||
|  | import { getRecipes } from "../api/recipes/index.ts"; | ||||||
|  |  | ||||||
|  | export const handler: Handlers<Recipe[] | null> = { | ||||||
|  |   async GET(_, ctx) { | ||||||
|  |     const recipes = await getRecipes(); | ||||||
|  |     return ctx.render(recipes); | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default function Greet(props: PageProps<Recipe[] | null>) { | ||||||
|  |   return ( | ||||||
|  |     <MainLayout> | ||||||
|  |       <div class="flex flex-wrap justify-center items-center gap-4 px-4"> | ||||||
|  |         {props.data?.map((doc) => { | ||||||
|  |           return <RecipeCard recipe={doc} />; | ||||||
|  |         })} | ||||||
|  |       </div> | ||||||
|  |     </MainLayout> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										3
									
								
								static/global.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								static/global.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | body { | ||||||
|  |   background: #1F1F1F | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								static/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								static/logo.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |   <path d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z" fill="#FFDB1E"/> | ||||||
|  |   <path d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z" fill="#fff" stroke="#FFDB1E"/> | ||||||
|  |   <path d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z" fill="#FFE600"/> | ||||||
|  |   <path d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z" fill="#fff"/> | ||||||
|  | </svg> | ||||||
| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										5
									
								
								twind.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								twind.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | import { Options } from "$fresh/plugins/twind.ts"; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   selfURL: import.meta.url, | ||||||
|  | } as Options; | ||||||
		Reference in New Issue
	
	Block a user