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