feat: refactor whole bunch of stuff

This commit is contained in:
Max Richter
2025-11-02 19:03:11 +01:00
parent 81ebc8f5e0
commit e6b90cb785
56 changed files with 753 additions and 360 deletions

View File

@@ -11,13 +11,13 @@ export function Card(
rating, rating,
title, title,
image, image,
thumbnail, thumbhash,
backgroundColor, backgroundColor,
backgroundSize = 100, backgroundSize = 100,
}: { }: {
backgroundSize?: number; backgroundSize?: number;
backgroundColor?: string; backgroundColor?: string;
thumbnail?: string; thumbhash?: string;
link?: string; link?: string;
title?: string; title?: string;
image?: string; image?: string;
@@ -39,13 +39,12 @@ export function Card(
<Link <Link
href={link} href={link}
style={backgroundStyle} style={backgroundStyle}
data-thumb={thumbnail} data-thumb={thumbhash}
class="text-white rounded-3xl shadow-md relative class="text-white rounded-3xl shadow-md relative
lg:w-56 lg:h-56 lg:w-56 lg:h-56
sm:w-48 sm:h-48 sm:w-48 sm:h-48
w-[37vw] h-[37vw]" w-[37vw] h-[37vw]"
> >
{true && (
<span class="absolute top-0 left-0 overflow-hidden rounded-3xl w-full h-full"> <span class="absolute top-0 left-0 overflow-hidden rounded-3xl w-full h-full">
<img <img
class="w-full h-full object-cover" class="w-full h-full object-cover"
@@ -54,7 +53,6 @@ export function Card(
src={image || "/placeholder.svg"} src={image || "/placeholder.svg"}
/> />
</span> </span>
)}
<div <div
class="p-4 flex flex-col justify-between relative z-10" class="p-4 flex flex-col justify-between relative z-10"
style={{ style={{
@@ -97,7 +95,7 @@ export function Card(
export function ResourceCard( export function ResourceCard(
{ res, sublink = "movies" }: { sublink?: string; res: GenericResource }, { res, sublink = "movies" }: { sublink?: string; res: GenericResource },
) { ) {
const img = res?.content?.image || res?.content?.cover; const img = res?.image?.url;
const imageUrl = img const imageUrl = img
? `/api/images?image=${img}&width=200&height=200` ? `/api/images?image=${img}&width=200&height=200`
@@ -105,11 +103,12 @@ export function ResourceCard(
return ( return (
<Card <Card
title={res.content?.name || res.content?.itemReviewed?.name || res.content?.headline || title={res.content?.name || res.content?.itemReviewed?.name ||
res.content?.headline ||
res?.name} res?.name}
backgroundColor={res.meta?.average} backgroundColor={res.image?.average}
thumbhash={res.image?.blurhash}
rating={parseRating(res.content?.reviewRating?.ratingValue)} rating={parseRating(res.content?.reviewRating?.ratingValue)}
thumbnail={res.cover}
image={imageUrl} image={imageUrl}
link={`/${sublink}/${res.name.replace(/\.md$/g, "")}`} link={`/${sublink}/${res.name.replace(/\.md$/g, "")}`}
/> />

View File

@@ -34,7 +34,7 @@ const Image = (
class: string; class: string;
src: string; src: string;
alt?: string; alt?: string;
thumbnail?: string; thumbhash?: string;
fill?: boolean; fill?: boolean;
width?: number | string; width?: number | string;
height?: number | string; height?: number | string;
@@ -55,18 +55,16 @@ const Image = (
height: props.fill ? "100%" : "", height: props.fill ? "100%" : "",
zIndex: props.fill ? -1 : "", zIndex: props.fill ? -1 : "",
}} }}
data-thumb={props.thumbnail} data-thumb={props.thumbhash}
> >
<img <img
data-thumb={props.thumbnail}
data-thumb-img data-thumb-img
loading="lazy" loading="lazy"
alt={props.alt} alt={props.alt}
style={props.style} style={props.style}
srcset={responsiveAttributes.srcset} srcset={responsiveAttributes.srcset}
sizes={responsiveAttributes.sizes} sizes={responsiveAttributes.sizes}
src={`/api/images?image=${asset(props.src)}${ src={`/api/images?image=${asset(props.src)}${props.width ? `&width=${props.width}` : ""
props.width ? `&width=${props.width}` : ""
}${props.height ? `&height=${props.height}` : ""}`} }${props.height ? `&height=${props.height}` : ""}`}
width={props.width} width={props.width}
height={props.height} height={props.height}

View File

@@ -6,22 +6,21 @@ import { IconArrowNarrowLeft } from "@components/icons.tsx";
import { IconEdit } from "@components/icons.tsx"; import { IconEdit } from "@components/icons.tsx";
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
const HeroContext = createContext<{ image?: string; thumbnail?: string }>({ const HeroContext = createContext<{ image?: string; thumbhash?: string }>({
image: undefined, image: undefined,
thumbnail: undefined, thumbhash: undefined,
}); });
function Wrapper( function Wrapper(
{ children, image, thumbnail }: { { children, image, thumbhash }: {
children: ComponentChildren; children: ComponentChildren;
image?: string; image?: string;
thumbnail?: string; thumbhash?: string;
}, },
) { ) {
return ( return (
<div <div
class={`flex justify-between flex-col relative w-full ${ class={`flex justify-between flex-col relative w-full ${image ? "min-h-[400px]" : "min-h-[200px]"
image ? "min-h-[400px]" : "min-h-[200px]"
} rounded-3xl overflow-hidden`} } rounded-3xl overflow-hidden`}
> >
<HeroContext.Provider value={{ image }}> <HeroContext.Provider value={{ image }}>
@@ -30,7 +29,7 @@ function Wrapper(
<Image <Image
fill fill
src={image} src={image}
thumbnail={thumbnail} thumbhash={thumbhash}
alt="Recipe Banner" alt="Recipe Banner"
// style={{ objectPosition: "0% 25%" }} // style={{ objectPosition: "0% 25%" }}
class="absolute object-cover w-full h-full -z-10" class="absolute object-cover w-full h-full -z-10"
@@ -52,8 +51,7 @@ function Title(
return ( return (
<OuterTag <OuterTag
href={link} href={link}
class={`${ class={`${ctx.image ? "noisy-gradient" : ""
ctx.image ? "noisy-gradient" : ""
} after:opacity-90 flex gap-4 items-center ${ctx.image ? "pt-12" : ""}`} } after:opacity-90 flex gap-4 items-center ${ctx.image ? "pt-12" : ""}`}
> >
<h2 <h2
@@ -83,8 +81,7 @@ function EditLink({ href }: { href: string }) {
const ctx = useContext(HeroContext); const ctx = useContext(HeroContext);
return ( return (
<a <a
class={`px-4 py-2 ${ class={`px-4 py-2 ${ctx.image ? "bg-gray-300 text-gray-800" : "text-gray-200"
ctx.image ? "bg-gray-300 text-gray-800" : "text-gray-200"
} rounded-lg flex gap-1 items-center`} } rounded-lg flex gap-1 items-center`}
href={href} href={href}
> >

View File

@@ -0,0 +1 @@
ALTER TABLE `image` RENAME COLUMN "blurhash" TO "thumbhash";

View File

@@ -0,0 +1,311 @@
{
"version": "6",
"dialect": "sqlite",
"id": "685b57ca-45e0-4373-baee-fc3abb4f2d74",
"prevId": "5694a345-e55c-4aa3-9f29-1045b28f5203",
"tables": {
"cache": {
"name": "cache",
"columns": {
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"json": {
"name": "json",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"binary": {
"name": "binary",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(current_timestamp)"
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"key_idx": {
"name": "key_idx",
"columns": [
"key"
],
"isUnique": false
},
"scope_idx": {
"name": "scope_idx",
"columns": [
"scope"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"document": {
"name": "document",
"columns": {
"name": {
"name": "name",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"last_modified": {
"name": "last_modified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content_type": {
"name": "content_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"perm": {
"name": "perm",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"name_idx": {
"name": "name_idx",
"columns": [
"name"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"image": {
"name": "image",
"columns": {
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(current_timestamp)"
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"average": {
"name": "average",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"thumbhash": {
"name": "thumbhash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mime": {
"name": "mime",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"performance": {
"name": "performance",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"search": {
"name": "search",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time": {
"name": "time",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(STRFTIME('%s', 'now') * 1000)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(current_timestamp)"
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(current_timestamp)"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {
"\"image\".\"blurhash\"": "\"image\".\"thumbhash\""
}
},
"internal": {
"indexes": {}
}
}

View File

@@ -71,6 +71,13 @@
"when": 1736172911816, "when": 1736172911816,
"tag": "0009_free_robin_chapel", "tag": "0009_free_robin_chapel",
"breakpoints": true "breakpoints": true
},
{
"idx": 10,
"version": "6",
"when": 1762099260474,
"tag": "0010_youthful_tyrannus",
"breakpoints": true
} }
] ]
} }

View File

@@ -2,7 +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"; import { renderMarkdown } from "@lib/markdown.ts";
function formatAmount(num: number) { function formatAmount(num: number) {
if (num === 0) return ""; if (num === 0) return "";

View File

@@ -1,7 +1,7 @@
import { Movie } from "@lib/resource/movies.ts";
import { TMDBMovie } from "@lib/types.ts"; import { TMDBMovie } from "@lib/types.ts";
import { getCookie } from "@lib/string.ts"; import { getCookie } from "@lib/string.ts";
import { MenuEntry } from "../types.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const addMovieInfos: MenuEntry = { export const addMovieInfos: MenuEntry = {
title: "Add Movie infos", title: "Add Movie infos",
@@ -9,7 +9,7 @@ export const addMovieInfos: MenuEntry = {
icon: "IconReportSearch", icon: "IconReportSearch",
cb: async (state, context) => { cb: async (state, context) => {
state.activeState.value = "loading"; state.activeState.value = "loading";
const movie = context as Movie; const movie = context as ReviewResource;
const query = movie.name; const query = movie.name;
@@ -33,7 +33,7 @@ export const addMovieInfos: MenuEntry = {
}); });
state.visible.value = false; state.visible.value = false;
state.activeState.value = "normal"; state.activeState.value = "normal";
window.location.reload(); globalThis.location.reload();
}, },
})), })),
}; };

View File

@@ -1,7 +1,7 @@
import { MenuEntry } from "@islands/KMenu/types.ts"; import { MenuEntry } from "@islands/KMenu/types.ts";
import { TMDBSeries } from "@lib/types.ts"; import { TMDBSeries } from "@lib/types.ts";
import { getCookie } from "@lib/string.ts"; import { getCookie } from "@lib/string.ts";
import { Series } from "@lib/resource/series.ts"; import { ReviewResource } from "@lib/marka/schema.ts";
export const addSeriesInfo: MenuEntry = { export const addSeriesInfo: MenuEntry = {
title: "Add Series infos", title: "Add Series infos",
@@ -9,7 +9,7 @@ export const addSeriesInfo: MenuEntry = {
icon: "IconReportSearch", icon: "IconReportSearch",
cb: async (state, context) => { cb: async (state, context) => {
state.activeState.value = "loading"; state.activeState.value = "loading";
const series = context as Series; const series = context as ReviewResource;
const query = series.name; const query = series.name;

View File

@@ -1,8 +1,8 @@
import { MenuEntry } from "@islands/KMenu/types.ts"; import { MenuEntry } from "@islands/KMenu/types.ts";
import { TMDBMovie } from "@lib/types.ts"; import { TMDBMovie } from "@lib/types.ts";
import { debounce } from "@lib/helpers.ts"; import { debounce } from "@lib/helpers.ts";
import { Movie } from "@lib/resource/movies.ts";
import { getCookie } from "@lib/string.ts"; import { getCookie } from "@lib/string.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const createNewMovie: MenuEntry = { export const createNewMovie: MenuEntry = {
title: "Create new movie", title: "Create new movie",
@@ -52,9 +52,9 @@ export const createNewMovie: MenuEntry = {
const response = await fetch("/api/movies/" + r.id, { const response = await fetch("/api/movies/" + r.id, {
method: "POST", method: "POST",
}); });
const movie = await response.json() as Movie; const movie = await response.json() as ReviewResource;
unsub(); unsub();
window.location.href = "/movies/" + movie.name; globalThis.location.href = "/movies/" + movie.name;
}, },
}; };
}), }),

View File

@@ -1,8 +1,8 @@
import { MenuEntry } from "@islands/KMenu/types.ts"; import { MenuEntry } from "@islands/KMenu/types.ts";
import { TMDBSeries } from "@lib/types.ts"; import { TMDBSeries } from "@lib/types.ts";
import { debounce } from "@lib/helpers.ts"; import { debounce } from "@lib/helpers.ts";
import { Series } from "@lib/resource/series.ts";
import { getCookie } from "@lib/string.ts"; import { getCookie } from "@lib/string.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const createNewSeries: MenuEntry = { export const createNewSeries: MenuEntry = {
title: "Create new series", title: "Create new series",
@@ -54,9 +54,9 @@ export const createNewSeries: MenuEntry = {
const response = await fetch("/api/series/" + r.id, { const response = await fetch("/api/series/" + r.id, {
method: "POST", method: "POST",
}); });
const series = await response.json() as Series; const series = await response.json() as ReviewResource;
unsub(); unsub();
window.location.href = "/series/" + series.name; globalThis.location.href = "/series/" + series.name;
}, },
}; };
}), }),

View File

@@ -6,13 +6,16 @@ declare global {
} }
export function Link( export function Link(
{ href, children, class: _class, style }: { props: {
href?: string; href?: string;
class?: string; class?: string;
style?: preact.JSX.CSSProperties; style?: preact.JSX.CSSProperties;
children: preact.ComponentChildren; children: preact.ComponentChildren;
"data-thumb"?: string;
}, },
) { ) {
const { href, children, class: _class, style } = props;
const thumbhash = props["data-thumb"];
function handleClick() { function handleClick() {
if (globalThis.loadingTimeout) { if (globalThis.loadingTimeout) {
return; return;
@@ -41,6 +44,7 @@ export function Link(
href={href} href={href}
style={style} style={style}
onClick={handleClick} onClick={handleClick}
data-thumb={thumbhash}
class={_class} class={_class}
> >
{children} {children}

View File

@@ -2,7 +2,6 @@ import { useEffect, useRef } from "preact/hooks";
import useDebouncedCallback from "@lib/hooks/useDebouncedCallback.ts"; import useDebouncedCallback from "@lib/hooks/useDebouncedCallback.ts";
import { IconLoader2, IconSearch } from "@components/icons.tsx"; import { IconLoader2, IconSearch } from "@components/icons.tsx";
import { useEventListener } from "@lib/hooks/useEventListener.ts"; import { useEventListener } from "@lib/hooks/useEventListener.ts";
import { GenericResource } from "@lib/types.ts";
import { resources } from "@lib/resources.ts"; import { resources } from "@lib/resources.ts";
import { getCookie } from "@lib/string.ts"; import { getCookie } from "@lib/string.ts";
import { IS_BROWSER } from "$fresh/runtime.ts"; import { IS_BROWSER } from "$fresh/runtime.ts";
@@ -11,6 +10,7 @@ import { Rating } from "@components/Rating.tsx";
import { useSignal } from "@preact/signals"; import { useSignal } from "@preact/signals";
import Image from "@components/Image.tsx"; import Image from "@components/Image.tsx";
import { Emoji } from "@components/Emoji.tsx"; import { Emoji } from "@components/Emoji.tsx";
import { GenericResource } from "@lib/marka/schema.ts";
export async function fetchQueryResource(url: URL, type = "") { export async function fetchQueryResource(url: URL, type = "") {
const query = url.searchParams.get("q"); const query = url.searchParams.get("q");
@@ -46,7 +46,7 @@ export const RedirectSearchHandler = () => {
}, IS_BROWSER ? document?.body : undefined); }, IS_BROWSER ? document?.body : undefined);
} }
return <></>; return;
}; };
const SearchResultImage = ({ src }: { src: string }) => { const SearchResultImage = ({ src }: { src: string }) => {
@@ -67,8 +67,9 @@ export const SearchResultItem = (
showEmoji?: boolean; showEmoji?: boolean;
}, },
) => { ) => {
const resourceType = resources[item.type]; const resourceType = resources[item?.content._type];
const href = resourceType ? `${resourceType.link}/${item.id}` : ""; const href = item?.path.replace("/resources", "").replace(/\.md$/, "");
console.log({ item, href });
return ( return (
<a <a
href={href} href={href}
@@ -77,8 +78,9 @@ export const SearchResultItem = (
{showEmoji && resourceType {showEmoji && resourceType
? <Emoji class="w-7 h-7" name={resourceType.emoji} /> ? <Emoji class="w-7 h-7" name={resourceType.emoji} />
: ""} : ""}
{item.meta?.image && <SearchResultImage src={item.meta?.image} />} {item.image && <SearchResultImage src={item.image?.url} />}
{item?.name} {item.content?.headline || item.content?.name ||
item.content?.itemReviewed.name || item?.name}
</a> </a>
); );
}; };

View File

@@ -47,7 +47,7 @@ export const imageTable = sqliteTable("image", {
), ),
url: text().notNull(), url: text().notNull(),
average: text().notNull(), average: text().notNull(),
blurhash: text().notNull(), thumbhash: text().notNull(),
mime: text().notNull(), mime: text().notNull(),
}); });

View File

@@ -104,12 +104,13 @@ 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") {
try { try {
return parseInt(rating); const res = parseInt(rating);
if (!Number.isNaN(res)) return res;
} catch (_e) { } catch (_e) {
// This is okay // This is okay
} }
return [...rating.matchAll(/⭐/g)].length; return rating.length / 2;
} }
return rating; return rating;
} }

View File

@@ -3,7 +3,7 @@ import { createLogger } from "@lib/log/index.ts";
import { generateThumbhash } from "@lib/thumbhash.ts"; import { generateThumbhash } from "@lib/thumbhash.ts";
import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts"; import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts";
import path from "node:path"; import path from "node:path";
import { ensureDir } from "fs"; import { mkdir } from "node:fs/promises";
import { DATA_DIR } from "@lib/env.ts"; import { DATA_DIR } from "@lib/env.ts";
import { db } from "@lib/db/sqlite.ts"; import { db } from "@lib/db/sqlite.ts";
import { imageTable } from "@lib/db/schema.ts"; import { imageTable } from "@lib/db/schema.ts";
@@ -13,7 +13,7 @@ import sharp from "npm:sharp@next";
const log = createLogger("cache/image"); const log = createLogger("cache/image");
const imageDir = path.join(DATA_DIR, "images"); const imageDir = path.join(DATA_DIR, "images");
await ensureDir(imageDir); await mkdir(imageDir, { recursive: true });
async function getRemoteImage(imageUrl: string) { async function getRemoteImage(imageUrl: string) {
try { try {
@@ -100,7 +100,7 @@ async function getLocalImagePath(
hostname, hostname,
pathname.split("/").filter((s) => s.length).join("-"), pathname.split("/").filter((s) => s.length).join("-"),
); );
await ensureDir(imagePath); await mkdir(imagePath, { recursive: true });
if (width || height) { if (width || height) {
imagePath = path.join(imagePath, `${width ?? "-"}x${height ?? "-"}`); imagePath = path.join(imagePath, `${width ?? "-"}x${height ?? "-"}`);
@@ -313,7 +313,7 @@ export async function getImage(url: string) {
// Store in database // Store in database
const [newImage] = await db.insert(imageTable).values({ const [newImage] = await db.insert(imageTable).values({
url: url, url: url,
blurhash: thumbhash.hash, thumbhash: thumbhash.hash,
average: thumbhash.average, average: thumbhash.average,
mime: imageContent.mediaType, mime: imageContent.mediaType,
}).returning(); }).returning();

View File

@@ -1,12 +1,12 @@
import * as env from "@lib/env.ts"; import * as env from "@lib/env.ts";
import { ensureDir } from "fs";
import { join } from "node:path"; import { join } from "node:path";
import { getLogLevel, LOG_LEVEL } from "@lib/log/types.ts"; import { getLogLevel, LOG_LEVEL } from "@lib/log/types.ts";
import { mkdir } from "node:fs/promises";
export const LOG_DIR = join(env.DATA_DIR, "logs"); export const LOG_DIR = join(env.DATA_DIR, "logs");
// Ensure the log directory exists // Ensure the log directory exists
await ensureDir(LOG_DIR); await mkdir(LOG_DIR, { recursive: true });
export let logLevel = getLogLevel(env.LOG_LEVEL); export let logLevel = getLogLevel(env.LOG_LEVEL);
export function setLogLevel(level: LOG_LEVEL) { export function setLogLevel(level: LOG_LEVEL) {

View File

@@ -1,35 +0,0 @@
import { MARKA_API_KEY } from "./env.ts";
const url = `https://marka.max-richter.dev/resources`;
//const url = "http://localhost:8080/resources";
export async function fetchResource(resource: string) {
try {
const response = await fetch(
`${url}/${resource}`,
);
return response.json();
} catch (_e) {
return [];
}
}
export async function createResource(
path: string,
content: string | object | ArrayBuffer,
) {
const isJson = typeof content === "object" &&
!(content instanceof ArrayBuffer);
const fetchUrl = `${url}/${path}`;
const response = await fetch(fetchUrl, {
method: "POST",
headers: {
"Content-Type": isJson ? "application/json" : "",
"Authentication": MARKA_API_KEY,
},
body: isJson ? JSON.stringify(content) : content,
});
if (!response.ok) {
throw new Error(`Failed to create resource: ${response.status}`);
}
return response.json();
}

80
lib/marka/index.ts Normal file
View File

@@ -0,0 +1,80 @@
import { MARKA_API_KEY } from "../env.ts";
import { getImage } from "../image.ts";
import { GenericResource } from "./schema.ts";
//const url = `https://marka.max-richter.dev`;
const url = "http://localhost:8080";
async function addImageToResource<T extends GenericResource>(
resource: GenericResource,
): Promise<T> {
const imageUrl = resource?.content?.image;
if (imageUrl) {
try {
const absoluteImageUrl = (imageUrl.startsWith("https://") ||
imageUrl.startsWith("http://"))
? imageUrl
: `${url}/${imageUrl}`;
const image = await getImage(absoluteImageUrl);
return { ...resource, image } as T;
} catch (e) {
console.log(`Failed to fetch image: ${imageUrl}`, e);
}
}
return resource as T;
}
export async function fetchResource<T extends GenericResource>(
resource: string,
): Promise<T | undefined> {
try {
const response = await fetch(
`${url}/resources/${resource}`,
);
const res = await response.json();
return addImageToResource<T>(res);
} catch (_e) {
return;
}
}
export async function listResources<T = GenericResource>(
resource: string,
): Promise<T[]> {
try {
const response = await fetch(
`${url}/resources/${resource}`,
);
const list = await response.json();
return Promise.all(
list?.content
.filter((a: GenericResource) => a?.content?._type)
.map((res: GenericResource) => addImageToResource(res)),
);
} catch (_e) {
return [];
}
}
export async function createResource(
path: string,
content: string | object | ArrayBuffer,
) {
const isJson = typeof content === "object" &&
!(content instanceof ArrayBuffer);
const fetchUrl = `${url}/resources/${path}`;
const headers = new Headers();
headers.append("Content-Type", isJson ? "application/json" : "");
if (MARKA_API_KEY) {
headers.append("Authentication", MARKA_API_KEY);
}
const response = await fetch(fetchUrl, {
method: "POST",
headers,
body: isJson ? JSON.stringify(content) : content,
});
if (!response.ok) {
throw new Error(`Failed to create resource: ${response.status}`);
}
return response.json();
}

126
lib/marka/schema.ts Normal file
View File

@@ -0,0 +1,126 @@
import { z } from "zod";
import { imageTable } from "../db/schema.ts";
export const PersonSchema = z.object({
_type: z.literal("Person"),
name: z.string().optional(),
});
export const ReviewRatingSchema = z.object({
bestRating: z.number().optional(),
worstRating: z.number().optional(),
// Accept number or string (e.g., "⭐️⭐️⭐️⭐️⭐️")
ratingValue: z.union([z.number(), z.string()]).optional(),
});
const WithAuthor = z.object({ author: PersonSchema.optional() });
const WithKeywords = z.object({ keywords: z.array(z.string()).optional() });
const WithImage = z.object({ image: z.string().optional() });
const WithDatePublished = z.object({ datePublished: z.string().optional() });
const BaseContent = WithAuthor.merge(WithKeywords)
.merge(WithImage)
.merge(WithDatePublished);
export const BaseFileSchema = z.object({
type: z.literal("file"),
name: z.string(),
path: z.string(),
modTime: z.string(), // ISO timestamp string
mime: z.string(),
size: z.number().int().nonnegative(),
});
const makeContentSchema = <
TName extends "Article" | "Review" | "Recipe",
TShape extends z.ZodRawShape,
>(
name: TName,
shape: TShape,
) =>
z
.object({
_type: z.literal(name),
keywords: z.array(z.string()).optional(),
})
.merge(BaseContent)
.extend(shape);
export const ArticleContentSchema = makeContentSchema("Article", {
headline: z.string().optional(),
articleBody: z.string().optional(),
url: z.string().optional(),
reviewRating: ReviewRatingSchema.optional(),
});
export const ReviewContentSchema = makeContentSchema("Review", {
tmdbId: z.number().optional(),
link: z.string().optional(),
reviewRating: ReviewRatingSchema.optional(),
reviewBody: z.string().optional(),
itemReviewed: z
.object({
name: z.string().optional(),
})
.optional(),
});
export const RecipeContentSchema = makeContentSchema("Recipe", {
description: z.string().optional(),
name: z.string().optional(),
recipeIngredient: z.array(z.string()).optional(),
recipeInstructions: z.array(z.string()).optional(),
totalTime: z.string().optional(),
recipeYield: z.number().optional(),
url: z.string().optional(),
});
export const articleMetadataSchema = z.object({
headline: z.union([z.null(), z.string()]).describe("Headline of the article"),
author: z.union([z.null(), z.string()]).describe("Author of the article"),
datePublished: z.union([z.null(), z.string()]).describe(
"Date the article was published",
),
keywords: z.union([z.null(), z.array(z.string())]).describe(
"Keywords for the article",
),
});
export const ArticleSchema = BaseFileSchema.extend({
content: ArticleContentSchema,
});
export const ReviewSchema = BaseFileSchema.extend({
content: ReviewContentSchema,
});
export const RecipeSchema = BaseFileSchema.extend({
content: RecipeContentSchema,
});
export const GenericResourceSchema = z.union([
ArticleSchema,
ReviewSchema,
RecipeSchema,
]);
export type Person = z.infer<typeof PersonSchema>;
export type ReviewRating = z.infer<typeof ReviewRatingSchema>;
export type BaseFile = z.infer<typeof BaseFileSchema>;
export type ArticleResource = z.infer<typeof ArticleSchema> & {
image?: typeof imageTable.$inferSelect;
};
export type ReviewResource = z.infer<typeof ReviewSchema> & {
image?: typeof imageTable.$inferSelect;
};
export type RecipeResource = z.infer<typeof RecipeSchema> & {
image?: typeof imageTable.$inferSelect;
};
export type GenericResource = z.infer<typeof GenericResourceSchema> & {
image?: typeof imageTable.$inferSelect;
};

View File

@@ -44,11 +44,3 @@ export function renderMarkdown(doc: string) {
allowMath: true, allowMath: true,
}); });
} }
export function createDocument(
path: string,
entry: string,
mimetype = "image/jpeg",
) {
console.log("creating", { path, entry, mimetype });
}

View File

@@ -4,7 +4,7 @@ import { OPENAI_API_KEY } from "@lib/env.ts";
import { hashString } from "@lib/helpers.ts"; import { hashString } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts"; import { createCache } from "@lib/cache.ts";
import { recipeResponseSchema } from "@lib/recipeSchema.ts"; import { recipeResponseSchema } from "@lib/recipeSchema.ts";
import { articleMetadataSchema } from "./resource/articles.ts"; import { articleMetadataSchema } from "./marka/schema.ts";
const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY }); const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY });

View File

@@ -1,4 +1,5 @@
import { z } from "npm:zod"; import { z } from "npm:zod";
import { RecipeResource } from "./marka/schema.ts";
export const IngredientSchema = z.object({ export const IngredientSchema = z.object({
quantity: z.string().describe( quantity: z.string().describe(
@@ -17,7 +18,6 @@ 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({
_type: z.literal("Recipe"),
name: z.string().describe( 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",
), ),
@@ -29,6 +29,9 @@ const recipeSchema = z.object({
_type: z.literal("Person"), _type: z.literal("Person"),
name: z.string().describe("author of the Recipe (optional)"), name: z.string().describe("author of the Recipe (optional)"),
}), }),
keywords: z.array(z.string()).describe(
"List of keywords that match the recipe",
),
recipeIngredient: z.array(z.string()) recipeIngredient: z.array(z.string())
.describe("List of ingredients"), .describe("List of ingredients"),
recipeInstructions: z.array(z.string()).describe("List of instructions"), recipeInstructions: z.array(z.string()).describe("List of instructions"),
@@ -48,7 +51,7 @@ export const recipeResponseSchema = z.union([recipeSchema, noRecipeSchema]);
export function isValidRecipe( export function isValidRecipe(
recipe: recipe:
| Recipe | RecipeResource
| null | null
| undefined, | undefined,
) { ) {

View File

@@ -1,8 +1,8 @@
import * as openai from "@lib/openai.ts"; import * as openai from "@lib/openai.ts";
import * as tmdb from "@lib/tmdb.ts"; import * as tmdb from "@lib/tmdb.ts";
import { GenericResource } from "@lib/types.ts";
import { parseRating } from "@lib/helpers.ts"; import { parseRating } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts"; import { createCache } from "@lib/cache.ts";
import { GenericResource, ReviewResource } from "./marka/schema.ts";
type RecommendationResource = { type RecommendationResource = {
id: string; id: string;
@@ -18,42 +18,48 @@ type RecommendationResource = {
const cache = createCache<RecommendationResource>("recommendations"); const cache = createCache<RecommendationResource>("recommendations");
export async function createRecommendationResource( export async function createRecommendationResource(
res: GenericResource, res: ReviewResource,
description?: string, description?: string,
) { ) {
const cacheId = `${res.type}:${res.id.replaceAll(":", "")}`; const cacheId = `${res.type}:${res.name.replaceAll(":", "")}`;
const resource = cache.get(cacheId) || { const resource = cache.get(cacheId) || {
id: res.id, id: res.name,
type: res.type, type: res.type,
rating: -1, rating: -1,
}; };
if (description && !resource.keywords) { if (description && !resource.keywords) {
const keywords = await openai.createKeywords(res.type, description, res.id); const keywords = await openai.createKeywords(
res.type,
description,
res.name,
);
if (keywords?.length) { if (keywords?.length) {
resource.keywords = keywords; resource.keywords = keywords;
} }
} }
const { author, date, rating } = res.meta || {}; const { author, datePublished, reviewRating } = res.content;
if (res?.tags) { if (res?.content?.keywords) {
resource.tags = res.tags; resource.keywords = res.content.keywords;
} }
if (typeof rating !== "undefined") { if (typeof reviewRating?.ratingValue !== "undefined") {
resource.rating = parseRating(rating); resource.rating = parseRating(reviewRating?.ratingValue);
} }
if (author) { if (author?.name) {
resource.author = author; resource.author = author.name;
} }
if (description) { if (description) {
resource.description = description; resource.description = description;
} }
if (date) { if (datePublished) {
const d = typeof date === "string" ? new Date(date) : date; const d = typeof datePublished === "string"
? new Date(datePublished)
: datePublished;
resource.year = d.getFullYear(); resource.year = d.getFullYear();
} }

View File

@@ -1,30 +0,0 @@
import { z } from "zod";
export type Article = {
_type: "Article";
headline?: string;
datePublished?: string;
articleBody?: string;
keywords?: string[];
image?: string;
url?: string;
reviewRating?: {
bestRating?: number;
worstRating?: number;
ratingValue?: number;
};
author?: {
_type: "Person";
name?: string;
};
};
export const articleMetadataSchema = z.object({
headline: z.union([z.null(), z.string()]).describe("Headline of the article"),
author: z.union([z.null(), z.string()]).describe("Author of the article"),
datePublished: z.union([z.null(), z.string()]).describe(
"Date the article was published",
),
keywords: z.union([z.null(), z.array(z.string())]).describe(
"Keywords for the article",
),
});

View File

@@ -1,21 +0,0 @@
export type Movie = {
_type: "Review";
tmdbId?: number;
link?: string;
author?: {
_type: "Person";
name?: string;
};
datePublished?: string;
reviewRating?: {
bestRating?: number;
worstRating?: number;
ratingValue?: number;
};
reviewBody?: string;
itemReviewed?: {
name?: string;
};
keywords?: string[];
image?: string;
};

View File

@@ -1,17 +0,0 @@
export type Recipe = {
_type: "Recipe";
author?: {
_type: "Person";
name?: string;
};
description?: string;
image?: string;
name?: string;
recipeIngredient?: string[];
recipeInstructions?: string[];
datePublished?: string;
totalTime?: string;
recipeYield?: number;
url?: string;
keywords?: string[];
};

View File

@@ -1,3 +0,0 @@
import { Movie } from "./movies.ts";
export type Series = Movie;

View File

@@ -1,12 +1,8 @@
import { resources } from "@lib/resources.ts"; import { resources } from "@lib/resources.ts";
import fuzzysort from "npm:fuzzysort"; import fuzzysort from "npm:fuzzysort";
import { GenericResource } from "@lib/types.ts";
import { extractHashTags } from "@lib/string.ts"; import { extractHashTags } from "@lib/string.ts";
import { Movie } from "@lib/resource/movies.ts"; import { listResources } from "./marka/index.ts";
import { Article } from "@lib/resource/articles.ts"; import { GenericResource } from "./marka/schema.ts";
import { Recipe } from "@lib/resource/recipes.ts";
import { Series } from "@lib/resource/series.ts";
import { fetchResource } from "./marka.ts";
type ResourceType = keyof typeof resources; type ResourceType = keyof typeof resources;
@@ -48,8 +44,8 @@ export function parseResourceUrl(_url: string | URL): SearchParams | undefined {
} }
const isResource = ( const isResource = (
item: Movie | Series | Article | Recipe | boolean, item: GenericResource | boolean | undefined,
): item is Movie | Series | Article | Recipe => { ): item is GenericResource => {
return !!item; return !!item;
}; };
@@ -57,10 +53,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")) && fetchResource("movies"), (!types || types.includes("movie")) && listResources("movies"),
(!types || types.includes("series")) && fetchResource("series"), (!types || types.includes("series")) && listResources("series"),
(!types || types.includes("article")) && fetchResource("articles"), (!types || types.includes("article")) && listResources("articles"),
(!types || types.includes("recipe")) && fetchResource("recipes"), (!types || types.includes("recipe")) && listResources("recipes"),
])).flat().filter(isResource); ])).flat().filter(isResource);
const results: Record<string, GenericResource> = {}; const results: Record<string, GenericResource> = {};
@@ -68,27 +64,35 @@ export async function searchResource(
for (const resource of resources) { for (const resource of resources) {
if ( if (
!(resource.name in results) && !(resource.name in results) &&
tags?.length && resource.tags.length && tags?.length && resource.content.keywords?.length &&
tags.every((t) => resource.tags.includes(t)) tags.every((t) => resource.content.keywords?.includes(t))
) { ) {
results[resource.id] = resource; results[resource.name] = resource;
} }
if ( if (
!(resource.id in results) && !(resource.name in results) &&
rating && resource?.meta?.rating && resource.meta.rating >= rating rating && resource?.content?.reviewRating &&
resource.content?.reviewRating?.ratingValue >= rating
) { ) {
results[resource.id] = resource; results[resource.name] = resource;
} }
} }
if (q.length && q !== "*") { if (q.length && q !== "*") {
const fuzzyResult = fuzzysort.go(q, resources, { const fuzzyResult = fuzzysort.go(q, resources, {
keys: ["content", "name", "description", "meta.author"], keys: [
"name",
"content.articleBody",
"content.reviewBody",
"content.name",
"content.description",
"content.author.name",
],
threshold: 0.3, threshold: 0.3,
}); });
for (const result of fuzzyResult) { for (const result of fuzzyResult) {
results[result.obj.id] = result.obj; results[result.obj.name] = result.obj;
} }
} }

View File

@@ -1,5 +1,5 @@
import { transcribe } from "@lib/openai.ts"; import { transcribe } from "@lib/openai.ts";
import { createDocument } from "@lib/documents.ts"; import { createResource } from "@lib/marka/index.ts";
import { createLogger } from "./log/index.ts"; import { createLogger } from "./log/index.ts";
import { convertOggToMp3 } from "./helpers.ts"; import { convertOggToMp3 } from "./helpers.ts";
@@ -48,8 +48,7 @@ export async function endTask(chatId: string): Promise<string | null> {
finalNote += "**[Voice message could not be transcribed]**\n\n"; finalNote += "**[Voice message could not be transcribed]**\n\n";
} }
} else if (entry.type === "photo") { } else if (entry.type === "photo") {
const photoUrl = `${ const photoUrl = `${task.noteName.replace(/\.md$/, "")
task.noteName.replace(/\.md$/, "")
}/photo-${photoIndex++}.jpg`; }/photo-${photoIndex++}.jpg`;
finalNote += `**Photo**:\n ${photoUrl}\n\n`; finalNote += `**Photo**:\n ${photoUrl}\n\n`;
@@ -62,13 +61,13 @@ export async function endTask(chatId: string): Promise<string | null> {
try { try {
for (const entry of photoTasks) { for (const entry of photoTasks) {
await createDocument(entry.path, entry.content, "image/jpeg"); await createResource(entry.path, entry.content);
} }
} catch (err) { } catch (err) {
log.error("Error creating photo document:", err); log.error("Error creating photo document:", err);
} }
try { try {
await createDocument(task.noteName, finalNote, "text/markdown"); await createResource(task.noteName, finalNote);
} catch (error) { } catch (error) {
log.error("Error creating document:", error); log.error("Error creating document:", error);
return error instanceof Error return error instanceof Error

View File

@@ -1,4 +1,4 @@
import { resources } from "@lib/resources.ts"; import { GenericResource, GenericResourceSchema } from "./marka/schema.ts";
export interface TMDBMovie { export interface TMDBMovie {
adult: boolean; adult: boolean;
@@ -33,22 +33,6 @@ export interface TMDBSeries {
vote_count: number; vote_count: number;
} }
export type GenericResource = {
name: string;
id: string;
tags?: string[];
type: keyof typeof resources;
content?: string;
meta?: {
image?: string;
author?: string;
rating?: number;
average?: string;
date?: Date | string;
thumbnail?: string;
};
};
export interface GiteaOauthUser { export interface GiteaOauthUser {
sub: string; sub: string;
name: string; name: string;
@@ -61,7 +45,7 @@ export interface GiteaOauthUser {
export type SearchResult = { export type SearchResult = {
id: string; id: string;
name: string; name: string;
type: keyof typeof resources; type: GenericResource["content"]["_type"];
date?: string; date?: string;
rating: number; rating: number;
tags: string[]; tags: string[];

View File

@@ -29,7 +29,7 @@ export default function App({ Component }: PageProps) {
<Component /> <Component />
</Partial> </Partial>
</body> </body>
<script src="/thumbnails.js" type="module" async defer /> <script src="/thumbhash.js" type="module" async defer />
</html> </html>
); );
} }

View File

@@ -3,7 +3,7 @@ import { Handlers, PageProps } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts"; import { AccessDeniedError } from "@lib/errors.ts";
import { getLogs, Log } from "@lib/log/index.ts"; import { getLogs, Log } from "@lib/log/index.ts";
import { formatDate } from "@lib/string.ts"; import { formatDate } from "@lib/string.ts";
import { renderMarkdown } from "@lib/documents.ts"; import { renderMarkdown } from "@lib/markdown.ts";
const renderLog = (t: unknown) => const renderLog = (t: unknown) =>
renderMarkdown(`\`\`\`js renderMarkdown(`\`\`\`js

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts"; import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(_, ctx) { async GET(_, ctx) {

View File

@@ -3,8 +3,6 @@ import { Defuddle } from "defuddle/node";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts"; import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts"; import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts"; import * as openai from "@lib/openai.ts";
import { Article } from "@lib/resource/articles.ts";
import { getYoutubeVideoDetails } from "@lib/youtube.ts"; import { getYoutubeVideoDetails } from "@lib/youtube.ts";
import { import {
extractYoutubeId, extractYoutubeId,
@@ -12,8 +10,9 @@ import {
toUrlSafeString, toUrlSafeString,
} from "@lib/string.ts"; } from "@lib/string.ts";
import { createLogger } from "@lib/log/index.ts"; import { createLogger } from "@lib/log/index.ts";
import { createResource } from "@lib/marka.ts"; import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts"; import { webScrape } from "@lib/webScraper.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
const log = createLogger("api/article"); const log = createLogger("api/article");
@@ -93,7 +92,7 @@ async function processCreateYoutubeVideo(
const id = newId || youtubeId; const id = newId || youtubeId;
const newArticle: Article = { const newArticle: ArticleResource["content"] = {
_type: "Article", _type: "Article",
headline: video.snippet.title, headline: video.snippet.title,
articleBody: video.snippet.description, articleBody: video.snippet.description,

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts"; import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET() { async GET() {

View File

@@ -80,13 +80,9 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
}); });
} }
const imageUrl = params.image.startsWith("resources") log.debug("Processing image request:", { params });
? `https://marka.max-richter.dev/${params.image.replace(/^\//, "")}`
: params.image;
log.debug("Processing image request:", { imageUrl, params }); const image = await getImageContent(params.image, params);
const image = await getImageContent(imageUrl, params);
// Generate ETag based on image content // Generate ETag based on image content
const eTag = await generateETag(image.content); const eTag = await generateETag(image.content);

View File

@@ -1,11 +1,11 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.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 { AccessDeniedError } from "@lib/errors.ts"; import { AccessDeniedError } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka.ts"; import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(_, ctx) { async GET(_, ctx) {
@@ -50,7 +50,7 @@ export const handler: Handlers = {
); );
} }
const movie: Movie = { const movie: ReviewResource["content"] = {
_type: "Review", _type: "Review",
image: `resources/${finalPath}`, image: `resources/${finalPath}`,
datePublished: releaseDate, datePublished: releaseDate,

View File

@@ -9,7 +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 { createResource, fetchResource } from "@lib/marka.ts"; import { createResource, fetchResource } from "@lib/marka/index.ts";
const POST = async ( const POST = async (
req: Request, req: Request,

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts"; import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET() { async GET() {

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts"; import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(_, ctx) { async GET(_, ctx) {

View File

@@ -3,15 +3,15 @@ import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts"; import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts"; import * as openai from "@lib/openai.ts";
import { createLogger } from "@lib/log/index.ts"; import { createLogger } from "@lib/log/index.ts";
import { Recipe } from "@lib/resource/recipes.ts";
import recipeSchema 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 { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts"; import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts";
import z from "zod"; import z from "zod";
import { createResource } from "@lib/marka.ts"; import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts"; import { webScrape } from "@lib/webScraper.ts";
import { Defuddle } from "defuddle/node"; import { Defuddle } from "defuddle/node";
import { RecipeResource } from "@lib/marka/schema.ts";
const log = createLogger("api/article"); const log = createLogger("api/article");
@@ -51,7 +51,7 @@ async function processCreateRecipeFromUrl(
recipe = await openai.extractRecipe(result.content); recipe = await openai.extractRecipe(result.content);
} }
const id = safeFileName(recipe?.title || ""); const id = safeFileName(recipe?.name || "");
if (!recipe) { if (!recipe) {
streamResponse.enqueue("failed to parse recipe"); streamResponse.enqueue("failed to parse recipe");
@@ -59,23 +59,13 @@ async function processCreateRecipeFromUrl(
return; return;
} }
const newRecipe: Recipe = { const newRecipe: RecipeResource["content"] = {
...recipe,
_type: "Recipe", _type: "Recipe",
name: recipe?.title,
description: recipe?.description,
recipeIngredient: recipe?.ingredients || [],
recipeInstructions: recipe?.instructions || [],
keywords: recipe.tags || [],
image: recipe?.image,
totalTime: recipe?.totalTime totalTime: recipe?.totalTime
? `${recipe?.totalTime?.toString()} minutes` ? `${recipe?.totalTime?.toString()} minutes`
: undefined, : undefined,
url: fetchUrl, url: fetchUrl,
author: {
_type: "Person",
name: recipe?.author,
},
recipeYield: recipe?.servings,
}; };
if (newRecipe?.image && newRecipe.image.length > 5) { if (newRecipe?.image && newRecipe.image.length > 5) {

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts"; import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET() { async GET() {

View File

@@ -1,25 +1,25 @@
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 { 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/marka.ts"; import { listResources } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
async function processUpdateRecommendations( async function processUpdateRecommendations(
streamResponse: ReturnType<typeof createStreamResponse>, streamResponse: ReturnType<typeof createStreamResponse>,
) { ) {
const allMovies = await fetchResource("movies"); const allMovies = await listResources<ReviewResource>("movies");
const movies = allMovies?.content.filter((m) => { const movies = allMovies?.filter((m: ReviewResource) => {
if (!m?.content) return false; if (!m?.content) return false;
if (!m.content.reviewRating) return false; if (!m.content.reviewRating) return false;
if (!m.content.tmdbId) return false; if (!m.content.tmdbId) return false;
return true; return true;
}) as Movie[]; }) as ReviewResource[];
streamResponse.enqueue("Fetched all movies"); streamResponse.enqueue("Fetched all movies");
@@ -27,22 +27,23 @@ async function processUpdateRecommendations(
const total = movies.length; const total = movies.length;
await Promise.all(movies.map(async (movie) => { await Promise.all(movies.map(async (movie) => {
if (!movie.meta.tmdbId) return; if (!movie.content.tmdbId) return;
if (!movie.meta.rating) return; if (!movie.content.reviewRating) return;
const recommendation = getRecommendation(movie.id, movie.type); const recommendation = getRecommendation(movie.name, movie.type);
if (recommendation) { if (recommendation) {
done++; done++;
return; return;
} }
try { try {
const movieDetails = await tmdb.getMovie(movie.meta.tmdbId); const movieDetails = await tmdb.getMovie(movie.content.tmdbId);
await createRecommendationResource(movie, movieDetails.overview); await createRecommendationResource(movie, movieDetails.overview);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
done++; done++;
streamResponse.enqueue( streamResponse.enqueue(
`${Math.floor((done / total) * 100)}% [${done + 1}/${total}] ${movie.id}`, `${Math.floor((done / total) * 100)}% [${done + 1
}/${total}] ${movie.name}`,
); );
})).catch((err) => { })).catch((err) => {
console.log(err); console.log(err);

View File

@@ -4,8 +4,8 @@ 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 { AccessDeniedError } from "@lib/errors.ts"; import { AccessDeniedError } from "@lib/errors.ts";
import { Series } from "@lib/resource/series.ts"; import { createResource, fetchResource } from "@lib/marka/index.ts";
import { createResource, fetchResource } from "@lib/marka.ts"; import { ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(_, ctx) { async GET(_, ctx) {
@@ -49,7 +49,7 @@ export const handler: Handlers = {
); );
} }
const series: Series = { const series: ReviewResource["content"] = {
_type: "Review", _type: "Review",
image: `resources/${finalPath}`, image: `resources/${finalPath}`,
datePublished: releaseDate, datePublished: releaseDate,

View File

@@ -9,7 +9,7 @@ import {
BadRequestError, BadRequestError,
NotFoundError, NotFoundError,
} from "@lib/errors.ts"; } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka.ts"; import { createResource, fetchResource } from "@lib/marka/index.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";

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts"; import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET() { async GET() {

View File

@@ -1,18 +1,18 @@
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 } 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";
import { isYoutubeLink } from "@lib/string.ts"; import { isYoutubeLink } from "@lib/string.ts";
import { removeImage, renderMarkdown } from "@lib/documents.ts"; import { removeImage, renderMarkdown } from "@lib/markdown.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx"; 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/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
export const handler: Handlers<{ article: Article; session: unknown }> = { export const handler: Handlers<{ article: ArticleResource; session: unknown }> = {
async GET(_, ctx) { async GET(_, ctx) {
const article = await fetchResource(`articles/${ctx.params.name}.md`); const article = await fetchResource(`articles/${ctx.params.name}.md`);
if (!article) { if (!article) {
@@ -30,11 +30,9 @@ export default function Greet(
const { author = "", date = "", articleBody = "" } = article?.content || {}; const { author = "", date = "", articleBody = "" } = article?.content || {};
const content = renderMarkdown( const content = renderMarkdown(
removeImage(articleBody, article.content.image), removeImage(articleBody, article.image?.url),
); );
console.log({ article });
return ( return (
<MainLayout <MainLayout
url={props.url} url={props.url}
@@ -46,8 +44,8 @@ export default function Greet(
<MetaTags resource={article} /> <MetaTags resource={article} />
<PageHero <PageHero
image={article.content.image} image={article.image?.url}
thumbnail={article.content.thumbnail} thumbhash={article.image?.thumbhash}
> >
<PageHero.Header> <PageHero.Header>
<PageHero.BackLink href="/articles" /> <PageHero.BackLink href="/articles" />

View File

@@ -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 } from "@lib/resource/articles.ts"; import { type ArticleResource } from "@lib/marka/schema.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,13 +9,13 @@ 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/marka.ts"; import { listResources } from "@lib/marka/index.ts";
export const handler: Handlers< export const handler: Handlers<
{ articles: Article[] | null; searchResults?: GenericResource[] } { articles: ArticleResource[] | null; searchResults?: GenericResource[] }
> = { > = {
async GET(req, ctx) { async GET(req, ctx) {
const { content: articles } = await fetchResource("articles"); const articles = await listResources("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"] });
@@ -25,7 +25,7 @@ export const handler: Handlers<
export default function Greet( export default function Greet(
props: PageProps< props: PageProps<
{ articles: Article[] | null; searchResults: GenericResource[] } { articles: ArticleResource[] | null; searchResults: GenericResource[] }
>, >,
) { ) {
const { articles, searchResults } = props.data; const { articles, searchResults } = props.data;

View File

@@ -1,7 +1,7 @@
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 { Movie } from "@lib/resource/movies.ts"; import { ReviewResource, ReviewSchema } from "@lib/marka/schema.ts";
import { removeImage, renderMarkdown } from "@lib/documents.ts"; import { removeImage, renderMarkdown } from "@lib/markdown.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";
import { Recommendations } from "@islands/Recommendations.tsx"; import { Recommendations } from "@islands/Recommendations.tsx";
@@ -9,20 +9,22 @@ 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 { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
export default async function Greet( export default async function Greet(
props: PageProps<{ movie: Movie; session: Record<string, string> }>, props: PageProps<{ movie: ReviewResource; session: Record<string, string> }>,
ctx: RouteContext, ctx: RouteContext,
) { ) {
const movie = await fetchResource(`movies/${ctx.params.name}.md`); const movie = await fetchResource<ReviewResource>(
`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.content; const { author = "", datePublished = "" } = movie.content;
const content = renderMarkdown( const content = renderMarkdown(
removeImage(movie.content.reviewBody || "", movie.content.image), removeImage(movie.content.reviewBody || "", movie.content.image),
@@ -34,8 +36,8 @@ 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.content.image} image={movie.image?.url}
thumbnail={movie.content.thumbnail} thumbhash={movie.image?.thumbhash}
> >
<PageHero.Header> <PageHero.Header>
<PageHero.BackLink href="/movies" /> <PageHero.BackLink href="/movies" />
@@ -60,7 +62,7 @@ export default async function Greet(
> >
{movie.content.reviewRating && ( {movie.content.reviewRating && (
<Star <Star
rating={parseRating(movie.content.reviewRating?.ratingValue)} rating={parseRating(movie.content?.reviewRating?.ratingValue)}
/> />
)} )}
</PageHero.Subline> </PageHero.Subline>

View File

@@ -1,5 +1,5 @@
import { MainLayout } from "@components/layouts/main.tsx"; import { MainLayout } from "@components/layouts/main.tsx";
import { Movie } from "@lib/resource/movies.ts"; import { ReviewResource } from "@lib/marka/schema.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";
@@ -7,15 +7,15 @@ import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx";
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/marka.ts"; import { listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { parseResourceUrl, searchResource } from "@lib/search.ts";
export default async function Greet( export default async function MovieIndex(
props: PageProps< props: PageProps<
{ movies: Movie[] | null; searchResults: GenericResource[] } { movies: ReviewResource[] | null; searchResults: GenericResource[] }
>, >,
) { ) {
const { content: allMovies } = await fetchResource("movies"); const allMovies = await listResources("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"] });
@@ -47,8 +47,8 @@ export default async function Greet(
<h3 class="text-2xl text-white font-light">🍿 Movies</h3> <h3 class="text-2xl text-white font-light">🍿 Movies</h3>
</header> </header>
<Grid> <Grid>
{movies?.map((doc) => { {movies?.map((doc, i) => {
return <ResourceCard res={doc} />; return <ResourceCard key={i} res={doc} />;
})} })}
</Grid> </Grid>
</MainLayout> </MainLayout>

View File

@@ -8,10 +8,10 @@ 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 { renderMarkdown } from "@lib/documents.ts"; import { renderMarkdown } from "@lib/markdown.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/marka.ts"; import { fetchResource } from "@lib/marka/index.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) {
@@ -86,8 +86,8 @@ export default function Page(
<MetaTags resource={recipe} /> <MetaTags resource={recipe} />
<PageHero <PageHero
image={recipe.content?.image} image={recipe.image?.url}
thumbnail={recipe.content?.thumbnail} thumbhash={recipe.image?.thumbhash}
> >
<PageHero.Header> <PageHero.Header>
<PageHero.BackLink href="/recipes" /> <PageHero.BackLink href="/recipes" />

View File

@@ -7,14 +7,14 @@ import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx";
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/marka.ts"; import { fetchResource, listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.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 { content: recipes } = await fetchResource("recipes"); const recipes = await listResources("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"] });

View File

@@ -1,17 +1,17 @@
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 { HashTags } from "@components/HashTags.tsx"; import { HashTags } from "@components/HashTags.tsx";
import { removeImage, renderMarkdown } from "@lib/documents.ts"; import { removeImage, renderMarkdown } from "@lib/markdown.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 { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts"; import { fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers<{ serie: Series; session: unknown }> = { export const handler: Handlers<{ serie: ReviewResource; session: unknown }> = {
async GET(_, ctx) { async GET(_, ctx) {
const serie = await fetchResource(`series/${ctx.params.name}.md`); const serie = await fetchResource(`series/${ctx.params.name}.md`);
@@ -44,8 +44,8 @@ export default function Greet(
<MetaTags resource={serie} /> <MetaTags resource={serie} />
<PageHero <PageHero
image={serie.content?.image} image={serie.image?.url}
thumbnail={serie.content?.thumbnail} thumbhash={serie.image?.thumbhash}
> >
<PageHero.Header> <PageHero.Header>
<PageHero.BackLink href="/series" /> <PageHero.BackLink href="/series" />

View File

@@ -2,19 +2,18 @@ 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 { 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 { GenericResource } from "@lib/types.ts"; import { listResources } from "@lib/marka/index.ts";
import { fetchResource } from "@lib/marka.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource, ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers< export const handler: Handlers<
{ series: Series[] | null; searchResults?: GenericResource[] } { series: ReviewResource[] | null; searchResults?: GenericResource[] }
> = { > = {
async GET(req, ctx) { async GET(req, ctx) {
const { content: series } = await fetchResource("series"); const series = await listResources("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"] });
@@ -24,7 +23,7 @@ export const handler: Handlers<
export default function Greet( export default function Greet(
props: PageProps< props: PageProps<
{ series: Series[] | null; searchResults: GenericResource[] } { series: ReviewResource[] | null; searchResults: GenericResource[] }
>, >,
) { ) {
const { series, searchResults } = props.data; const { series, searchResults } = props.data;
@@ -50,8 +49,8 @@ export default function Greet(
<h3 class="text-2xl text-white font-light">🎥 Series</h3> <h3 class="text-2xl text-white font-light">🎥 Series</h3>
</header> </header>
<Grid> <Grid>
{series?.map((doc) => { {series?.map((doc, i) => {
return <ResourceCard sublink="series" res={doc} />; return <ResourceCard key={i} sublink="series" res={doc} />;
})} })}
</Grid> </Grid>
</MainLayout> </MainLayout>

View File

@@ -226,7 +226,7 @@ function rgbaToDataURL(w, h, rgba) {
return "data:image/png;base64," + btoa(String.fromCharCode(...bytes)); return "data:image/png;base64," + btoa(String.fromCharCode(...bytes));
} }
function updateThumbnailImages() { function updateThumbhashImages() {
document.querySelectorAll("[data-thumb]").forEach((entry) => { document.querySelectorAll("[data-thumb]").forEach((entry) => {
const hash = entry.getAttribute("data-thumb"); const hash = entry.getAttribute("data-thumb");
@@ -257,7 +257,7 @@ function updateThumbnailImages() {
child.style.opacity = 1; child.style.opacity = 1;
child.style.filter = "blur(0px)"; child.style.filter = "blur(0px)";
setTimeout(() => { setTimeout(() => {
entry.style.background = ""; //entry.style.background = "";
}, 400); }, 400);
}); });
} }
@@ -265,5 +265,5 @@ function updateThumbnailImages() {
}); });
} }
globalThis.addEventListener("load", updateThumbnailImages); globalThis.addEventListener("load", updateThumbhashImages);
globalThis.addEventListener("loading-finished", updateThumbnailImages); globalThis.addEventListener("loading-finished", updateThumbhashImages);