feat: init
This commit is contained in:
commit
8e461cea26
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;
|
Loading…
x
Reference in New Issue
Block a user