commit 8e461cea26cf17f03e16adf6e3c701a1afc36762 Author: Max Richter Date: Wed Jul 26 13:47:01 2023 +0200 feat: init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e06ffc --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec0e33e --- /dev/null +++ b/README.md @@ -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. diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 0000000..f1b80a0 --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,12 @@ +import { JSX } from "preact"; +import { IS_BROWSER } from "$fresh/runtime.ts"; + +export function Button(props: JSX.HTMLAttributes) { + return ( + +

{props.count}

+ + + ); +} diff --git a/lib/documents.ts b/lib/documents.ts new file mode 100644 index 0000000..3f7d26f --- /dev/null +++ b/lib/documents.ts @@ -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 { + 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 { + 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; +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; +} diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..e1038ae --- /dev/null +++ b/lib/index.ts @@ -0,0 +1 @@ +export * from "./documents.ts"; diff --git a/lib/recipes.ts b/lib/recipes.ts new file mode 100644 index 0000000..ff4f45d --- /dev/null +++ b/lib/recipes.ts @@ -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, + }; +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..984b0ae --- /dev/null +++ b/main.ts @@ -0,0 +1,15 @@ +/// +/// +/// +/// +/// + +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)] }); diff --git a/routes/_404.tsx b/routes/_404.tsx new file mode 100644 index 0000000..c8228ab --- /dev/null +++ b/routes/_404.tsx @@ -0,0 +1,28 @@ + +import { Head } from "$fresh/runtime.ts"; + +export default function Error404() { + return ( + <> + + 404 - Page not found + +
+
+ the fresh logo: a sliced lemon dripping with juice +

404 - Page not found

+

+ The page you were looking for doesn't exist. +

+ Go back home +
+
+ + ); +} diff --git a/routes/_app.tsx b/routes/_app.tsx new file mode 100644 index 0000000..17a354d --- /dev/null +++ b/routes/_app.tsx @@ -0,0 +1,23 @@ +import { AppProps } from "$fresh/server.ts"; + +import { Head } from "$fresh/runtime.ts"; +export default function App({ Component }: AppProps) { + return ( + <> + + + + + + + + + ); +} diff --git a/routes/api/index.ts b/routes/api/index.ts new file mode 100644 index 0000000..35d830c --- /dev/null +++ b/routes/api/index.ts @@ -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 => { + const documents = await getDocuments(); + const response = new Response(JSON.stringify(documents)); + return response; +}; diff --git a/routes/api/recipes/[name].ts b/routes/api/recipes/[name].ts new file mode 100644 index 0000000..d87b4f3 --- /dev/null +++ b/routes/api/recipes/[name].ts @@ -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 => { + const recipe = await getRecipe(_ctx.params.name); + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + + return new Response(JSON.stringify(recipe)); +}; diff --git a/routes/api/recipes/images/[image].ts b/routes/api/recipes/images/[image].ts new file mode 100644 index 0000000..fff6fb2 --- /dev/null +++ b/routes/api/recipes/images/[image].ts @@ -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 => { + 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, + }); +}; diff --git a/routes/api/recipes/index.ts b/routes/api/recipes/index.ts new file mode 100644 index 0000000..2ebb2b4 --- /dev/null +++ b/routes/api/recipes/index.ts @@ -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 => { + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + + const recipes = await getRecipes(); + + return new Response(JSON.stringify(recipes), { headers }); +}; diff --git a/routes/index.tsx b/routes/index.tsx new file mode 100644 index 0000000..20bb512 --- /dev/null +++ b/routes/index.tsx @@ -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 ( + <> + + app + +
+
+ the fresh logo: a sliced lemon dripping with juice +

Welcome to fresh

+

+ Try updating this message in the + ./routes/index.tsx file, and refresh. +

+ +
+
+ + ); +} diff --git a/routes/recipes/[name].tsx b/routes/recipes/[name].tsx new file mode 100644 index 0000000..a903bce --- /dev/null +++ b/routes/recipes/[name].tsx @@ -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 = { + async GET(_, ctx) { + const recipe = await getRecipe(ctx.params.name); + return ctx.render(recipe); + }, +}; + +export default function Greet(props: PageProps) { + return ( + + +
+

Ingredients

+ +

Preperation

+
+
+ ); +} diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx new file mode 100644 index 0000000..19d9b25 --- /dev/null +++ b/routes/recipes/index.tsx @@ -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 = { + async GET(_, ctx) { + const recipes = await getRecipes(); + return ctx.render(recipes); + }, +}; + +export default function Greet(props: PageProps) { + return ( + +
+ {props.data?.map((doc) => { + return ; + })} +
+
+ ); +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..1cfaaa2 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/global.css b/static/global.css new file mode 100644 index 0000000..7c270fa --- /dev/null +++ b/static/global.css @@ -0,0 +1,3 @@ +body { + background: #1F1F1F +} diff --git a/static/logo.svg b/static/logo.svg new file mode 100644 index 0000000..ef2fbe4 --- /dev/null +++ b/static/logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/twind.config.ts b/twind.config.ts new file mode 100644 index 0000000..2a7ac27 --- /dev/null +++ b/twind.config.ts @@ -0,0 +1,5 @@ +import { Options } from "$fresh/plugins/twind.ts"; + +export default { + selfURL: import.meta.url, +} as Options;