From f680b5f832734ca1cfac46cd33041c3b04c99e5a Mon Sep 17 00:00:00 2001 From: Max Richter Date: Tue, 28 Oct 2025 20:15:23 +0100 Subject: [PATCH] feat: initial refactor to use marka as backend --- components/Card.tsx | 16 +-- components/MetaTags.tsx | 14 +-- islands/IngredientsList.tsx | 49 +++----- lib/crud.ts | 149 ---------------------- lib/db/sqlite.ts | 2 +- lib/documents.ts | 161 ++---------------------- lib/env.ts | 3 - lib/helpers.ts | 2 +- lib/recipeSchema.ts | 47 ++++--- lib/resource/articles.ts | 91 -------------- lib/resource/movies.ts | 106 ---------------- lib/resource/recipes.ts | 188 ---------------------------- lib/resource/series.ts | 102 --------------- lib/resources.ts | 11 ++ lib/search.ts | 17 +-- main.ts | 2 +- routes/api/articles/[name].ts | 4 +- routes/api/articles/create/index.ts | 6 +- routes/api/articles/index.ts | 6 +- routes/api/images/index.ts | 15 +-- routes/api/index.ts | 4 +- routes/api/movies/[name].ts | 8 +- routes/api/movies/enhance/[name].ts | 9 +- routes/api/movies/index.ts | 4 +- routes/api/recipes/[name].ts | 4 +- routes/api/recipes/create/index.ts | 12 +- routes/api/recipes/index.ts | 4 +- routes/api/recommendation/all.ts | 13 +- routes/api/series/[name].ts | 5 +- routes/api/series/enhance/[name].ts | 7 +- routes/articles/[name].tsx | 36 +++--- routes/articles/index.tsx | 6 +- routes/index.tsx | 7 +- routes/movies/[name].tsx | 35 +++--- routes/movies/index.tsx | 9 +- routes/recipes/[name].tsx | 53 ++++---- routes/recipes/index.tsx | 11 +- routes/series/[name].tsx | 32 +++-- routes/series/index.tsx | 7 +- 39 files changed, 245 insertions(+), 1012 deletions(-) delete mode 100644 lib/crud.ts diff --git a/components/Card.tsx b/components/Card.tsx index 4e80707..1cea3e5 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -3,6 +3,7 @@ import { IconBrandYoutube } from "@components/icons.tsx"; import { GenericResource } from "@lib/types.ts"; import { SmallRating } from "@components/Rating.tsx"; import { Link } from "@islands/Link.tsx"; +import { parseRating } from "@lib/helpers.ts"; export function Card( { @@ -96,20 +97,21 @@ export function Card( export function ResourceCard( { res, sublink = "movies" }: { sublink?: string; res: GenericResource }, ) { - const { meta: { image } = {} } = res || {}; + const img = res?.content?.image || res?.content?.cover; - const imageUrl = image - ? `/api/images?image=${image}&width=200&height=200` + const imageUrl = img + ? `/api/images?image=${img}&width=200&height=200` : "/placeholder.svg"; return ( ); } diff --git a/components/MetaTags.tsx b/components/MetaTags.tsx index 44b877d..1ec5601 100644 --- a/components/MetaTags.tsx +++ b/components/MetaTags.tsx @@ -2,13 +2,13 @@ import { GenericResource } from "@lib/types.ts"; import { Head } from "$fresh/runtime.ts"; function generateJsonLd(resource: GenericResource): string { - const imageUrl = resource.meta?.image - ? `/api/images?image=${resource.meta.image}&width=1200` + const imageUrl = resource.content?.image + ? `/api/images?image=${resource.content.image}&width=1200` : "/images/og-image.jpg"; const baseSchema: Record = { "@context": "https://schema.org", - "@type": resource.type.charAt(0).toUpperCase() + resource.type.slice(1), // Converts type to PascalCase + "@type": resource.content?._type, // Converts type to PascalCase name: resource.name, description: resource.content || resource.meta?.average || "", keywords: resource.tags?.join(", ") || "", @@ -45,14 +45,14 @@ function generateJsonLd(resource: GenericResource): string { export function MetaTags({ resource }: { resource: GenericResource }) { const jsonLd = generateJsonLd(resource); - const imageUrl = resource.meta?.image - ? `/api/images?image=${resource.meta.image}&width=1200` + const imageUrl = resource.content?.image + ? `/api/images?image=${resource.content.image}&width=1200` : "/images/og-image.jpg"; return ( <> - - + + = ( { ingredients, amount, portion }, ) => { - return ( - - - {ingredients.map((item, index) => { - if ("items" in item) { - // Render IngredientGroup - const { name, items: groupIngredients } = item as IngredientGroup; - + return ( +
+ + {ingredients.filter((s) => !!s?.length).map((item) => { return ( - <> - - - - {groupIngredients.map((item, index) => { - // Render Ingredient - return ( - - ); - })} - +
+
); - } else { - return ( - - ); - } - })} -
-
{name}
- ); -}; + // return ( + // + // ); + })} + + + ); + }; diff --git a/lib/crud.ts b/lib/crud.ts deleted file mode 100644 index 3075c6a..0000000 --- a/lib/crud.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { - createDocument, - getDocument, - getDocuments, - transformDocument, -} from "@lib/documents.ts"; -import { Root } from "https://esm.sh/remark-frontmatter@4.0.1"; -import { GenericResource } from "@lib/types.ts"; -import { parseRating } from "@lib/helpers.ts"; -import { isLocalImage } from "@lib/string.ts"; -import { SILVERBULLET_SERVER } from "@lib/env.ts"; -import { imageTable } from "@lib/db/schema.ts"; -import { db } from "@lib/db/sqlite.ts"; -import { eq } from "drizzle-orm/sql"; -import { createCache } from "@lib/cache.ts"; - -export async function addThumbnailToResource( - res: T, -): Promise { - if (!res?.meta?.image) return res; - - const imageUrl = isLocalImage(res.meta.image) - ? `${SILVERBULLET_SERVER}/${res.meta.image}` - : res.meta.image; - - const image = await db.select().from(imageTable) - .where(eq(imageTable.url, imageUrl)) - .limit(1) - .then((images) => images[0]); - - if (image) { - return { - ...res, - meta: { - ...res.meta, - average: image.average, - thumbnail: image.blurhash, - }, - }; - } - return res; -} - -type SortType = "rating" | "date" | "name" | "author"; - -function sortFunction(sortType: SortType) { - return (a: T, b: T) => { - switch (sortType) { - case "rating": - return parseRating(a.meta?.rating || 0) > - parseRating(b.meta?.rating || 0) - ? -1 - : 1; - case "date": - return (a.meta?.date || 0) > (b.meta?.date || 0) ? -1 : 1; - case "name": - return a.name.localeCompare(b.name); - case "author": - return a.meta?.author?.localeCompare(b.meta?.author || "") || 0; - default: - return 0; - } - }; -} - -export function createCrud( - { prefix, parse, render, hasThumbnails = false }: { - prefix: string; - hasThumbnails?: boolean; - render?: (doc: T) => string; - parse: (doc: string, id: string) => T; - }, -) { - const cache = createCache(`crud/${prefix}`, { expires: 60 * 1000 }); - - function pathFromId(id: string) { - return `${prefix}${id.replaceAll(":", "")}.md`; - } - - async function read(id: string) { - const path = pathFromId(id); - - const content = await getDocument(path); - if (!content) { - return; - } - - let parsed = parse(content, id); - - if (hasThumbnails) { - parsed = await addThumbnailToResource(parsed); - } - const doc = { ...parsed, content }; - - return doc; - } - function create(id: string, content: string | ArrayBuffer | T) { - const path = pathFromId(id); - cache.set("all", undefined); - if ( - typeof content === "string" || content instanceof ArrayBuffer - ) { - return createDocument(path, content); - } - - if (render) { - const rendered = render(content); - return createDocument(path, rendered); - } - - throw new Error("No renderer defined for " + prefix + " CRUD"); - } - - async function update(id: string, updater: (r: Root) => Root) { - const path = pathFromId(id); - const content = await getDocument(path); - if (!content) { - return; - } - const newDoc = transformDocument(content, updater); - await createDocument(path, newDoc); - } - - async function readAll({ sort = "rating" }: { sort?: SortType } = {}) { - if (cache.has("all")) { - return cache.get("all") as unknown as T[]; - } - const allDocuments = await getDocuments(); - const parsed = (await Promise.all( - allDocuments.filter((d) => { - return d.name.startsWith(prefix) && - d.contentType === "text/markdown" && - !d.name.endsWith("index.md"); - }).map((doc) => { - const id = doc.name.replace(prefix, "").replace(/\.md$/, ""); - return read(id); - }), - )).sort(sortFunction(sort)).filter((v) => !!v); - - return parsed; - } - - return { - read, - readAll, - create, - update, - }; -} diff --git a/lib/db/sqlite.ts b/lib/db/sqlite.ts index 9737e40..10ea2b9 100644 --- a/lib/db/sqlite.ts +++ b/lib/db/sqlite.ts @@ -1,5 +1,5 @@ -import { drizzle } from "drizzle-orm/libsql/node"; import { DATA_DIR } from "@lib/env.ts"; +import { drizzle } from "drizzle-orm/libsql"; import path from "node:path"; const DB_FILE = "file:" + path.resolve(DATA_DIR, "db.sqlite"); diff --git a/lib/documents.ts b/lib/documents.ts index 9be0e46..71fe185 100644 --- a/lib/documents.ts +++ b/lib/documents.ts @@ -1,19 +1,7 @@ -import { unified } from "https://esm.sh/unified@10.1.2"; import { render } from "gfm"; import "https://esm.sh/prismjs@1.29.0/components/prism-typescript?no-check"; import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check"; import "https://esm.sh/prismjs@1.29.0/components/prism-rust?no-check"; -import remarkParse from "https://esm.sh/remark-parse@10.0.2"; -import remarkStringify from "https://esm.sh/remark-stringify@10.0.3"; -import remarkFrontmatter, { - Root, -} from "https://esm.sh/remark-frontmatter@4.0.1"; -import { SILVERBULLET_SERVER } from "@lib/env.ts"; -import { fixRenderedMarkdown } from "@lib/helpers.ts"; -import { createLogger } from "@lib/log/index.ts"; -import { db } from "@lib/db/sqlite.ts"; -import { documentTable } from "@lib/db/schema.ts"; -import { eq } from "drizzle-orm/sql"; export type Document = { name: string; @@ -24,114 +12,6 @@ export type Document = { perm: string; }; -const log = createLogger("documents"); - -export async function getDocuments(): Promise { - let documents = await db.select().from(documentTable).all(); - if (documents.length) return documents; - - const headers = new Headers(); - headers.append("Accept", "application/json"); - headers.append("X-Sync-Mode", "true"); - log.debug("fetching all documents"); - const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, { - headers: headers, - }); - - documents = await response.json(); - await db.delete(documentTable); - await db.insert(documentTable).values(documents); - - return documents; -} - -export function createDocument( - name: string, - content: string | ArrayBuffer, - mediaType?: string, -) { - const headers = new Headers(); - - if (mediaType) { - headers.append("Content-Type", mediaType); - } - - log.info("creating document", { name }); - - if (typeof content === "string") { - updateDocument(name, content).catch(log.error); - } - - return fetch(SILVERBULLET_SERVER + "/" + name, { - body: content, - method: "PUT", - headers, - }); -} - -async function fetchDocument(name: string) { - log.debug("fetching document", { name }); - const headers = new Headers(); - headers.append("X-Sync-Mode", "true"); - const response = await fetch(SILVERBULLET_SERVER + "/" + name, { headers }); - if (response.status === 404) { - return; - } - return response.text(); -} - -export async function getDocument(name: string): Promise { - const documents = await db.select().from(documentTable).where( - eq(documentTable.name, name), - ).limit(1); - // This updates the document in the background - fetchDocument(name).then((content) => { - if (content) { - updateDocument(name, content); - } else { - db.delete(documentTable).where(eq(documentTable.name, name)); - } - }).catch( - log.error, - ); - if (documents[0]?.content) return documents[0].content; - - const text = await fetchDocument(name); - if (!text) { - db.delete(documentTable).where(eq(documentTable.name, name)); - return; - } - await updateDocument(name, text); - - return text; -} - -export function updateDocument(name: string, content: string) { - return db.update(documentTable).set({ - content, - }).where(eq(documentTable.name, name)).run(); -} - -export function transformDocument(input: string, cb: (r: Root) => Root) { - const out = unified() - .use(remarkParse) - .use(remarkFrontmatter, ["yaml"]) - .use(() => (tree) => { - return cb(tree); - }) - .use(remarkStringify) - .processSync(input); - - return fixRenderedMarkdown(String(out)); -} - -export function parseDocument(doc: string) { - return unified() - .use(remarkParse) - .use(remarkFrontmatter, ["yaml", "toml"]) - .parse(doc); -} - function removeFrontmatter(doc: string) { if (doc.trim().startsWith("---")) { return doc.trim().split("---").filter((s) => s.length).slice(1).join("---"); @@ -160,42 +40,15 @@ export function removeImage(doc: string, imageUrl?: string) { export function renderMarkdown(doc: string) { return render(removeFrontmatter(doc), { - baseUrl: SILVERBULLET_SERVER, + baseUrl: "https://max-richter.dev", allowMath: true, }); } -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; +export function createDocument( + path: string, + entry: string, + mimetype = "image/jpeg", +) { + console.log("creating", { path, entry, mimetype }); } diff --git a/lib/env.ts b/lib/env.ts index 5e21eb0..0ec51fe 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -4,7 +4,6 @@ export const PROXY_SERVER = Deno.env.get("PROXY_SERVER"); export const PROXY_USERNAME = Deno.env.get("PROXY_USERNAME"); export const PROXY_PASSWORD = Deno.env.get("PROXY_PASSWORD"); -export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER"); export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY"); export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY"); export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY"); @@ -15,8 +14,6 @@ export const GITEA_CLIENT_ID = Deno.env.get("GITEA_CLIENT_ID")!; export const GITEA_CLIENT_SECRET = Deno.env.get("GITEA_CLIENT_SECRET"); export const GITEA_REDIRECT_URL = Deno.env.get("GITEA_REDIRECT_URL"); -export const DATABASE_URL = Deno.env.get("DATABASE_URL") || "dev.db"; - const duration = Deno.env.get("SESSION_DURATION"); export const SESSION_DURATION = duration ? +duration : (60 * 60 * 24); diff --git a/lib/helpers.ts b/lib/helpers.ts index 6d6a745..5d351dc 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -103,7 +103,7 @@ export function debounce) => void>( export function parseRating(rating: string | number) { if (typeof rating === "string") { - return [...rating.matchAll(/⭐/)].length; + return [...rating.matchAll(/⭐/g)].length; } return rating; } diff --git a/lib/recipeSchema.ts b/lib/recipeSchema.ts index 54a0149..a1636ad 100644 --- a/lib/recipeSchema.ts +++ b/lib/recipeSchema.ts @@ -17,25 +17,31 @@ export const IngredientGroupSchema = z.object({ export type IngredientGroup = z.infer; const recipeSchema = z.object({ - title: z.string().describe( - "Title of the Recipe, without the name of the website or author", - ), - image: z.string().describe("URL of the main image of the recipe"), - author: z.string().describe("author of the Recipe (optional)"), - description: z.string().describe("Optional, short description of the recipe"), - ingredients: z.array(z.union([IngredientSchema, IngredientGroupSchema])) - .describe("List of ingredients"), - instructions: z.array(z.string()).describe("List of instructions"), - servings: z.number().describe("Amount of Portions"), - prepTime: z.number().describe("Preparation time in minutes"), - cookTime: z.number().describe("Cooking time in minutes"), - totalTime: z.number().describe("Total time in minutes"), - tags: z.array(z.string()).describe( - "List of tags (e.g., ['vegan', 'dessert'])", - ), - notes: z.array(z.string()).describe("Optional notes about the recipe"), + name: z.string(), + content: z.object({ + _type: z.literal("Recipe"), + name: z.string().describe( + "Title of the Recipe, without the name of the website or author", + ), + description: z.string().describe( + "Optional, short description of the recipe", + ), + image: z.string().describe("URL of the main image of the recipe"), + author: z.object({ + _type: z.literal("Person"), + name: z.string().describe("author of the Recipe (optional)"), + }), + recipeEngredient: z.array(z.string()) + .describe("List of ingredients"), + recipeInstructions: z.array(z.string()).describe("List of instructions"), + recipeYield: z.number().describe("Amount of Portions"), + prepTime: z.number().describe("Preparation time in minutes"), + cookTime: z.number().describe("Cooking time in minutes"), + }), }); +export type Recipe = z.infer; + const noRecipeSchema = z.object({ errorMessages: z.array(z.string()).describe( "List of error messages, if no recipe was found", @@ -46,12 +52,13 @@ export const recipeResponseSchema = z.union([recipeSchema, noRecipeSchema]); export function isValidRecipe( recipe: - | { ingredients?: unknown[]; instructions?: string[]; name?: string } + | Recipe | null | undefined, ) { - return recipe?.ingredients?.length && recipe.ingredients.length > 1 && - recipe?.instructions?.length && + return recipe?.content?.recipeIngredient?.length && + recipe?.content?.recipeIngredient.length > 1 && + recipe?.content?.recipeInstructions?.length && recipe.name?.length; } diff --git a/lib/resource/articles.ts b/lib/resource/articles.ts index 8b76558..038a210 100644 --- a/lib/resource/articles.ts +++ b/lib/resource/articles.ts @@ -1,9 +1,3 @@ -import { parseDocument } from "@lib/documents.ts"; -import { parse, stringify } from "@std/yaml"; -import { createCrud } from "@lib/crud.ts"; -import { extractHashTags, formatDate } from "@lib/string.ts"; -import { fixRenderedMarkdown } from "@lib/helpers.ts"; - export type Article = { id: string; type: "article"; @@ -21,88 +15,3 @@ export type Article = { rating?: number; }; }; - -function renderArticle(article: Article) { - const meta = article.meta; - if ("date" in meta) { - meta.date = formatDate(meta.date); - } - - return fixRenderedMarkdown(`${meta - ? `--- -${stringify(meta)} ----` - : `--- ----` - } -# ${article.name} -${article.tags.map((t) => `#${t}`).join(" ")} -${article.content} -`); -} - -function parseArticle(original: string, id: string): Article { - const doc = parseDocument(original); - - let meta = {} as Article["meta"]; - let name = ""; - - const range = [Infinity, -Infinity]; - - for (const child of doc.children) { - if (child.type === "yaml") { - try { - meta = parse(child.value) as Article["meta"]; - } catch (err) { - console.log("Error parsing YAML", err); - console.log("YAML:", child.value); - } - - if (meta["rating"] && typeof meta["rating"] === "string") { - meta.rating = [...meta.rating?.matchAll("⭐")].length; - } - - 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 (name) { - const start = child.position?.start.offset || Infinity; - const end = child.position?.end.offset || -Infinity; - if (start < range[0]) range[0] = start; - if (end > range[1]) range[1] = end; - } - } - - let content = original.slice(range[0], range[1]); - const tags = extractHashTags(content); - for (const tag of tags) { - content = content.replace("#" + tag, ""); - } - - return { - type: "article", - id, - name, - tags, - content, - meta, - }; -} - -const crud = createCrud
({ - prefix: "Media/articles/", - parse: parseArticle, - render: renderArticle, - hasThumbnails: true, -}); -export const getAllArticles = crud.readAll; -export const getArticle = crud.read; -export const createArticle = crud.create; diff --git a/lib/resource/movies.ts b/lib/resource/movies.ts index 2b86b62..ed05851 100644 --- a/lib/resource/movies.ts +++ b/lib/resource/movies.ts @@ -1,9 +1,3 @@ -import { parseDocument } from "@lib/documents.ts"; -import { parse, stringify } from "yaml"; -import { createCrud } from "@lib/crud.ts"; -import { extractHashTags, formatDate } from "@lib/string.ts"; -import { fixRenderedMarkdown } from "@lib/helpers.ts"; - export type Movie = { id: string; name: string; @@ -21,103 +15,3 @@ export type Movie = { rating: number; }; }; - -export function renderMovie(movie: Movie) { - const meta = movie.meta; - if ("date" in meta && typeof meta.date !== "string") { - meta.date = formatDate(meta.date) as unknown as Date; - } - - delete meta.thumbnail; - delete meta.average; - - const movieImage = `![](${movie.meta.image})`; - - return fixRenderedMarkdown(`${ - meta - ? `--- -${stringify(meta)} ----` - : `--- ----` - } -# ${movie.name} -${ - // So we do not add a new image to the description everytime we render - (movie.meta.image && !movie.description.includes(movieImage)) - ? movieImage - : ""} -${movie.tags.map((t) => `#${t}`).join(" ")} -${movie.description} -`); -} - -export function parseMovie(original: string, id: string): Movie { - const doc = parseDocument(original); - - let meta = {} as Movie["meta"]; - let name = ""; - - const range = [Infinity, -Infinity]; - - for (const child of doc.children) { - if (child.type === "yaml") { - try { - meta = (parse(child.value) || {}) as Movie["meta"]; - } catch (_) { - // ignore here - } - - if (meta["rating"] && typeof meta["rating"] === "string") { - meta.rating = [...meta.rating?.matchAll("⭐")].length; - } - - 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 (name) { - const start = child.position?.start.offset || Infinity; - const end = child.position?.end.offset || -Infinity; - if (start < range[0]) range[0] = start; - if (end > range[1]) range[1] = end; - } - } - - let description = original.slice(range[0], range[1]); - const tags = extractHashTags(description); - for (const tag of tags) { - description = description.replace("#" + tag, ""); - } - - return { - type: "movie", - id, - name, - tags, - description, - meta, - }; -} - -const crud = createCrud({ - prefix: "Media/movies/", - parse: parseMovie, - render: renderMovie, - hasThumbnails: true, -}); - -export const getMovie = async (id: string) => { - const movie = await crud.read(id); - return movie; -}; - -export const getAllMovies = crud.readAll; -export const createMovie = crud.create; diff --git a/lib/resource/recipes.ts b/lib/resource/recipes.ts index bb737a7..41f64ed 100644 --- a/lib/resource/recipes.ts +++ b/lib/resource/recipes.ts @@ -1,15 +1,3 @@ -import { - type DocumentChild, - getTextOfRange, - parseDocument, -} from "@lib/documents.ts"; -import { parse, stringify } from "yaml"; -import { createCrud } from "@lib/crud.ts"; -import { extractHashTags } from "@lib/string.ts"; -import { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts"; -import { fixRenderedMarkdown } from "@lib/helpers.ts"; -import { parseIngredients } from "@lib/parseIngredient.ts"; - export type Recipe = { type: "recipe"; id: string; @@ -31,179 +19,3 @@ export type Recipe = { thumbnail?: string; }; }; - -function extractSteps( - content: string, - seperator: RegExp = /\n(?=\d+\.)/g, -): string[] { - const steps = content.split(seperator).map((step) => { - const match = step.match(/^(\d+)\.\s*(.*)/); - if (match) return match[2]; - return step; - }).filter((step) => !!step); - return steps as string[]; -} - -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") { - try { - meta = parse(child.value) as Recipe["meta"]; - } catch (err) { - console.log("Error parsing YAML", err); - } - 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); - } - - let description = getTextOfRange(groups[0], original); - - let ingredientsText = getTextOfRange(groups[1], original); - if (ingredientsText) { - ingredientsText = ingredientsText.replace(/#+\s?Ingredients?/, ""); - } else { - ingredientsText = ""; - } - - const ingredients = parseIngredients(ingredientsText); - - const instructionText = getTextOfRange(groups[2], original); - let instructions = extractSteps(instructionText || ""); - if (instructions.length <= 1) { - const d = extractSteps(instructionText || "", /\n/g); - if (d.length > instructions.length) { - instructions = d; - } - } - - const tags = extractHashTags(description || ""); - if (description) { - for (const tag of tags) { - description = description.replace("#" + tag, ""); - } - } - - return { - type: "recipe", - id, - meta, - name, - tags, - markdown: original, - notes: getTextOfRange(groups[3], original)?.split("\n"), - description, - ingredients, - instructions, - }; -} - -function filterUndefinedFromObject( - obj: T, -) { - return Object.fromEntries( - Object.entries(obj).filter(([_, v]) => v !== undefined), - ); -} - -export function renderRecipe(recipe: Recipe) { - const meta = filterUndefinedFromObject(recipe.meta || {}); - - // Clean up meta properties - delete meta.thumbnail; - delete meta.average; - - const recipeImage = meta.image ? `![](${meta.image})` : ""; - - // Format ingredient groups and standalone ingredients - const ingredients = recipe.ingredients - .map((item) => { - if ("items" in item) { - return `\n*${item.name}*\n${ - item.items - .map((ing) => { - if (ing.quantity && ing.unit) { - return `- **${ing.quantity.trim() || ""}${ - ing.unit.trim() || "" - }** ${ing.name}`; - } - return `- ${ing.name}`; - }) - .join("\n") - }`; - } - if (item.quantity && item.unit) { - return `- **${item.quantity?.trim() || ""}${ - item.unit?.trim() || "" - }** ${item.name}`; - } - - if (item.quantity) { - return `- **${item.quantity}** ${item.name}`; - } - - return `- ${item.name}`; - }) - .join("\n"); - - // Format instructions as a numbered list - const instructions = recipe.instructions - ? recipe.instructions.map((step, i) => `${i + 1}. ${step}`).join("\n") - : ""; - - // Render the final markdown - return fixRenderedMarkdown(`${ - Object.keys(meta).length - ? `--- -${stringify(meta)} ----` - : `--- ----` - } -# ${recipe.name} -${recipe.meta?.image ? recipeImage : ""} -${recipe.tags.map((t) => `#${t.replaceAll(" ", "-")}`).join(" ")} -${recipe.description || ""} - ---- - -${ingredients ? `## Ingredients\n\n${ingredients}\n\n---\n` : ""} -${instructions ? `${instructions}\n\n---` : ""} -${recipe.notes?.length ? `\n${recipe.notes.join("\n")}` : ""} -`); -} - -const crud = createCrud({ - prefix: `Recipes/`, - parse: parseRecipe, - render: renderRecipe, - hasThumbnails: true, -}); - -export const getAllRecipes = crud.readAll; -export const getRecipe = crud.read; -export const updateRecipe = crud.update; -export const createRecipe = crud.create; diff --git a/lib/resource/series.ts b/lib/resource/series.ts index ff8488b..eb7a800 100644 --- a/lib/resource/series.ts +++ b/lib/resource/series.ts @@ -1,9 +1,3 @@ -import { parseDocument } from "@lib/documents.ts"; -import { parse, stringify } from "yaml"; -import { createCrud } from "@lib/crud.ts"; -import { extractHashTags, formatDate } from "@lib/string.ts"; -import { fixRenderedMarkdown } from "@lib/helpers.ts"; - export type Series = { id: string; name: string; @@ -21,99 +15,3 @@ export type Series = { done?: boolean; }; }; - -function renderSeries(series: Series) { - const meta = series.meta; - if ("date" in meta) { - meta.date = formatDate(meta.date); - } - - delete meta.thumbnail; - delete meta.average; - - const movieImage = `![](${series.meta.image})`; - - return fixRenderedMarkdown(`${ - meta - ? `--- -${stringify(meta)} ----` - : `--- ----` - } -# ${series.name} -${ - // So we do not add a new image to the description everytime we render - (series.meta.image && !series.description.includes(movieImage)) - ? movieImage - : ""} -${series.tags.map((t) => `#${t}`).join(" ")} -${series.description} -`); -} - -export function parseSeries(original: string, id: string): Series { - const doc = parseDocument(original); - - let meta = {} as Series["meta"]; - let name = ""; - - const range = [Infinity, -Infinity]; - - for (const child of doc.children) { - if (child.type === "yaml") { - try { - meta = (parse(child.value) || {}) as Series["meta"]; - } catch (_) { - // ignore here - } - - if (meta["rating"] && typeof meta["rating"] === "string") { - meta.rating = [...meta.rating?.matchAll("⭐")].length; - } - - 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 (name) { - const start = child.position?.start.offset || Infinity; - const end = child.position?.end.offset || -Infinity; - if (start < range[0]) range[0] = start; - if (end > range[1]) range[1] = end; - } - } - - let description = original.slice(range[0], range[1]); - const tags = extractHashTags(description); - for (const tag of tags) { - description = description.replace("#" + tag, ""); - } - - return { - type: "series", - id, - name, - tags, - description, - meta, - }; -} - -const crud = createCrud({ - prefix: "Media/series/", - parse: parseSeries, - render: renderSeries, - hasThumbnails: true, -}); - -export const getSeries = crud.read; -export const getAllSeries = crud.readAll; -export const createSeries = crud.create; diff --git a/lib/resources.ts b/lib/resources.ts index f7ec601..c532332 100644 --- a/lib/resources.ts +++ b/lib/resources.ts @@ -30,3 +30,14 @@ export const resources = { prefix: "Media/series/", }, } as const; + +export async function fetchResource(resource: string) { + try { + const response = await fetch( + `https://marka.max-richter.dev/resources/${resource}`, + ); + return response.json(); + } catch (_e) { + return []; + } +} diff --git a/lib/search.ts b/lib/search.ts index b89f338..d9b87c5 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -2,10 +2,11 @@ import { resources } from "@lib/resources.ts"; import fuzzysort from "npm:fuzzysort"; import { GenericResource } from "@lib/types.ts"; import { extractHashTags } from "@lib/string.ts"; -import { getAllMovies, Movie } from "@lib/resource/movies.ts"; -import { Article, getAllArticles } from "@lib/resource/articles.ts"; -import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; -import { getAllSeries, Series } from "@lib/resource/series.ts"; +import { Movie } from "@lib/resource/movies.ts"; +import { Article } from "@lib/resource/articles.ts"; +import { Recipe } from "@lib/resource/recipes.ts"; +import { Series } from "@lib/resource/series.ts"; +import { fetchResource } from "./resources.ts"; type ResourceType = keyof typeof resources; @@ -56,10 +57,10 @@ export async function searchResource( { q, tags = [], types, rating }: SearchParams, ): Promise { const resources = (await Promise.all([ - (!types || types.includes("movie")) && getAllMovies(), - (!types || types.includes("series")) && getAllSeries(), - (!types || types.includes("article")) && getAllArticles(), - (!types || types.includes("recipe")) && getAllRecipes(), + (!types || types.includes("movie")) && fetchResource("movies"), + (!types || types.includes("series")) && fetchResource("series"), + (!types || types.includes("article")) && fetchResource("articles"), + (!types || types.includes("recipe")) && fetchResource("recipes"), ])).flat().filter(isResource); const results: Record = {}; diff --git a/main.ts b/main.ts index 768122e..248e889 100644 --- a/main.ts +++ b/main.ts @@ -7,6 +7,6 @@ import { start } from "$fresh/server.ts"; import manifest from "./fresh.gen.ts"; import config from "./fresh.config.ts"; -import "@lib/telegram.ts"; +// import "@lib/telegram.ts"; await start(manifest, config); diff --git a/routes/api/articles/[name].ts b/routes/api/articles/[name].ts index 8616d12..01f8e5b 100644 --- a/routes/api/articles/[name].ts +++ b/routes/api/articles/[name].ts @@ -1,10 +1,10 @@ import { Handlers } from "$fresh/server.ts"; -import { getArticle } from "@lib/resource/articles.ts"; import { json } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET(_, ctx) { - const article = await getArticle(ctx.params.name); + const article = await fetchResource(`articles/${ctx.params.name}`); return json(article); }, }; diff --git a/routes/api/articles/create/index.ts b/routes/api/articles/create/index.ts index ee9e745..f2cfbc2 100644 --- a/routes/api/articles/create/index.ts +++ b/routes/api/articles/create/index.ts @@ -6,7 +6,7 @@ import { createStreamResponse, isValidUrl } from "@lib/helpers.ts"; import * as openai from "@lib/openai.ts"; import tds from "https://cdn.skypack.dev/turndown@7.2.0"; -import { Article, createArticle } from "@lib/resource/articles.ts"; +import { Article } from "@lib/resource/articles.ts"; import { getYoutubeVideoDetails } from "@lib/youtube.ts"; import { extractYoutubeId, @@ -170,7 +170,7 @@ async function processCreateArticle( streamResponse.enqueue("writing to disk"); - await createArticle(newArticle.id, newArticle); + // await createArticle(newArticle.id, newArticle); streamResponse.enqueue("id: " + newArticle.id); } @@ -210,7 +210,7 @@ async function processCreateYoutubeVideo( streamResponse.enqueue("creating article"); - await createArticle(newArticle.id, newArticle); + // await createArticle(newArticle.id, newArticle); streamResponse.enqueue("finished"); diff --git a/routes/api/articles/index.ts b/routes/api/articles/index.ts index 513c24b..c8772e8 100644 --- a/routes/api/articles/index.ts +++ b/routes/api/articles/index.ts @@ -1,10 +1,10 @@ import { Handlers } from "$fresh/server.ts"; -import { getAllArticles } from "@lib/resource/articles.ts"; import { json } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET() { - const movies = await getAllArticles(); - return json(movies); + const articles = await fetchResource("articles"); + return json(articles?.content); }, }; diff --git a/routes/api/images/index.ts b/routes/api/images/index.ts index 873e36a..27df52f 100644 --- a/routes/api/images/index.ts +++ b/routes/api/images/index.ts @@ -1,8 +1,6 @@ import { FreshContext, Handlers } from "$fresh/server.ts"; import { getImageContent } from "@lib/image.ts"; -import { SILVERBULLET_SERVER } from "@lib/env.ts"; import { createLogger } from "@lib/log/index.ts"; -import { isLocalImage } from "@lib/string.ts"; const log = createLogger("api/image"); @@ -64,11 +62,10 @@ function parseParams(reqUrl: URL): ImageParams | string { // Helper function to generate ETag async function generateETag(content: ArrayBuffer): Promise { const hashBuffer = await crypto.subtle.digest("SHA-256", content); - return `"${ - Array.from(new Uint8Array(hashBuffer)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - }"`; + return `"${Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + }"`; } async function GET(req: Request, _ctx: FreshContext): Promise { @@ -83,8 +80,8 @@ async function GET(req: Request, _ctx: FreshContext): Promise { }); } - const imageUrl = isLocalImage(params.image) - ? `${SILVERBULLET_SERVER}/${params.image.replace(/^\//, "")}` + const imageUrl = params.image.startsWith("resources") + ? `https://marka.max-richter.dev/${params.image.replace(/^\//, "")}` : params.image; log.debug("Processing image request:", { imageUrl, params }); diff --git a/routes/api/index.ts b/routes/api/index.ts index e205871..fdf544d 100644 --- a/routes/api/index.ts +++ b/routes/api/index.ts @@ -1,10 +1,8 @@ import { Handlers } from "$fresh/server.ts"; -import { getDocuments } from "@lib/documents.ts"; import { json } from "@lib/helpers.ts"; export const handler: Handlers = { async GET() { - const documents = await getDocuments(); - return json(documents); + return json([]); }, }; diff --git a/routes/api/movies/[name].ts b/routes/api/movies/[name].ts index 80e45fa..71cd78f 100644 --- a/routes/api/movies/[name].ts +++ b/routes/api/movies/[name].ts @@ -1,16 +1,16 @@ import { Handlers } from "$fresh/server.ts"; -import { createMovie, getMovie, Movie } from "@lib/resource/movies.ts"; +import { Movie } from "@lib/resource/movies.ts"; import { json } from "@lib/helpers.ts"; import * as tmdb from "@lib/tmdb.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; import { isString, safeFileName } from "@lib/string.ts"; -import { createDocument } from "@lib/documents.ts"; import { AccessDeniedError } from "@lib/errors.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET(_, ctx) { - const movie = await getMovie(ctx.params.name); - return json(movie); + const movie = await fetchResource(`movies/${ctx.params.name}`); + return json(movie?.content); }, async POST(_, ctx) { const session = ctx.state.session; diff --git a/routes/api/movies/enhance/[name].ts b/routes/api/movies/enhance/[name].ts index bcaf103..6d54cc5 100644 --- a/routes/api/movies/enhance/[name].ts +++ b/routes/api/movies/enhance/[name].ts @@ -1,7 +1,5 @@ import { FreshContext, Handlers } from "$fresh/server.ts"; -import { createDocument } from "@lib/documents.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; -import { createMovie, getMovie } from "@lib/resource/movies.ts"; import * as tmdb from "@lib/tmdb.ts"; import { isString, safeFileName } from "@lib/string.ts"; import { json } from "@lib/helpers.ts"; @@ -11,6 +9,7 @@ import { NotFoundError, } from "@lib/errors.ts"; import { createRecommendationResource } from "@lib/recommendation.ts"; +import { fetchResource } from "@lib/resources.ts"; const POST = async ( req: Request, @@ -21,7 +20,7 @@ const POST = async ( throw new AccessDeniedError(); } - const movie = await getMovie(ctx.params.name); + const movie = await fetchResource(`movies/${ctx.params.name}`); if (!movie) { throw new NotFoundError(); } @@ -72,12 +71,12 @@ const POST = async ( const poster = await tmdb.getMoviePoster(posterPath); const extension = fileExtension(posterPath); finalPath = `Media/movies/images/${safeFileName(name)}_cover.${extension}`; - await createDocument(finalPath, poster); + // await createDocument(finalPath, poster); movie.meta = movie.meta || {}; movie.meta.image = finalPath; } - await createMovie(movie.id, movie); + // await createMovie(movie.id, movie); createRecommendationResource(movie, movieDetails.overview); diff --git a/routes/api/movies/index.ts b/routes/api/movies/index.ts index 868b2c5..791aea9 100644 --- a/routes/api/movies/index.ts +++ b/routes/api/movies/index.ts @@ -1,10 +1,10 @@ import { Handlers } from "$fresh/server.ts"; -import { getAllMovies } from "@lib/resource/movies.ts"; import { json } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET() { - const movies = await getAllMovies(); + const movies = await fetchResource("movies"); return json(movies); }, }; diff --git a/routes/api/recipes/[name].ts b/routes/api/recipes/[name].ts index 2c92669..a8eb3e1 100644 --- a/routes/api/recipes/[name].ts +++ b/routes/api/recipes/[name].ts @@ -1,10 +1,10 @@ import { Handlers } from "$fresh/server.ts"; -import { getRecipe } from "@lib/resource/recipes.ts"; import { json } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET(_, ctx) { - const recipe = await getRecipe(ctx.params.name); + const recipe = await fetchResource(`recipes/${ctx.params.name}`); return json(recipe); }, }; diff --git a/routes/api/recipes/create/index.ts b/routes/api/recipes/create/index.ts index 24669a3..3ab0d4c 100644 --- a/routes/api/recipes/create/index.ts +++ b/routes/api/recipes/create/index.ts @@ -6,8 +6,8 @@ import { createStreamResponse, isValidUrl } from "@lib/helpers.ts"; import * as openai from "@lib/openai.ts"; import tds from "https://cdn.skypack.dev/turndown@7.2.0"; import { createLogger } from "@lib/log/index.ts"; -import { createRecipe, Recipe } from "@lib/resource/recipes.ts"; -import recipeSchema, { isValidRecipe } from "@lib/recipeSchema.ts"; +import { Recipe } from "@lib/resource/recipes.ts"; +import recipeSchema from "@lib/recipeSchema.ts"; import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; import { safeFileName } from "@lib/string.ts"; import { createDocument } from "@lib/documents.ts"; @@ -205,10 +205,10 @@ async function processCreateRecipeFromUrl( streamResponse.enqueue("downloading image"); try { streamResponse.enqueue("downloading image"); - const res = await fetch(src); + // const res = await fetch(src); streamResponse.enqueue("saving image"); - const buffer = await res.arrayBuffer(); - await createDocument(finalPath, buffer); + // const buffer = await res.arrayBuffer(); + // await createDocument(finalPath, buffer); newRecipe.meta.image = finalPath; } catch (err) { console.log("Failed to save image", err); @@ -218,7 +218,7 @@ async function processCreateRecipeFromUrl( streamResponse.enqueue("finished processing, creating file"); - await createRecipe(newRecipe.id, newRecipe); + // await createRecipe(newRecipe.id, newRecipe); streamResponse.enqueue("id: " + newRecipe.id); } diff --git a/routes/api/recipes/index.ts b/routes/api/recipes/index.ts index c93b8a1..2b3cfd9 100644 --- a/routes/api/recipes/index.ts +++ b/routes/api/recipes/index.ts @@ -1,10 +1,10 @@ import { Handlers } from "$fresh/server.ts"; -import { getAllRecipes } from "@lib/resource/recipes.ts"; import { json } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET() { - const recipes = await getAllRecipes(); + const recipes = await fetchResource("recipes"); return json(recipes); }, }; diff --git a/routes/api/recommendation/all.ts b/routes/api/recommendation/all.ts index 88f4255..06e4fc3 100644 --- a/routes/api/recommendation/all.ts +++ b/routes/api/recommendation/all.ts @@ -1,22 +1,23 @@ import { Handlers } from "$fresh/server.ts"; import { createStreamResponse } from "@lib/helpers.ts"; -import { getAllMovies, Movie } from "@lib/resource/movies.ts"; +import { Movie } from "@lib/resource/movies.ts"; import * as tmdb from "@lib/tmdb.ts"; import { createRecommendationResource, getRecommendation, } from "@lib/recommendation.ts"; import { AccessDeniedError } from "@lib/errors.ts"; +import { fetchResource } from "@lib/resources.ts"; async function processUpdateRecommendations( streamResponse: ReturnType, ) { - const allMovies = await getAllMovies(); + const allMovies = await fetchResource("movies"); - const movies = allMovies.filter((m) => { - if (!m?.meta) return false; - if (!m.meta.rating) return false; - if (!m.meta.tmdbId) return false; + const movies = allMovies?.content.filter((m) => { + if (!m?.content) return false; + if (!m.content.reviewRating) return false; + if (!m.content.tmdbId) return false; return true; }) as Movie[]; diff --git a/routes/api/series/[name].ts b/routes/api/series/[name].ts index 16ca4ad..c9b40e3 100644 --- a/routes/api/series/[name].ts +++ b/routes/api/series/[name].ts @@ -5,11 +5,12 @@ import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts" import { isString, safeFileName } from "@lib/string.ts"; import { createDocument } from "@lib/documents.ts"; import { AccessDeniedError } from "@lib/errors.ts"; -import { createSeries, getSeries, Series } from "@lib/resource/series.ts"; +import { Series } from "@lib/resource/series.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers = { async GET(_, ctx) { - const series = await getSeries(ctx.params.name); + const series = await fetchResource(`series/${ctx.params.name}`); return json(series); }, async POST(_, ctx) { diff --git a/routes/api/series/enhance/[name].ts b/routes/api/series/enhance/[name].ts index 7c90fcc..9686a51 100644 --- a/routes/api/series/enhance/[name].ts +++ b/routes/api/series/enhance/[name].ts @@ -9,7 +9,6 @@ import { BadRequestError, NotFoundError, } from "@lib/errors.ts"; -import { createSeries, getSeries } from "@lib/resource/series.ts"; const isString = (input: string | undefined): input is string => { return typeof input === "string"; @@ -64,15 +63,15 @@ const POST = async ( let finalPath = ""; if (posterPath && !series.meta?.image) { - const poster = await tmdb.getMoviePoster(posterPath); + // const poster = await tmdb.getMoviePoster(posterPath); const extension = fileExtension(posterPath); finalPath = `Media/series/images/${safeFileName(name)}_cover.${extension}`; - await createDocument(finalPath, poster); + // await createDocument(finalPath, poster); series.meta = series.meta || {}; series.meta.image = finalPath; } - await createSeries(series.id, series); + // await createSeries(series.id, series); return json(series); }; diff --git a/routes/articles/[name].tsx b/routes/articles/[name].tsx index 47f916e..83e9505 100644 --- a/routes/articles/[name].tsx +++ b/routes/articles/[name].tsx @@ -1,6 +1,6 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Article, getArticle } from "@lib/resource/articles.ts"; +import { Article } from "@lib/resource/articles.ts"; import { KMenu } from "@islands/KMenu.tsx"; import { YoutubePlayer } from "@components/Youtube.tsx"; import { HashTags } from "@components/HashTags.tsx"; @@ -10,10 +10,11 @@ import { RedirectSearchHandler } from "@islands/Search.tsx"; import PageHero from "@components/PageHero.tsx"; import { Star } from "@components/Stars.tsx"; import { MetaTags } from "@components/MetaTags.tsx"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers<{ article: Article; session: unknown }> = { async GET(_, ctx) { - const article = await getArticle(ctx.params.name); + const article = await fetchResource(`articles/${ctx.params.name}.md`); if (!article) { return ctx.renderNotFound(); } @@ -26,34 +27,39 @@ export default function Greet( ) { const { article, session } = props.data; - const { author = "", date = "" } = article.meta; + const { author = "", date = "", articleBody = "" } = article?.content || {}; const content = renderMarkdown( - removeImage(article.content, article.meta.image), + removeImage(articleBody, article.content.image), ); + console.log({ article }); + return ( ${article.name}`} + title={`Article > ${article.content.headline}`} context={article} > - + {session && ( )} - - {article.name} + + {article.content.headline} - {article.meta.rating && } + {article.content.rating && } - {article.tags.length > 0 && ( + {article.content?.tags?.length > 0 && ( <>
- + )}
- {isYoutubeLink(article.meta.link) && ( - + {isYoutubeLink(article.content.url) && ( + )}
-          {content||""}
+          {content || ""}
         
diff --git a/routes/articles/index.tsx b/routes/articles/index.tsx index 10550b1..e292034 100644 --- a/routes/articles/index.tsx +++ b/routes/articles/index.tsx @@ -1,6 +1,6 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Article, getAllArticles } from "@lib/resource/articles.ts"; +import { Article } from "@lib/resource/articles.ts"; import { KMenu } from "@islands/KMenu.tsx"; import { Grid } from "@components/Grid.tsx"; import { IconArrowLeft } from "@components/icons.tsx"; @@ -9,12 +9,14 @@ import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { GenericResource } from "@lib/types.ts"; import { ResourceCard } from "@components/Card.tsx"; import { Link } from "@islands/Link.tsx"; +import { fetchResource } from "@lib/resources.ts"; + export const handler: Handlers< { articles: Article[] | null; searchResults?: GenericResource[] } > = { async GET(req, ctx) { - const articles = await getAllArticles(); + const { content: articles } = await fetchResource("articles"); const searchParams = parseResourceUrl(req.url); const searchResults = searchParams && await searchResource({ ...searchParams, types: ["article"] }); diff --git a/routes/index.tsx b/routes/index.tsx index 7ac49b8..3a99311 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -4,7 +4,7 @@ import { PageProps } from "$fresh/server.ts"; import { resources } from "@lib/resources.ts"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { KMenu } from "@islands/KMenu.tsx"; -import "@lib/telegram.ts"; +// import "@lib/telegram.ts"; export default function Home(props: PageProps) { return ( @@ -22,11 +22,10 @@ export default function Home(props: PageProps) { ); diff --git a/routes/movies/[name].tsx b/routes/movies/[name].tsx index 31fce9b..8607149 100644 --- a/routes/movies/[name].tsx +++ b/routes/movies/[name].tsx @@ -1,7 +1,6 @@ import { PageProps, RouteContext } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; -import { getMovie, Movie } from "@lib/resource/movies.ts"; -import { HashTags } from "@components/HashTags.tsx"; +import { Movie } from "@lib/resource/movies.ts"; import { removeImage, renderMarkdown } from "@lib/documents.ts"; import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; @@ -9,22 +8,24 @@ import { Recommendations } from "@islands/Recommendations.tsx"; import PageHero from "@components/PageHero.tsx"; import { Star } from "@components/Stars.tsx"; import { MetaTags } from "@components/MetaTags.tsx"; +import { parseRating } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export default async function Greet( props: PageProps<{ movie: Movie; session: Record }>, ctx: RouteContext, ) { - const movie = await getMovie(ctx.params.name); + const movie = await fetchResource(`movies/${ctx.params.name}.md`); const session = ctx.state.session; if (!movie) { return ctx.renderNotFound(); } - const { author = "", date = "" } = movie.meta; + const { author = "", date = "" } = movie.content; const content = renderMarkdown( - removeImage(movie.description || "", movie.meta.image), + removeImage(movie.content.reviewBody || "", movie.content.image), ); return ( @@ -33,14 +34,14 @@ export default async function Greet( {session && ( )} @@ -49,13 +50,17 @@ export default async function Greet( - {movie.meta.rating && } + {movie.content.reviewRating && ( + + )} @@ -65,14 +70,8 @@ export default async function Greet( type="movie" /> )} - {movie.tags.length > 0 && ( - <> -
- - - )}
- {movie?.description?.length > 80 + {movie?.content?.reviewBody?.length > 80 ?

Review

: <>}
,
 ) {
-  const allMovies = await getAllMovies();
+  const { content: allMovies } = await fetchResource("movies");
   const searchParams = parseResourceUrl(props.url);
   const searchResults = searchParams &&
     await searchResource({ ...searchParams, types: ["movie"] });
   const movies = allMovies.sort((a, b) =>
-    a?.meta?.rating > b?.meta?.rating ? -1 : 1
+    a?.content?.reviewRating?.ratingValue > b?.content?.reviewRating?.ratingValue ? -1 : 1
   );
 
   return (
diff --git a/routes/recipes/[name].tsx b/routes/recipes/[name].tsx
index 23d04e1..4125be5 100644
--- a/routes/recipes/[name].tsx
+++ b/routes/recipes/[name].tsx
@@ -3,7 +3,7 @@ import { IngredientsList } from "@islands/IngredientsList.tsx";
 import { MainLayout } from "@components/layouts/main.tsx";
 import Counter from "@islands/Counter.tsx";
 import { Signal, useSignal } from "@preact/signals";
-import { getRecipe, Recipe } from "@lib/resource/recipes.ts";
+import { Recipe } from "@lib/recipeSchema.ts";
 import { RedirectSearchHandler } from "@islands/Search.tsx";
 import { KMenu } from "@islands/KMenu.tsx";
 import PageHero from "@components/PageHero.tsx";
@@ -11,11 +11,12 @@ import { Star } from "@components/Stars.tsx";
 import { renderMarkdown } from "@lib/documents.ts";
 import { isValidRecipe } from "@lib/recipeSchema.ts";
 import { MetaTags } from "@components/MetaTags.tsx";
+import { fetchResource } from "@lib/resources.ts";
 
 export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
   async GET(_, ctx) {
     try {
-      const recipe = await getRecipe(ctx.params.name);
+      const recipe = await fetchResource(`recipes/${ctx.params.name}.md`);
       if (!recipe) {
         return ctx.renderNotFound();
       }
@@ -38,22 +39,24 @@ function ValidRecipe({
         {portion && }
       

Preparation

    - {recipe.instructions && (recipe.instructions.map((instruction) => { - return ( -
  1. - ); - }))} + {recipe.content.recipeInstructions && + (recipe.content.recipeInstructions.filter((inst) => !!inst?.length) + .map((instruction) => { + return ( +
  2. + ); + }))}
@@ -65,35 +68,38 @@ export default function Page( ) { const { recipe, session } = props.data; - const portion = recipe.meta?.portion; + const portion = recipe.recipeYield; const amount = useSignal(portion || 1); const subline = [ - recipe?.meta?.time && `Duration ${recipe.meta.time}`, + recipe?.content?.prepTime && `Duration ${recipe?.content?.prepTime}`, ].filter(Boolean) as string[]; return ( ${recipe.name}`} + title={`Recipes > ${recipe.content?.name}`} context={recipe} > - + {session && ( )} - - {recipe.name} + + {recipe.content.name} ) : ( -
+
+ {JSON.stringify(recipe)} +
)}
diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index dae1384..a14a064 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -1,19 +1,20 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; -import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; +import { Recipe } from "@lib/recipeSchema.ts"; import { Grid } from "@components/Grid.tsx"; import { IconArrowLeft } from "@components/icons.tsx"; import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; -import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { GenericResource } from "@lib/types.ts"; import { ResourceCard } from "@components/Card.tsx"; +import { fetchResource } from "@lib/resources.ts"; +import { parseResourceUrl, searchResource } from "@lib/search.ts"; export const handler: Handlers< { recipes: Recipe[] | null; searchResults?: GenericResource[] } > = { async GET(req, ctx) { - const recipes = await getAllRecipes(); + const { content: recipes } = await fetchResource("recipes"); const searchParams = parseResourceUrl(req.url); const searchResults = searchParams && await searchResource({ ...searchParams, types: ["recipe"] }); @@ -48,8 +49,8 @@ export default function Greet(

🍽️ Recipes

- {recipes?.map((doc) => { - return ; + {recipes?.filter((s) => !!s?.content?.name).map((doc) => { + return ; })} diff --git a/routes/series/[name].tsx b/routes/series/[name].tsx index 251f45a..f10392d 100644 --- a/routes/series/[name].tsx +++ b/routes/series/[name].tsx @@ -2,16 +2,18 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; import { HashTags } from "@components/HashTags.tsx"; import { removeImage, renderMarkdown } from "@lib/documents.ts"; -import { getSeries, Series } from "@lib/resource/series.ts"; +import { Series } from "@lib/resource/series.ts"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { KMenu } from "@islands/KMenu.tsx"; import PageHero from "@components/PageHero.tsx"; import { Star } from "@components/Stars.tsx"; import { MetaTags } from "@components/MetaTags.tsx"; +import { parseRating } from "@lib/helpers.ts"; +import { fetchResource } from "@lib/resources.ts"; export const handler: Handlers<{ serie: Series; session: unknown }> = { async GET(_, ctx) { - const serie = await getSeries(ctx.params.name); + const serie = await fetchResource(`series/${ctx.params.name}`); if (!serie) { return ctx.renderNotFound(); @@ -25,24 +27,28 @@ export default function Greet( ) { const { serie, session } = props.data; - const { author = "", date = "" } = serie.meta; + const { author = "", date = "" } = serie.content; const content = renderMarkdown( - removeImage(serie.description || "", serie.meta.image), + removeImage(serie.description || "", serie.content.image), ); return ( - ${serie.name}`} context={serie}> + ${serie.content.name}`} + context={serie} + > - + {session && ( )} @@ -57,18 +63,22 @@ export default function Greet( date.toString(), ]} > - {serie.meta.rating && } + {serie.content.reviewRating && ( + + )} - {serie.tags.length > 0 && ( + {serie.content?.tags?.length > 0 && ( <>
- + )}
- {serie?.description?.length > 80 + {serie?.content?.reviewBody?.length > 80 ?

Review

: <>}
 = {
   async GET(req, ctx) {
-    const series = await getAllSeries();
+    const { content: series } = await fetchResource("series");
     const searchParams = parseResourceUrl(req.url);
     const searchResults = searchParams &&
       await searchResource({ ...searchParams, types: ["series"] });