feat: initial refactor to use marka as backend
This commit is contained in:
@@ -3,6 +3,7 @@ import { IconBrandYoutube } from "@components/icons.tsx";
|
|||||||
import { GenericResource } from "@lib/types.ts";
|
import { GenericResource } from "@lib/types.ts";
|
||||||
import { SmallRating } from "@components/Rating.tsx";
|
import { SmallRating } from "@components/Rating.tsx";
|
||||||
import { Link } from "@islands/Link.tsx";
|
import { Link } from "@islands/Link.tsx";
|
||||||
|
import { parseRating } from "@lib/helpers.ts";
|
||||||
|
|
||||||
export function Card(
|
export function Card(
|
||||||
{
|
{
|
||||||
@@ -96,20 +97,21 @@ export function Card(
|
|||||||
export function ResourceCard(
|
export function ResourceCard(
|
||||||
{ res, sublink = "movies" }: { sublink?: string; res: GenericResource },
|
{ res, sublink = "movies" }: { sublink?: string; res: GenericResource },
|
||||||
) {
|
) {
|
||||||
const { meta: { image } = {} } = res || {};
|
const img = res?.content?.image || res?.content?.cover;
|
||||||
|
|
||||||
const imageUrl = image
|
const imageUrl = img
|
||||||
? `/api/images?image=${image}&width=200&height=200`
|
? `/api/images?image=${img}&width=200&height=200`
|
||||||
: "/placeholder.svg";
|
: "/placeholder.svg";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={res.name}
|
title={res.content?.name || res.content?.itemReviewed?.name || res.content?.headline ||
|
||||||
|
res?.name}
|
||||||
backgroundColor={res.meta?.average}
|
backgroundColor={res.meta?.average}
|
||||||
rating={res.meta?.rating}
|
rating={parseRating(res.content?.reviewRating?.ratingValue)}
|
||||||
thumbnail={res.meta?.thumbnail}
|
thumbnail={res.cover}
|
||||||
image={imageUrl}
|
image={imageUrl}
|
||||||
link={`/${sublink}/${res.id}`}
|
link={`/${sublink}/${res.name.replace(/\.md$/g, "")}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { GenericResource } from "@lib/types.ts";
|
|||||||
import { Head } from "$fresh/runtime.ts";
|
import { Head } from "$fresh/runtime.ts";
|
||||||
|
|
||||||
function generateJsonLd(resource: GenericResource): string {
|
function generateJsonLd(resource: GenericResource): string {
|
||||||
const imageUrl = resource.meta?.image
|
const imageUrl = resource.content?.image
|
||||||
? `/api/images?image=${resource.meta.image}&width=1200`
|
? `/api/images?image=${resource.content.image}&width=1200`
|
||||||
: "/images/og-image.jpg";
|
: "/images/og-image.jpg";
|
||||||
|
|
||||||
const baseSchema: Record<string, unknown> = {
|
const baseSchema: Record<string, unknown> = {
|
||||||
"@context": "https://schema.org",
|
"@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,
|
name: resource.name,
|
||||||
description: resource.content || resource.meta?.average || "",
|
description: resource.content || resource.meta?.average || "",
|
||||||
keywords: resource.tags?.join(", ") || "",
|
keywords: resource.tags?.join(", ") || "",
|
||||||
@@ -45,14 +45,14 @@ function generateJsonLd(resource: GenericResource): string {
|
|||||||
export function MetaTags({ resource }: { resource: GenericResource }) {
|
export function MetaTags({ resource }: { resource: GenericResource }) {
|
||||||
const jsonLd = generateJsonLd(resource);
|
const jsonLd = generateJsonLd(resource);
|
||||||
|
|
||||||
const imageUrl = resource.meta?.image
|
const imageUrl = resource.content?.image
|
||||||
? `/api/images?image=${resource.meta.image}&width=1200`
|
? `/api/images?image=${resource.content.image}&width=1200`
|
||||||
: "/images/og-image.jpg";
|
: "/images/og-image.jpg";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<meta property="og:title" content={resource.name} />
|
<meta property="og:title" content={resource.content?.name} />
|
||||||
<meta property="og:type" content={resource.type} />
|
<meta property="og:type" content={resource.content?._type} />
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property="og:image"
|
||||||
content={imageUrl}
|
content={imageUrl}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Signal } from "@preact/signals";
|
|||||||
import type { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
|
import type { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
|
||||||
import { FunctionalComponent } from "preact";
|
import { FunctionalComponent } from "preact";
|
||||||
import { unitsOfMeasure } from "@lib/parseIngredient.ts";
|
import { unitsOfMeasure } from "@lib/parseIngredient.ts";
|
||||||
|
import { renderMarkdown } from "@lib/documents.ts";
|
||||||
|
|
||||||
function formatAmount(num: number) {
|
function formatAmount(num: number) {
|
||||||
if (num === 0) return "";
|
if (num === 0) return "";
|
||||||
@@ -65,36 +66,16 @@ export const IngredientsList: FunctionalComponent<
|
|||||||
return (
|
return (
|
||||||
<table class="w-full border-collapse table-auto">
|
<table class="w-full border-collapse table-auto">
|
||||||
<tbody>
|
<tbody>
|
||||||
{ingredients.map((item, index) => {
|
{ingredients.filter((s) => !!s?.length).map((item) => {
|
||||||
if ("items" in item) {
|
|
||||||
// Render IngredientGroup
|
|
||||||
const { name, items: groupIngredients } = item as IngredientGroup;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(item) }}>
|
||||||
<tr key={index}>
|
</div>
|
||||||
<td colSpan={3} class="pr-4 py-2 font-italic">{name}</td>
|
|
||||||
</tr>
|
|
||||||
{groupIngredients.map((item, index) => {
|
|
||||||
// Render Ingredient
|
|
||||||
return (
|
|
||||||
<Ingredient
|
|
||||||
key={index}
|
|
||||||
ingredient={item}
|
|
||||||
amount={amount}
|
|
||||||
portion={portion}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
// return (
|
||||||
</>
|
// <Ingredient ingredient={item} amount={amount} portion={portion} />
|
||||||
);
|
// );
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Ingredient ingredient={item} amount={amount} portion={portion} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
149
lib/crud.ts
149
lib/crud.ts
@@ -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<T extends GenericResource>(
|
|
||||||
res: T,
|
|
||||||
): Promise<T> {
|
|
||||||
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<T extends GenericResource>(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<T extends GenericResource>(
|
|
||||||
{ prefix, parse, render, hasThumbnails = false }: {
|
|
||||||
prefix: string;
|
|
||||||
hasThumbnails?: boolean;
|
|
||||||
render?: (doc: T) => string;
|
|
||||||
parse: (doc: string, id: string) => T;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const cache = createCache<T>(`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<T>(sort)).filter((v) => !!v);
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
read,
|
|
||||||
readAll,
|
|
||||||
create,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { drizzle } from "drizzle-orm/libsql/node";
|
|
||||||
import { DATA_DIR } from "@lib/env.ts";
|
import { DATA_DIR } from "@lib/env.ts";
|
||||||
|
import { drizzle } from "drizzle-orm/libsql";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const DB_FILE = "file:" + path.resolve(DATA_DIR, "db.sqlite");
|
const DB_FILE = "file:" + path.resolve(DATA_DIR, "db.sqlite");
|
||||||
|
|||||||
161
lib/documents.ts
161
lib/documents.ts
@@ -1,19 +1,7 @@
|
|||||||
import { unified } from "https://esm.sh/unified@10.1.2";
|
|
||||||
import { render } from "gfm";
|
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-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-bash?no-check";
|
||||||
import "https://esm.sh/prismjs@1.29.0/components/prism-rust?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 = {
|
export type Document = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -24,114 +12,6 @@ export type Document = {
|
|||||||
perm: string;
|
perm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = createLogger("documents");
|
|
||||||
|
|
||||||
export async function getDocuments(): Promise<Document[]> {
|
|
||||||
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<string | undefined> {
|
|
||||||
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) {
|
function removeFrontmatter(doc: string) {
|
||||||
if (doc.trim().startsWith("---")) {
|
if (doc.trim().startsWith("---")) {
|
||||||
return doc.trim().split("---").filter((s) => s.length).slice(1).join("---");
|
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) {
|
export function renderMarkdown(doc: string) {
|
||||||
return render(removeFrontmatter(doc), {
|
return render(removeFrontmatter(doc), {
|
||||||
baseUrl: SILVERBULLET_SERVER,
|
baseUrl: "https://max-richter.dev",
|
||||||
allowMath: true,
|
allowMath: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParsedDocument = ReturnType<typeof parseDocument>;
|
export function createDocument(
|
||||||
export type DocumentChild = ParsedDocument["children"][number];
|
path: string,
|
||||||
|
entry: string,
|
||||||
export function findRangeOfChildren(children: DocumentChild[]) {
|
mimetype = "image/jpeg",
|
||||||
const firstChild = children[0];
|
) {
|
||||||
const lastChild = children.length > 1
|
console.log("creating", { path, entry, mimetype });
|
||||||
? 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_USERNAME = Deno.env.get("PROXY_USERNAME");
|
||||||
export const PROXY_PASSWORD = Deno.env.get("PROXY_PASSWORD");
|
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 TMDB_API_KEY = Deno.env.get("TMDB_API_KEY");
|
||||||
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
export const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
||||||
export const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_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_CLIENT_SECRET = Deno.env.get("GITEA_CLIENT_SECRET");
|
||||||
export const GITEA_REDIRECT_URL = Deno.env.get("GITEA_REDIRECT_URL");
|
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");
|
const duration = Deno.env.get("SESSION_DURATION");
|
||||||
export const SESSION_DURATION = duration ? +duration : (60 * 60 * 24);
|
export const SESSION_DURATION = duration ? +duration : (60 * 60 * 24);
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function debounce<T extends (...args: Parameters<T>) => void>(
|
|||||||
|
|
||||||
export function parseRating(rating: string | number) {
|
export function parseRating(rating: string | number) {
|
||||||
if (typeof rating === "string") {
|
if (typeof rating === "string") {
|
||||||
return [...rating.matchAll(/⭐/)].length;
|
return [...rating.matchAll(/⭐/g)].length;
|
||||||
}
|
}
|
||||||
return rating;
|
return rating;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,25 +17,31 @@ export const IngredientGroupSchema = z.object({
|
|||||||
export type IngredientGroup = z.infer<typeof IngredientGroupSchema>;
|
export type IngredientGroup = z.infer<typeof IngredientGroupSchema>;
|
||||||
|
|
||||||
const recipeSchema = z.object({
|
const recipeSchema = z.object({
|
||||||
title: z.string().describe(
|
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",
|
"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"),
|
image: z.string().describe("URL of the main image of the recipe"),
|
||||||
author: z.string().describe("author of the Recipe (optional)"),
|
author: z.object({
|
||||||
description: z.string().describe("Optional, short description of the recipe"),
|
_type: z.literal("Person"),
|
||||||
ingredients: z.array(z.union([IngredientSchema, IngredientGroupSchema]))
|
name: z.string().describe("author of the Recipe (optional)"),
|
||||||
|
}),
|
||||||
|
recipeEngredient: z.array(z.string())
|
||||||
.describe("List of ingredients"),
|
.describe("List of ingredients"),
|
||||||
instructions: z.array(z.string()).describe("List of instructions"),
|
recipeInstructions: z.array(z.string()).describe("List of instructions"),
|
||||||
servings: z.number().describe("Amount of Portions"),
|
recipeYield: z.number().describe("Amount of Portions"),
|
||||||
prepTime: z.number().describe("Preparation time in minutes"),
|
prepTime: z.number().describe("Preparation time in minutes"),
|
||||||
cookTime: z.number().describe("Cooking 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"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type Recipe = z.infer<typeof recipeSchema>;
|
||||||
|
|
||||||
const noRecipeSchema = z.object({
|
const noRecipeSchema = z.object({
|
||||||
errorMessages: z.array(z.string()).describe(
|
errorMessages: z.array(z.string()).describe(
|
||||||
"List of error messages, if no recipe was found",
|
"List of error messages, if no recipe was found",
|
||||||
@@ -46,12 +52,13 @@ export const recipeResponseSchema = z.union([recipeSchema, noRecipeSchema]);
|
|||||||
|
|
||||||
export function isValidRecipe(
|
export function isValidRecipe(
|
||||||
recipe:
|
recipe:
|
||||||
| { ingredients?: unknown[]; instructions?: string[]; name?: string }
|
| Recipe
|
||||||
| null
|
| null
|
||||||
| undefined,
|
| undefined,
|
||||||
) {
|
) {
|
||||||
return recipe?.ingredients?.length && recipe.ingredients.length > 1 &&
|
return recipe?.content?.recipeIngredient?.length &&
|
||||||
recipe?.instructions?.length &&
|
recipe?.content?.recipeIngredient.length > 1 &&
|
||||||
|
recipe?.content?.recipeInstructions?.length &&
|
||||||
recipe.name?.length;
|
recipe.name?.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
export type Article = {
|
||||||
id: string;
|
id: string;
|
||||||
type: "article";
|
type: "article";
|
||||||
@@ -21,88 +15,3 @@ export type Article = {
|
|||||||
rating?: number;
|
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<Article>({
|
|
||||||
prefix: "Media/articles/",
|
|
||||||
parse: parseArticle,
|
|
||||||
render: renderArticle,
|
|
||||||
hasThumbnails: true,
|
|
||||||
});
|
|
||||||
export const getAllArticles = crud.readAll;
|
|
||||||
export const getArticle = crud.read;
|
|
||||||
export const createArticle = crud.create;
|
|
||||||
|
|||||||
@@ -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 = {
|
export type Movie = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -21,103 +15,3 @@ export type Movie = {
|
|||||||
rating: number;
|
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 = ``;
|
|
||||||
|
|
||||||
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<Movie>({
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -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 = {
|
export type Recipe = {
|
||||||
type: "recipe";
|
type: "recipe";
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,179 +19,3 @@ export type Recipe = {
|
|||||||
thumbnail?: string;
|
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<T extends { [key: string]: unknown }>(
|
|
||||||
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 ? `` : "";
|
|
||||||
|
|
||||||
// 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<Recipe>({
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -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 = {
|
export type Series = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -21,99 +15,3 @@ export type Series = {
|
|||||||
done?: boolean;
|
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 = ``;
|
|
||||||
|
|
||||||
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<Series>({
|
|
||||||
prefix: "Media/series/",
|
|
||||||
parse: parseSeries,
|
|
||||||
render: renderSeries,
|
|
||||||
hasThumbnails: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSeries = crud.read;
|
|
||||||
export const getAllSeries = crud.readAll;
|
|
||||||
export const createSeries = crud.create;
|
|
||||||
|
|||||||
@@ -30,3 +30,14 @@ export const resources = {
|
|||||||
prefix: "Media/series/",
|
prefix: "Media/series/",
|
||||||
},
|
},
|
||||||
} as const;
|
} 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { resources } from "@lib/resources.ts";
|
|||||||
import fuzzysort from "npm:fuzzysort";
|
import fuzzysort from "npm:fuzzysort";
|
||||||
import { GenericResource } from "@lib/types.ts";
|
import { GenericResource } from "@lib/types.ts";
|
||||||
import { extractHashTags } from "@lib/string.ts";
|
import { extractHashTags } from "@lib/string.ts";
|
||||||
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
|
import { Movie } from "@lib/resource/movies.ts";
|
||||||
import { Article, getAllArticles } from "@lib/resource/articles.ts";
|
import { Article } from "@lib/resource/articles.ts";
|
||||||
import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts";
|
import { Recipe } from "@lib/resource/recipes.ts";
|
||||||
import { getAllSeries, Series } from "@lib/resource/series.ts";
|
import { Series } from "@lib/resource/series.ts";
|
||||||
|
import { fetchResource } from "./resources.ts";
|
||||||
|
|
||||||
type ResourceType = keyof typeof resources;
|
type ResourceType = keyof typeof resources;
|
||||||
|
|
||||||
@@ -56,10 +57,10 @@ export async function searchResource(
|
|||||||
{ q, tags = [], types, rating }: SearchParams,
|
{ q, tags = [], types, rating }: SearchParams,
|
||||||
): Promise<GenericResource[]> {
|
): Promise<GenericResource[]> {
|
||||||
const resources = (await Promise.all([
|
const resources = (await Promise.all([
|
||||||
(!types || types.includes("movie")) && getAllMovies(),
|
(!types || types.includes("movie")) && fetchResource("movies"),
|
||||||
(!types || types.includes("series")) && getAllSeries(),
|
(!types || types.includes("series")) && fetchResource("series"),
|
||||||
(!types || types.includes("article")) && getAllArticles(),
|
(!types || types.includes("article")) && fetchResource("articles"),
|
||||||
(!types || types.includes("recipe")) && getAllRecipes(),
|
(!types || types.includes("recipe")) && fetchResource("recipes"),
|
||||||
])).flat().filter(isResource);
|
])).flat().filter(isResource);
|
||||||
|
|
||||||
const results: Record<string, GenericResource> = {};
|
const results: Record<string, GenericResource> = {};
|
||||||
|
|||||||
2
main.ts
2
main.ts
@@ -7,6 +7,6 @@
|
|||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "./fresh.gen.ts";
|
||||||
import config from "./fresh.config.ts";
|
import config from "./fresh.config.ts";
|
||||||
import "@lib/telegram.ts";
|
// import "@lib/telegram.ts";
|
||||||
|
|
||||||
await start(manifest, config);
|
await start(manifest, config);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { getArticle } from "@lib/resource/articles.ts";
|
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const article = await getArticle(ctx.params.name);
|
const article = await fetchResource(`articles/${ctx.params.name}`);
|
||||||
return json(article);
|
return json(article);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
|
|||||||
import * as openai from "@lib/openai.ts";
|
import * as openai from "@lib/openai.ts";
|
||||||
|
|
||||||
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
|
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 { getYoutubeVideoDetails } from "@lib/youtube.ts";
|
||||||
import {
|
import {
|
||||||
extractYoutubeId,
|
extractYoutubeId,
|
||||||
@@ -170,7 +170,7 @@ async function processCreateArticle(
|
|||||||
|
|
||||||
streamResponse.enqueue("writing to disk");
|
streamResponse.enqueue("writing to disk");
|
||||||
|
|
||||||
await createArticle(newArticle.id, newArticle);
|
// await createArticle(newArticle.id, newArticle);
|
||||||
|
|
||||||
streamResponse.enqueue("id: " + newArticle.id);
|
streamResponse.enqueue("id: " + newArticle.id);
|
||||||
}
|
}
|
||||||
@@ -210,7 +210,7 @@ async function processCreateYoutubeVideo(
|
|||||||
|
|
||||||
streamResponse.enqueue("creating article");
|
streamResponse.enqueue("creating article");
|
||||||
|
|
||||||
await createArticle(newArticle.id, newArticle);
|
// await createArticle(newArticle.id, newArticle);
|
||||||
|
|
||||||
streamResponse.enqueue("finished");
|
streamResponse.enqueue("finished");
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { getAllArticles } from "@lib/resource/articles.ts";
|
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET() {
|
async GET() {
|
||||||
const movies = await getAllArticles();
|
const articles = await fetchResource("articles");
|
||||||
return json(movies);
|
return json(articles?.content);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { FreshContext, Handlers } from "$fresh/server.ts";
|
import { FreshContext, Handlers } from "$fresh/server.ts";
|
||||||
import { getImageContent } from "@lib/image.ts";
|
import { getImageContent } from "@lib/image.ts";
|
||||||
import { SILVERBULLET_SERVER } from "@lib/env.ts";
|
|
||||||
import { createLogger } from "@lib/log/index.ts";
|
import { createLogger } from "@lib/log/index.ts";
|
||||||
import { isLocalImage } from "@lib/string.ts";
|
|
||||||
|
|
||||||
const log = createLogger("api/image");
|
const log = createLogger("api/image");
|
||||||
|
|
||||||
@@ -64,8 +62,7 @@ function parseParams(reqUrl: URL): ImageParams | string {
|
|||||||
// Helper function to generate ETag
|
// Helper function to generate ETag
|
||||||
async function generateETag(content: ArrayBuffer): Promise<string> {
|
async function generateETag(content: ArrayBuffer): Promise<string> {
|
||||||
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
|
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
|
||||||
return `"${
|
return `"${Array.from(new Uint8Array(hashBuffer))
|
||||||
Array.from(new Uint8Array(hashBuffer))
|
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
}"`;
|
}"`;
|
||||||
@@ -83,8 +80,8 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = isLocalImage(params.image)
|
const imageUrl = params.image.startsWith("resources")
|
||||||
? `${SILVERBULLET_SERVER}/${params.image.replace(/^\//, "")}`
|
? `https://marka.max-richter.dev/${params.image.replace(/^\//, "")}`
|
||||||
: params.image;
|
: params.image;
|
||||||
|
|
||||||
log.debug("Processing image request:", { imageUrl, params });
|
log.debug("Processing image request:", { imageUrl, params });
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { getDocuments } from "@lib/documents.ts";
|
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET() {
|
async GET() {
|
||||||
const documents = await getDocuments();
|
return json([]);
|
||||||
return json(documents);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
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 { json } from "@lib/helpers.ts";
|
||||||
import * as tmdb from "@lib/tmdb.ts";
|
import * as tmdb from "@lib/tmdb.ts";
|
||||||
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
||||||
import { isString, safeFileName } from "@lib/string.ts";
|
import { isString, safeFileName } from "@lib/string.ts";
|
||||||
import { createDocument } from "@lib/documents.ts";
|
|
||||||
import { AccessDeniedError } from "@lib/errors.ts";
|
import { AccessDeniedError } from "@lib/errors.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const movie = await getMovie(ctx.params.name);
|
const movie = await fetchResource(`movies/${ctx.params.name}`);
|
||||||
return json(movie);
|
return json(movie?.content);
|
||||||
},
|
},
|
||||||
async POST(_, ctx) {
|
async POST(_, ctx) {
|
||||||
const session = ctx.state.session;
|
const session = ctx.state.session;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { FreshContext, Handlers } from "$fresh/server.ts";
|
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 { 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 * as tmdb from "@lib/tmdb.ts";
|
||||||
import { isString, safeFileName } from "@lib/string.ts";
|
import { isString, safeFileName } from "@lib/string.ts";
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
@@ -11,6 +9,7 @@ import {
|
|||||||
NotFoundError,
|
NotFoundError,
|
||||||
} from "@lib/errors.ts";
|
} from "@lib/errors.ts";
|
||||||
import { createRecommendationResource } from "@lib/recommendation.ts";
|
import { createRecommendationResource } from "@lib/recommendation.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
const POST = async (
|
const POST = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -21,7 +20,7 @@ const POST = async (
|
|||||||
throw new AccessDeniedError();
|
throw new AccessDeniedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const movie = await getMovie(ctx.params.name);
|
const movie = await fetchResource(`movies/${ctx.params.name}`);
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
throw new NotFoundError();
|
throw new NotFoundError();
|
||||||
}
|
}
|
||||||
@@ -72,12 +71,12 @@ const POST = async (
|
|||||||
const poster = await tmdb.getMoviePoster(posterPath);
|
const poster = await tmdb.getMoviePoster(posterPath);
|
||||||
const extension = fileExtension(posterPath);
|
const extension = fileExtension(posterPath);
|
||||||
finalPath = `Media/movies/images/${safeFileName(name)}_cover.${extension}`;
|
finalPath = `Media/movies/images/${safeFileName(name)}_cover.${extension}`;
|
||||||
await createDocument(finalPath, poster);
|
// await createDocument(finalPath, poster);
|
||||||
movie.meta = movie.meta || {};
|
movie.meta = movie.meta || {};
|
||||||
movie.meta.image = finalPath;
|
movie.meta.image = finalPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
await createMovie(movie.id, movie);
|
// await createMovie(movie.id, movie);
|
||||||
|
|
||||||
createRecommendationResource(movie, movieDetails.overview);
|
createRecommendationResource(movie, movieDetails.overview);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { getAllMovies } from "@lib/resource/movies.ts";
|
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET() {
|
async GET() {
|
||||||
const movies = await getAllMovies();
|
const movies = await fetchResource("movies");
|
||||||
return json(movies);
|
return json(movies);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { getRecipe } from "@lib/resource/recipes.ts";
|
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const recipe = await getRecipe(ctx.params.name);
|
const recipe = await fetchResource(`recipes/${ctx.params.name}`);
|
||||||
return json(recipe);
|
return json(recipe);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
|
|||||||
import * as openai from "@lib/openai.ts";
|
import * as openai from "@lib/openai.ts";
|
||||||
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
|
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
|
||||||
import { createLogger } from "@lib/log/index.ts";
|
import { createLogger } from "@lib/log/index.ts";
|
||||||
import { createRecipe, Recipe } from "@lib/resource/recipes.ts";
|
import { Recipe } from "@lib/resource/recipes.ts";
|
||||||
import recipeSchema, { isValidRecipe } from "@lib/recipeSchema.ts";
|
import recipeSchema from "@lib/recipeSchema.ts";
|
||||||
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
||||||
import { safeFileName } from "@lib/string.ts";
|
import { safeFileName } from "@lib/string.ts";
|
||||||
import { createDocument } from "@lib/documents.ts";
|
import { createDocument } from "@lib/documents.ts";
|
||||||
@@ -205,10 +205,10 @@ async function processCreateRecipeFromUrl(
|
|||||||
streamResponse.enqueue("downloading image");
|
streamResponse.enqueue("downloading image");
|
||||||
try {
|
try {
|
||||||
streamResponse.enqueue("downloading image");
|
streamResponse.enqueue("downloading image");
|
||||||
const res = await fetch(src);
|
// const res = await fetch(src);
|
||||||
streamResponse.enqueue("saving image");
|
streamResponse.enqueue("saving image");
|
||||||
const buffer = await res.arrayBuffer();
|
// const buffer = await res.arrayBuffer();
|
||||||
await createDocument(finalPath, buffer);
|
// await createDocument(finalPath, buffer);
|
||||||
newRecipe.meta.image = finalPath;
|
newRecipe.meta.image = finalPath;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Failed to save image", err);
|
console.log("Failed to save image", err);
|
||||||
@@ -218,7 +218,7 @@ async function processCreateRecipeFromUrl(
|
|||||||
|
|
||||||
streamResponse.enqueue("finished processing, creating file");
|
streamResponse.enqueue("finished processing, creating file");
|
||||||
|
|
||||||
await createRecipe(newRecipe.id, newRecipe);
|
// await createRecipe(newRecipe.id, newRecipe);
|
||||||
|
|
||||||
streamResponse.enqueue("id: " + newRecipe.id);
|
streamResponse.enqueue("id: " + newRecipe.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { getAllRecipes } from "@lib/resource/recipes.ts";
|
|
||||||
import { json } from "@lib/helpers.ts";
|
import { json } from "@lib/helpers.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers = {
|
export const handler: Handlers = {
|
||||||
async GET() {
|
async GET() {
|
||||||
const recipes = await getAllRecipes();
|
const recipes = await fetchResource("recipes");
|
||||||
return json(recipes);
|
return json(recipes);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { Handlers } from "$fresh/server.ts";
|
import { Handlers } from "$fresh/server.ts";
|
||||||
import { createStreamResponse } from "@lib/helpers.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 * as tmdb from "@lib/tmdb.ts";
|
||||||
import {
|
import {
|
||||||
createRecommendationResource,
|
createRecommendationResource,
|
||||||
getRecommendation,
|
getRecommendation,
|
||||||
} from "@lib/recommendation.ts";
|
} from "@lib/recommendation.ts";
|
||||||
import { AccessDeniedError } from "@lib/errors.ts";
|
import { AccessDeniedError } from "@lib/errors.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
async function processUpdateRecommendations(
|
async function processUpdateRecommendations(
|
||||||
streamResponse: ReturnType<typeof createStreamResponse>,
|
streamResponse: ReturnType<typeof createStreamResponse>,
|
||||||
) {
|
) {
|
||||||
const allMovies = await getAllMovies();
|
const allMovies = await fetchResource("movies");
|
||||||
|
|
||||||
const movies = allMovies.filter((m) => {
|
const movies = allMovies?.content.filter((m) => {
|
||||||
if (!m?.meta) return false;
|
if (!m?.content) return false;
|
||||||
if (!m.meta.rating) return false;
|
if (!m.content.reviewRating) return false;
|
||||||
if (!m.meta.tmdbId) return false;
|
if (!m.content.tmdbId) return false;
|
||||||
return true;
|
return true;
|
||||||
}) as Movie[];
|
}) as Movie[];
|
||||||
|
|
||||||
|
|||||||
@@ -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 { isString, safeFileName } from "@lib/string.ts";
|
||||||
import { createDocument } from "@lib/documents.ts";
|
import { createDocument } from "@lib/documents.ts";
|
||||||
import { AccessDeniedError } from "@lib/errors.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 = {
|
export const handler: Handlers = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const series = await getSeries(ctx.params.name);
|
const series = await fetchResource(`series/${ctx.params.name}`);
|
||||||
return json(series);
|
return json(series);
|
||||||
},
|
},
|
||||||
async POST(_, ctx) {
|
async POST(_, ctx) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
BadRequestError,
|
BadRequestError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
} from "@lib/errors.ts";
|
} from "@lib/errors.ts";
|
||||||
import { createSeries, getSeries } from "@lib/resource/series.ts";
|
|
||||||
|
|
||||||
const isString = (input: string | undefined): input is string => {
|
const isString = (input: string | undefined): input is string => {
|
||||||
return typeof input === "string";
|
return typeof input === "string";
|
||||||
@@ -64,15 +63,15 @@ const POST = async (
|
|||||||
|
|
||||||
let finalPath = "";
|
let finalPath = "";
|
||||||
if (posterPath && !series.meta?.image) {
|
if (posterPath && !series.meta?.image) {
|
||||||
const poster = await tmdb.getMoviePoster(posterPath);
|
// const poster = await tmdb.getMoviePoster(posterPath);
|
||||||
const extension = fileExtension(posterPath);
|
const extension = fileExtension(posterPath);
|
||||||
|
|
||||||
finalPath = `Media/series/images/${safeFileName(name)}_cover.${extension}`;
|
finalPath = `Media/series/images/${safeFileName(name)}_cover.${extension}`;
|
||||||
await createDocument(finalPath, poster);
|
// await createDocument(finalPath, poster);
|
||||||
series.meta = series.meta || {};
|
series.meta = series.meta || {};
|
||||||
series.meta.image = finalPath;
|
series.meta.image = finalPath;
|
||||||
}
|
}
|
||||||
await createSeries(series.id, series);
|
// await createSeries(series.id, series);
|
||||||
|
|
||||||
return json(series);
|
return json(series);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
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 { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { YoutubePlayer } from "@components/Youtube.tsx";
|
import { YoutubePlayer } from "@components/Youtube.tsx";
|
||||||
import { HashTags } from "@components/HashTags.tsx";
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
@@ -10,10 +10,11 @@ import { RedirectSearchHandler } from "@islands/Search.tsx";
|
|||||||
import PageHero from "@components/PageHero.tsx";
|
import PageHero from "@components/PageHero.tsx";
|
||||||
import { Star } from "@components/Stars.tsx";
|
import { Star } from "@components/Stars.tsx";
|
||||||
import { MetaTags } from "@components/MetaTags.tsx";
|
import { MetaTags } from "@components/MetaTags.tsx";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers<{ article: Article; session: unknown }> = {
|
export const handler: Handlers<{ article: Article; session: unknown }> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const article = await getArticle(ctx.params.name);
|
const article = await fetchResource(`articles/${ctx.params.name}.md`);
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return ctx.renderNotFound();
|
return ctx.renderNotFound();
|
||||||
}
|
}
|
||||||
@@ -26,34 +27,39 @@ export default function Greet(
|
|||||||
) {
|
) {
|
||||||
const { article, session } = props.data;
|
const { article, session } = props.data;
|
||||||
|
|
||||||
const { author = "", date = "" } = article.meta;
|
const { author = "", date = "", articleBody = "" } = article?.content || {};
|
||||||
|
|
||||||
const content = renderMarkdown(
|
const content = renderMarkdown(
|
||||||
removeImage(article.content, article.meta.image),
|
removeImage(articleBody, article.content.image),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log({ article });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout
|
<MainLayout
|
||||||
url={props.url}
|
url={props.url}
|
||||||
title={`Article > ${article.name}`}
|
title={`Article > ${article.content.headline}`}
|
||||||
context={article}
|
context={article}
|
||||||
>
|
>
|
||||||
<RedirectSearchHandler />
|
<RedirectSearchHandler />
|
||||||
<KMenu type="main" context={{ type: "article" }} />
|
<KMenu type="main" context={{ type: "article" }} />
|
||||||
<MetaTags resource={article} />
|
<MetaTags resource={article} />
|
||||||
|
|
||||||
<PageHero image={article.meta.image} thumbnail={article.meta.thumbnail}>
|
<PageHero
|
||||||
|
image={article.content.image}
|
||||||
|
thumbnail={article.content.thumbnail}
|
||||||
|
>
|
||||||
<PageHero.Header>
|
<PageHero.Header>
|
||||||
<PageHero.BackLink href="/articles" />
|
<PageHero.BackLink href="/articles" />
|
||||||
{session && (
|
{session && (
|
||||||
<PageHero.EditLink
|
<PageHero.EditLink
|
||||||
href={`https://notes.max-richter.dev/Media/articles/${article.id}`}
|
href={`https://notes.max-richter.dev/resources/articles/${article.name}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PageHero.Header>
|
</PageHero.Header>
|
||||||
<PageHero.Footer>
|
<PageHero.Footer>
|
||||||
<PageHero.Title link={article.meta.link}>
|
<PageHero.Title link={article.content.url}>
|
||||||
{article.name}
|
{article.content.headline}
|
||||||
</PageHero.Title>
|
</PageHero.Title>
|
||||||
<PageHero.Subline
|
<PageHero.Subline
|
||||||
entries={[
|
entries={[
|
||||||
@@ -64,20 +70,20 @@ export default function Greet(
|
|||||||
date.toString(),
|
date.toString(),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{article.meta.rating && <Star rating={article.meta.rating} />}
|
{article.content.rating && <Star rating={article.content.rating} />}
|
||||||
</PageHero.Subline>
|
</PageHero.Subline>
|
||||||
</PageHero.Footer>
|
</PageHero.Footer>
|
||||||
</PageHero>
|
</PageHero>
|
||||||
{article.tags.length > 0 && (
|
{article.content?.tags?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<HashTags tags={article.tags} />
|
<HashTags tags={article.content.tags} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div class="px-8 text-white mt-10">
|
<div class="px-8 text-white mt-10">
|
||||||
{isYoutubeLink(article.meta.link) && (
|
{isYoutubeLink(article.content.url) && (
|
||||||
<YoutubePlayer link={article.meta.link} />
|
<YoutubePlayer link={article.content.url} />
|
||||||
)}
|
)}
|
||||||
<pre
|
<pre
|
||||||
class="whitespace-break-spaces markdown-body"
|
class="whitespace-break-spaces markdown-body"
|
||||||
@@ -85,7 +91,7 @@ export default function Greet(
|
|||||||
data-dark-theme="dark"
|
data-dark-theme="dark"
|
||||||
dangerouslySetInnerHTML={{ __html: content || "" }}
|
dangerouslySetInnerHTML={{ __html: content || "" }}
|
||||||
>
|
>
|
||||||
{content||""}
|
{content || ""}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
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 { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { Grid } from "@components/Grid.tsx";
|
import { Grid } from "@components/Grid.tsx";
|
||||||
import { IconArrowLeft } from "@components/icons.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 { GenericResource } from "@lib/types.ts";
|
||||||
import { ResourceCard } from "@components/Card.tsx";
|
import { ResourceCard } from "@components/Card.tsx";
|
||||||
import { Link } from "@islands/Link.tsx";
|
import { Link } from "@islands/Link.tsx";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
|
|
||||||
export const handler: Handlers<
|
export const handler: Handlers<
|
||||||
{ articles: Article[] | null; searchResults?: GenericResource[] }
|
{ articles: Article[] | null; searchResults?: GenericResource[] }
|
||||||
> = {
|
> = {
|
||||||
async GET(req, ctx) {
|
async GET(req, ctx) {
|
||||||
const articles = await getAllArticles();
|
const { content: articles } = await fetchResource("articles");
|
||||||
const searchParams = parseResourceUrl(req.url);
|
const searchParams = parseResourceUrl(req.url);
|
||||||
const searchResults = searchParams &&
|
const searchResults = searchParams &&
|
||||||
await searchResource({ ...searchParams, types: ["article"] });
|
await searchResource({ ...searchParams, types: ["article"] });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { PageProps } from "$fresh/server.ts";
|
|||||||
import { resources } from "@lib/resources.ts";
|
import { resources } from "@lib/resources.ts";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import "@lib/telegram.ts";
|
// import "@lib/telegram.ts";
|
||||||
|
|
||||||
export default function Home(props: PageProps) {
|
export default function Home(props: PageProps) {
|
||||||
return (
|
return (
|
||||||
@@ -22,8 +22,7 @@ export default function Home(props: PageProps) {
|
|||||||
<Card
|
<Card
|
||||||
title={`${m.name}`}
|
title={`${m.name}`}
|
||||||
backgroundSize={80}
|
backgroundSize={80}
|
||||||
image={`${
|
image={`${m.emoji.endsWith(".png")
|
||||||
m.emoji.endsWith(".png")
|
|
||||||
? `/emojis/${encodeURIComponent(m.emoji)}`
|
? `/emojis/${encodeURIComponent(m.emoji)}`
|
||||||
: "/placeholder.svg"
|
: "/placeholder.svg"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { PageProps, RouteContext } from "$fresh/server.ts";
|
import { PageProps, RouteContext } from "$fresh/server.ts";
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
import { Movie } from "@lib/resource/movies.ts";
|
||||||
import { HashTags } from "@components/HashTags.tsx";
|
|
||||||
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
@@ -9,22 +8,24 @@ import { Recommendations } from "@islands/Recommendations.tsx";
|
|||||||
import PageHero from "@components/PageHero.tsx";
|
import PageHero from "@components/PageHero.tsx";
|
||||||
import { Star } from "@components/Stars.tsx";
|
import { Star } from "@components/Stars.tsx";
|
||||||
import { MetaTags } from "@components/MetaTags.tsx";
|
import { MetaTags } from "@components/MetaTags.tsx";
|
||||||
|
import { parseRating } from "@lib/helpers.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export default async function Greet(
|
export default async function Greet(
|
||||||
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
|
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
|
||||||
ctx: RouteContext,
|
ctx: RouteContext,
|
||||||
) {
|
) {
|
||||||
const movie = await getMovie(ctx.params.name);
|
const movie = await fetchResource(`movies/${ctx.params.name}.md`);
|
||||||
const session = ctx.state.session;
|
const session = ctx.state.session;
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
return ctx.renderNotFound();
|
return ctx.renderNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { author = "", date = "" } = movie.meta;
|
const { author = "", date = "" } = movie.content;
|
||||||
|
|
||||||
const content = renderMarkdown(
|
const content = renderMarkdown(
|
||||||
removeImage(movie.description || "", movie.meta.image),
|
removeImage(movie.content.reviewBody || "", movie.content.image),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,14 +34,14 @@ export default async function Greet(
|
|||||||
<KMenu type="main" context={movie} />
|
<KMenu type="main" context={movie} />
|
||||||
<MetaTags resource={movie} />
|
<MetaTags resource={movie} />
|
||||||
<PageHero
|
<PageHero
|
||||||
image={movie.meta.image}
|
image={movie.content.image}
|
||||||
thumbnail={movie.meta.thumbnail}
|
thumbnail={movie.content.thumbnail}
|
||||||
>
|
>
|
||||||
<PageHero.Header>
|
<PageHero.Header>
|
||||||
<PageHero.BackLink href="/movies" />
|
<PageHero.BackLink href="/movies" />
|
||||||
{session && (
|
{session && (
|
||||||
<PageHero.EditLink
|
<PageHero.EditLink
|
||||||
href={`https://notes.max-richter.dev/Media/movies/${movie.id}`}
|
href={`https://notes.max-richter.dev/resources/movies/${movie.name}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PageHero.Header>
|
</PageHero.Header>
|
||||||
@@ -49,13 +50,17 @@ export default async function Greet(
|
|||||||
<PageHero.Subline
|
<PageHero.Subline
|
||||||
entries={[
|
entries={[
|
||||||
author && {
|
author && {
|
||||||
title: author,
|
title: author?.name,
|
||||||
href: `/?q=${encodeURIComponent(author)}`,
|
href: `/?q=${encodeURIComponent(author?.name)}`,
|
||||||
},
|
},
|
||||||
date.toString(),
|
date.toString(),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{movie.meta.rating && <Star rating={movie.meta.rating} />}
|
{movie.content.reviewRating && (
|
||||||
|
<Star
|
||||||
|
rating={parseRating(movie.content.reviewRating?.ratingValue)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageHero.Subline>
|
</PageHero.Subline>
|
||||||
</PageHero.Footer>
|
</PageHero.Footer>
|
||||||
</PageHero>
|
</PageHero>
|
||||||
@@ -65,14 +70,8 @@ export default async function Greet(
|
|||||||
type="movie"
|
type="movie"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{movie.tags.length > 0 && (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<HashTags tags={movie.tags} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div class="px-8 text-white mt-10">
|
<div class="px-8 text-white mt-10">
|
||||||
{movie?.description?.length > 80
|
{movie?.content?.reviewBody?.length > 80
|
||||||
? <h2 class="text-4xl font-bold mb-4">Review</h2>
|
? <h2 class="text-4xl font-bold mb-4">Review</h2>
|
||||||
: <></>}
|
: <></>}
|
||||||
<pre
|
<pre
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
|
import { Movie } from "@lib/resource/movies.ts";
|
||||||
import { ResourceCard } from "@components/Card.tsx";
|
import { ResourceCard } from "@components/Card.tsx";
|
||||||
import { Grid } from "@components/Grid.tsx";
|
import { Grid } from "@components/Grid.tsx";
|
||||||
import { IconArrowLeft } from "@components/icons.tsx";
|
import { IconArrowLeft } from "@components/icons.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
|
||||||
import { GenericResource } from "@lib/types.ts";
|
import { GenericResource } from "@lib/types.ts";
|
||||||
import { PageProps } from "$fresh/server.ts";
|
import { PageProps } from "$fresh/server.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
||||||
|
|
||||||
export default async function Greet(
|
export default async function Greet(
|
||||||
props: PageProps<
|
props: PageProps<
|
||||||
{ movies: Movie[] | null; searchResults: GenericResource[] }
|
{ movies: Movie[] | null; searchResults: GenericResource[] }
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const allMovies = await getAllMovies();
|
const { content: allMovies } = await fetchResource("movies");
|
||||||
const searchParams = parseResourceUrl(props.url);
|
const searchParams = parseResourceUrl(props.url);
|
||||||
const searchResults = searchParams &&
|
const searchResults = searchParams &&
|
||||||
await searchResource({ ...searchParams, types: ["movie"] });
|
await searchResource({ ...searchParams, types: ["movie"] });
|
||||||
const movies = allMovies.sort((a, b) =>
|
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 (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { IngredientsList } from "@islands/IngredientsList.tsx";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import Counter from "@islands/Counter.tsx";
|
import Counter from "@islands/Counter.tsx";
|
||||||
import { Signal, useSignal } from "@preact/signals";
|
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 { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import PageHero from "@components/PageHero.tsx";
|
import PageHero from "@components/PageHero.tsx";
|
||||||
@@ -11,11 +11,12 @@ import { Star } from "@components/Stars.tsx";
|
|||||||
import { renderMarkdown } from "@lib/documents.ts";
|
import { renderMarkdown } from "@lib/documents.ts";
|
||||||
import { isValidRecipe } from "@lib/recipeSchema.ts";
|
import { isValidRecipe } from "@lib/recipeSchema.ts";
|
||||||
import { MetaTags } from "@components/MetaTags.tsx";
|
import { MetaTags } from "@components/MetaTags.tsx";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
|
||||||
export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
|
export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
try {
|
try {
|
||||||
const recipe = await getRecipe(ctx.params.name);
|
const recipe = await fetchResource(`recipes/${ctx.params.name}.md`);
|
||||||
if (!recipe) {
|
if (!recipe) {
|
||||||
return ctx.renderNotFound();
|
return ctx.renderNotFound();
|
||||||
}
|
}
|
||||||
@@ -38,14 +39,16 @@ function ValidRecipe({
|
|||||||
{portion && <Counter count={amount} />}
|
{portion && <Counter count={amount} />}
|
||||||
</div>
|
</div>
|
||||||
<IngredientsList
|
<IngredientsList
|
||||||
ingredients={recipe.ingredients}
|
ingredients={recipe.content.recipeIngredient}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
portion={portion}
|
portion={portion}
|
||||||
/>
|
/>
|
||||||
<h3 class="text-3xl my-5">Preparation</h3>
|
<h3 class="text-3xl my-5">Preparation</h3>
|
||||||
<div class="pl-2">
|
<div class="pl-2">
|
||||||
<ol class="list-decimal grid gap-4">
|
<ol class="list-decimal grid gap-4">
|
||||||
{recipe.instructions && (recipe.instructions.map((instruction) => {
|
{recipe.content.recipeInstructions &&
|
||||||
|
(recipe.content.recipeInstructions.filter((inst) => !!inst?.length)
|
||||||
|
.map((instruction) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
@@ -65,35 +68,38 @@ export default function Page(
|
|||||||
) {
|
) {
|
||||||
const { recipe, session } = props.data;
|
const { recipe, session } = props.data;
|
||||||
|
|
||||||
const portion = recipe.meta?.portion;
|
const portion = recipe.recipeYield;
|
||||||
const amount = useSignal(portion || 1);
|
const amount = useSignal(portion || 1);
|
||||||
|
|
||||||
const subline = [
|
const subline = [
|
||||||
recipe?.meta?.time && `Duration ${recipe.meta.time}`,
|
recipe?.content?.prepTime && `Duration ${recipe?.content?.prepTime}`,
|
||||||
].filter(Boolean) as string[];
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout
|
<MainLayout
|
||||||
url={props.url}
|
url={props.url}
|
||||||
title={`Recipes > ${recipe.name}`}
|
title={`Recipes > ${recipe.content?.name}`}
|
||||||
context={recipe}
|
context={recipe}
|
||||||
>
|
>
|
||||||
<RedirectSearchHandler />
|
<RedirectSearchHandler />
|
||||||
<KMenu type="main" context={recipe} />
|
<KMenu type="main" context={recipe} />
|
||||||
<MetaTags resource={recipe} />
|
<MetaTags resource={recipe} />
|
||||||
|
|
||||||
<PageHero image={recipe.meta?.image} thumbnail={recipe.meta?.thumbnail}>
|
<PageHero
|
||||||
|
image={recipe.content?.image}
|
||||||
|
thumbnail={recipe.content?.thumbnail}
|
||||||
|
>
|
||||||
<PageHero.Header>
|
<PageHero.Header>
|
||||||
<PageHero.BackLink href="/recipes" />
|
<PageHero.BackLink href="/recipes" />
|
||||||
{session && (
|
{session && (
|
||||||
<PageHero.EditLink
|
<PageHero.EditLink
|
||||||
href={`https://notes.max-richter.dev/Recipes/${recipe.id}`}
|
href={`https://notes.max-richter.dev/resources/recipes/${recipe.name}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PageHero.Header>
|
</PageHero.Header>
|
||||||
<PageHero.Footer>
|
<PageHero.Footer>
|
||||||
<PageHero.Title link={recipe.meta?.link}>
|
<PageHero.Title link={recipe.content?.link}>
|
||||||
{recipe.name}
|
{recipe.content.name}
|
||||||
</PageHero.Title>
|
</PageHero.Title>
|
||||||
<PageHero.Subline
|
<PageHero.Subline
|
||||||
entries={subline}
|
entries={subline}
|
||||||
@@ -113,12 +119,9 @@ export default function Page(
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div
|
<div class="whitespace-break-spaces markdown-body">
|
||||||
class="whitespace-break-spaces markdown-body"
|
{JSON.stringify(recipe)}
|
||||||
dangerouslySetInnerHTML={{
|
</div>
|
||||||
__html: renderMarkdown(recipe?.markdown || ""),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
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 { Grid } from "@components/Grid.tsx";
|
||||||
import { IconArrowLeft } from "@components/icons.tsx";
|
import { IconArrowLeft } from "@components/icons.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
|
||||||
import { GenericResource } from "@lib/types.ts";
|
import { GenericResource } from "@lib/types.ts";
|
||||||
import { ResourceCard } from "@components/Card.tsx";
|
import { ResourceCard } from "@components/Card.tsx";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
||||||
|
|
||||||
export const handler: Handlers<
|
export const handler: Handlers<
|
||||||
{ recipes: Recipe[] | null; searchResults?: GenericResource[] }
|
{ recipes: Recipe[] | null; searchResults?: GenericResource[] }
|
||||||
> = {
|
> = {
|
||||||
async GET(req, ctx) {
|
async GET(req, ctx) {
|
||||||
const recipes = await getAllRecipes();
|
const { content: recipes } = await fetchResource("recipes");
|
||||||
const searchParams = parseResourceUrl(req.url);
|
const searchParams = parseResourceUrl(req.url);
|
||||||
const searchResults = searchParams &&
|
const searchResults = searchParams &&
|
||||||
await searchResource({ ...searchParams, types: ["recipe"] });
|
await searchResource({ ...searchParams, types: ["recipe"] });
|
||||||
@@ -48,8 +49,8 @@ export default function Greet(
|
|||||||
<h3 class="text-2xl text-white font-light">🍽️ Recipes</h3>
|
<h3 class="text-2xl text-white font-light">🍽️ Recipes</h3>
|
||||||
</header>
|
</header>
|
||||||
<Grid>
|
<Grid>
|
||||||
{recipes?.map((doc) => {
|
{recipes?.filter((s) => !!s?.content?.name).map((doc) => {
|
||||||
return <ResourceCard sublink="recipes" res={doc} />;
|
return <ResourceCard sublink="recipes" key={doc.name} res={doc} />;
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { Handlers, PageProps } from "$fresh/server.ts";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { HashTags } from "@components/HashTags.tsx";
|
import { HashTags } from "@components/HashTags.tsx";
|
||||||
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
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 { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import PageHero from "@components/PageHero.tsx";
|
import PageHero from "@components/PageHero.tsx";
|
||||||
import { Star } from "@components/Stars.tsx";
|
import { Star } from "@components/Stars.tsx";
|
||||||
import { MetaTags } from "@components/MetaTags.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 }> = {
|
export const handler: Handlers<{ serie: Series; session: unknown }> = {
|
||||||
async GET(_, ctx) {
|
async GET(_, ctx) {
|
||||||
const serie = await getSeries(ctx.params.name);
|
const serie = await fetchResource(`series/${ctx.params.name}`);
|
||||||
|
|
||||||
if (!serie) {
|
if (!serie) {
|
||||||
return ctx.renderNotFound();
|
return ctx.renderNotFound();
|
||||||
@@ -25,24 +27,28 @@ export default function Greet(
|
|||||||
) {
|
) {
|
||||||
const { serie, session } = props.data;
|
const { serie, session } = props.data;
|
||||||
|
|
||||||
const { author = "", date = "" } = serie.meta;
|
const { author = "", date = "" } = serie.content;
|
||||||
|
|
||||||
const content = renderMarkdown(
|
const content = renderMarkdown(
|
||||||
removeImage(serie.description || "", serie.meta.image),
|
removeImage(serie.description || "", serie.content.image),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout url={props.url} title={`Serie > ${serie.name}`} context={serie}>
|
<MainLayout
|
||||||
|
url={props.url}
|
||||||
|
title={`Serie > ${serie.content.name}`}
|
||||||
|
context={serie}
|
||||||
|
>
|
||||||
<RedirectSearchHandler />
|
<RedirectSearchHandler />
|
||||||
<KMenu type="main" context={serie} />
|
<KMenu type="main" context={serie} />
|
||||||
|
|
||||||
<MetaTags resource={serie} />
|
<MetaTags resource={serie} />
|
||||||
<PageHero image={serie.meta.image} thumbnail={serie.meta.thumbnail}>
|
<PageHero image={serie.content.image} thumbnail={serie.content.thumbnail}>
|
||||||
<PageHero.Header>
|
<PageHero.Header>
|
||||||
<PageHero.BackLink href="/series" />
|
<PageHero.BackLink href="/series" />
|
||||||
{session && (
|
{session && (
|
||||||
<PageHero.EditLink
|
<PageHero.EditLink
|
||||||
href={`https://notes.max-richter.dev/Media/series/${serie.id}`}
|
href={`https://notes.max-richter.dev/resources/series/${serie.name}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PageHero.Header>
|
</PageHero.Header>
|
||||||
@@ -57,18 +63,22 @@ export default function Greet(
|
|||||||
date.toString(),
|
date.toString(),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{serie.meta.rating && <Star rating={serie.meta.rating} />}
|
{serie.content.reviewRating && (
|
||||||
|
<Star
|
||||||
|
rating={parseRating(serie.content.reviewRating.ratingValue)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageHero.Subline>
|
</PageHero.Subline>
|
||||||
</PageHero.Footer>
|
</PageHero.Footer>
|
||||||
</PageHero>
|
</PageHero>
|
||||||
{serie.tags.length > 0 && (
|
{serie.content?.tags?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<HashTags tags={serie.tags} />
|
<HashTags tags={serie.content.tags} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div class="px-8 text-white mt-10">
|
<div class="px-8 text-white mt-10">
|
||||||
{serie?.description?.length > 80
|
{serie?.content?.reviewBody?.length > 80
|
||||||
? <h2 class="text-4xl font-bold mb-4">Review</h2>
|
? <h2 class="text-4xl font-bold mb-4">Review</h2>
|
||||||
: <></>}
|
: <></>}
|
||||||
<pre
|
<pre
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import { Handlers, PageProps } from "$fresh/server.ts";
|
|||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { Grid } from "@components/Grid.tsx";
|
import { Grid } from "@components/Grid.tsx";
|
||||||
import { IconArrowLeft } from "@components/icons.tsx";
|
import { IconArrowLeft } from "@components/icons.tsx";
|
||||||
import { getAllSeries, Series } from "@lib/resource/series.ts";
|
import { Series } from "@lib/resource/series.ts";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { ResourceCard } from "@components/Card.tsx";
|
import { ResourceCard } from "@components/Card.tsx";
|
||||||
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
|
||||||
import { GenericResource } from "@lib/types.ts";
|
import { GenericResource } from "@lib/types.ts";
|
||||||
|
import { fetchResource } from "@lib/resources.ts";
|
||||||
|
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
||||||
|
|
||||||
export const handler: Handlers<
|
export const handler: Handlers<
|
||||||
{ series: Series[] | null; searchResults?: GenericResource[] }
|
{ series: Series[] | null; searchResults?: GenericResource[] }
|
||||||
> = {
|
> = {
|
||||||
async GET(req, ctx) {
|
async GET(req, ctx) {
|
||||||
const series = await getAllSeries();
|
const { content: series } = await fetchResource("series");
|
||||||
const searchParams = parseResourceUrl(req.url);
|
const searchParams = parseResourceUrl(req.url);
|
||||||
const searchResults = searchParams &&
|
const searchResults = searchParams &&
|
||||||
await searchResource({ ...searchParams, types: ["series"] });
|
await searchResource({ ...searchParams, types: ["series"] });
|
||||||
|
|||||||
Reference in New Issue
Block a user