feat: refactor whole bunch of stuff
This commit is contained in:
@@ -11,13 +11,13 @@ export function Card(
|
||||
rating,
|
||||
title,
|
||||
image,
|
||||
thumbnail,
|
||||
thumbhash,
|
||||
backgroundColor,
|
||||
backgroundSize = 100,
|
||||
}: {
|
||||
backgroundSize?: number;
|
||||
backgroundColor?: string;
|
||||
thumbnail?: string;
|
||||
thumbhash?: string;
|
||||
link?: string;
|
||||
title?: string;
|
||||
image?: string;
|
||||
@@ -39,13 +39,12 @@ export function Card(
|
||||
<Link
|
||||
href={link}
|
||||
style={backgroundStyle}
|
||||
data-thumb={thumbnail}
|
||||
data-thumb={thumbhash}
|
||||
class="text-white rounded-3xl shadow-md relative
|
||||
lg:w-56 lg:h-56
|
||||
sm:w-48 sm:h-48
|
||||
w-[37vw] h-[37vw]"
|
||||
>
|
||||
{true && (
|
||||
<span class="absolute top-0 left-0 overflow-hidden rounded-3xl w-full h-full">
|
||||
<img
|
||||
class="w-full h-full object-cover"
|
||||
@@ -54,7 +53,6 @@ export function Card(
|
||||
src={image || "/placeholder.svg"}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
class="p-4 flex flex-col justify-between relative z-10"
|
||||
style={{
|
||||
@@ -97,7 +95,7 @@ export function Card(
|
||||
export function ResourceCard(
|
||||
{ res, sublink = "movies" }: { sublink?: string; res: GenericResource },
|
||||
) {
|
||||
const img = res?.content?.image || res?.content?.cover;
|
||||
const img = res?.image?.url;
|
||||
|
||||
const imageUrl = img
|
||||
? `/api/images?image=${img}&width=200&height=200`
|
||||
@@ -105,11 +103,12 @@ export function ResourceCard(
|
||||
|
||||
return (
|
||||
<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}
|
||||
backgroundColor={res.meta?.average}
|
||||
backgroundColor={res.image?.average}
|
||||
thumbhash={res.image?.blurhash}
|
||||
rating={parseRating(res.content?.reviewRating?.ratingValue)}
|
||||
thumbnail={res.cover}
|
||||
image={imageUrl}
|
||||
link={`/${sublink}/${res.name.replace(/\.md$/g, "")}`}
|
||||
/>
|
||||
|
||||
@@ -34,7 +34,7 @@ const Image = (
|
||||
class: string;
|
||||
src: string;
|
||||
alt?: string;
|
||||
thumbnail?: string;
|
||||
thumbhash?: string;
|
||||
fill?: boolean;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
@@ -55,18 +55,16 @@ const Image = (
|
||||
height: props.fill ? "100%" : "",
|
||||
zIndex: props.fill ? -1 : "",
|
||||
}}
|
||||
data-thumb={props.thumbnail}
|
||||
data-thumb={props.thumbhash}
|
||||
>
|
||||
<img
|
||||
data-thumb={props.thumbnail}
|
||||
data-thumb-img
|
||||
loading="lazy"
|
||||
alt={props.alt}
|
||||
style={props.style}
|
||||
srcset={responsiveAttributes.srcset}
|
||||
sizes={responsiveAttributes.sizes}
|
||||
src={`/api/images?image=${asset(props.src)}${
|
||||
props.width ? `&width=${props.width}` : ""
|
||||
src={`/api/images?image=${asset(props.src)}${props.width ? `&width=${props.width}` : ""
|
||||
}${props.height ? `&height=${props.height}` : ""}`}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
|
||||
@@ -6,22 +6,21 @@ import { IconArrowNarrowLeft } from "@components/icons.tsx";
|
||||
import { IconEdit } from "@components/icons.tsx";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
const HeroContext = createContext<{ image?: string; thumbnail?: string }>({
|
||||
const HeroContext = createContext<{ image?: string; thumbhash?: string }>({
|
||||
image: undefined,
|
||||
thumbnail: undefined,
|
||||
thumbhash: undefined,
|
||||
});
|
||||
|
||||
function Wrapper(
|
||||
{ children, image, thumbnail }: {
|
||||
{ children, image, thumbhash }: {
|
||||
children: ComponentChildren;
|
||||
image?: string;
|
||||
thumbnail?: string;
|
||||
thumbhash?: string;
|
||||
},
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
class={`flex justify-between flex-col relative w-full ${
|
||||
image ? "min-h-[400px]" : "min-h-[200px]"
|
||||
class={`flex justify-between flex-col relative w-full ${image ? "min-h-[400px]" : "min-h-[200px]"
|
||||
} rounded-3xl overflow-hidden`}
|
||||
>
|
||||
<HeroContext.Provider value={{ image }}>
|
||||
@@ -30,7 +29,7 @@ function Wrapper(
|
||||
<Image
|
||||
fill
|
||||
src={image}
|
||||
thumbnail={thumbnail}
|
||||
thumbhash={thumbhash}
|
||||
alt="Recipe Banner"
|
||||
// style={{ objectPosition: "0% 25%" }}
|
||||
class="absolute object-cover w-full h-full -z-10"
|
||||
@@ -52,8 +51,7 @@ function Title(
|
||||
return (
|
||||
<OuterTag
|
||||
href={link}
|
||||
class={`${
|
||||
ctx.image ? "noisy-gradient" : ""
|
||||
class={`${ctx.image ? "noisy-gradient" : ""
|
||||
} after:opacity-90 flex gap-4 items-center ${ctx.image ? "pt-12" : ""}`}
|
||||
>
|
||||
<h2
|
||||
@@ -83,8 +81,7 @@ function EditLink({ href }: { href: string }) {
|
||||
const ctx = useContext(HeroContext);
|
||||
return (
|
||||
<a
|
||||
class={`px-4 py-2 ${
|
||||
ctx.image ? "bg-gray-300 text-gray-800" : "text-gray-200"
|
||||
class={`px-4 py-2 ${ctx.image ? "bg-gray-300 text-gray-800" : "text-gray-200"
|
||||
} rounded-lg flex gap-1 items-center`}
|
||||
href={href}
|
||||
>
|
||||
|
||||
1
drizzle/0010_youthful_tyrannus.sql
Normal file
1
drizzle/0010_youthful_tyrannus.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `image` RENAME COLUMN "blurhash" TO "thumbhash";
|
||||
311
drizzle/meta/0010_snapshot.json
Normal file
311
drizzle/meta/0010_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,13 @@
|
||||
"when": 1736172911816,
|
||||
"tag": "0009_free_robin_chapel",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "6",
|
||||
"when": 1762099260474,
|
||||
"tag": "0010_youthful_tyrannus",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Signal } from "@preact/signals";
|
||||
import type { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
|
||||
import { FunctionalComponent } from "preact";
|
||||
import { unitsOfMeasure } from "@lib/parseIngredient.ts";
|
||||
import { renderMarkdown } from "@lib/documents.ts";
|
||||
import { renderMarkdown } from "@lib/markdown.ts";
|
||||
|
||||
function formatAmount(num: number) {
|
||||
if (num === 0) return "";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { TMDBMovie } from "@lib/types.ts";
|
||||
import { getCookie } from "@lib/string.ts";
|
||||
import { MenuEntry } from "../types.ts";
|
||||
import { ReviewResource } from "@lib/marka/schema.ts";
|
||||
|
||||
export const addMovieInfos: MenuEntry = {
|
||||
title: "Add Movie infos",
|
||||
@@ -9,7 +9,7 @@ export const addMovieInfos: MenuEntry = {
|
||||
icon: "IconReportSearch",
|
||||
cb: async (state, context) => {
|
||||
state.activeState.value = "loading";
|
||||
const movie = context as Movie;
|
||||
const movie = context as ReviewResource;
|
||||
|
||||
const query = movie.name;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const addMovieInfos: MenuEntry = {
|
||||
});
|
||||
state.visible.value = false;
|
||||
state.activeState.value = "normal";
|
||||
window.location.reload();
|
||||
globalThis.location.reload();
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { TMDBSeries } from "@lib/types.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 = {
|
||||
title: "Add Series infos",
|
||||
@@ -9,7 +9,7 @@ export const addSeriesInfo: MenuEntry = {
|
||||
icon: "IconReportSearch",
|
||||
cb: async (state, context) => {
|
||||
state.activeState.value = "loading";
|
||||
const series = context as Series;
|
||||
const series = context as ReviewResource;
|
||||
|
||||
const query = series.name;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { TMDBMovie } from "@lib/types.ts";
|
||||
import { debounce } from "@lib/helpers.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { getCookie } from "@lib/string.ts";
|
||||
import { ReviewResource } from "@lib/marka/schema.ts";
|
||||
|
||||
export const createNewMovie: MenuEntry = {
|
||||
title: "Create new movie",
|
||||
@@ -52,9 +52,9 @@ export const createNewMovie: MenuEntry = {
|
||||
const response = await fetch("/api/movies/" + r.id, {
|
||||
method: "POST",
|
||||
});
|
||||
const movie = await response.json() as Movie;
|
||||
const movie = await response.json() as ReviewResource;
|
||||
unsub();
|
||||
window.location.href = "/movies/" + movie.name;
|
||||
globalThis.location.href = "/movies/" + movie.name;
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { TMDBSeries } from "@lib/types.ts";
|
||||
import { debounce } from "@lib/helpers.ts";
|
||||
import { Series } from "@lib/resource/series.ts";
|
||||
import { getCookie } from "@lib/string.ts";
|
||||
import { ReviewResource } from "@lib/marka/schema.ts";
|
||||
|
||||
export const createNewSeries: MenuEntry = {
|
||||
title: "Create new series",
|
||||
@@ -54,9 +54,9 @@ export const createNewSeries: MenuEntry = {
|
||||
const response = await fetch("/api/series/" + r.id, {
|
||||
method: "POST",
|
||||
});
|
||||
const series = await response.json() as Series;
|
||||
const series = await response.json() as ReviewResource;
|
||||
unsub();
|
||||
window.location.href = "/series/" + series.name;
|
||||
globalThis.location.href = "/series/" + series.name;
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -6,13 +6,16 @@ declare global {
|
||||
}
|
||||
|
||||
export function Link(
|
||||
{ href, children, class: _class, style }: {
|
||||
props: {
|
||||
href?: string;
|
||||
class?: string;
|
||||
style?: preact.JSX.CSSProperties;
|
||||
children: preact.ComponentChildren;
|
||||
"data-thumb"?: string;
|
||||
},
|
||||
) {
|
||||
const { href, children, class: _class, style } = props;
|
||||
const thumbhash = props["data-thumb"];
|
||||
function handleClick() {
|
||||
if (globalThis.loadingTimeout) {
|
||||
return;
|
||||
@@ -41,6 +44,7 @@ export function Link(
|
||||
href={href}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
data-thumb={thumbhash}
|
||||
class={_class}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useRef } from "preact/hooks";
|
||||
import useDebouncedCallback from "@lib/hooks/useDebouncedCallback.ts";
|
||||
import { IconLoader2, IconSearch } from "@components/icons.tsx";
|
||||
import { useEventListener } from "@lib/hooks/useEventListener.ts";
|
||||
import { GenericResource } from "@lib/types.ts";
|
||||
import { resources } from "@lib/resources.ts";
|
||||
import { getCookie } from "@lib/string.ts";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
@@ -11,6 +10,7 @@ import { Rating } from "@components/Rating.tsx";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import Image from "@components/Image.tsx";
|
||||
import { Emoji } from "@components/Emoji.tsx";
|
||||
import { GenericResource } from "@lib/marka/schema.ts";
|
||||
|
||||
export async function fetchQueryResource(url: URL, type = "") {
|
||||
const query = url.searchParams.get("q");
|
||||
@@ -46,7 +46,7 @@ export const RedirectSearchHandler = () => {
|
||||
}, IS_BROWSER ? document?.body : undefined);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
return;
|
||||
};
|
||||
|
||||
const SearchResultImage = ({ src }: { src: string }) => {
|
||||
@@ -67,8 +67,9 @@ export const SearchResultItem = (
|
||||
showEmoji?: boolean;
|
||||
},
|
||||
) => {
|
||||
const resourceType = resources[item.type];
|
||||
const href = resourceType ? `${resourceType.link}/${item.id}` : "";
|
||||
const resourceType = resources[item?.content._type];
|
||||
const href = item?.path.replace("/resources", "").replace(/\.md$/, "");
|
||||
console.log({ item, href });
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
@@ -77,8 +78,9 @@ export const SearchResultItem = (
|
||||
{showEmoji && resourceType
|
||||
? <Emoji class="w-7 h-7" name={resourceType.emoji} />
|
||||
: ""}
|
||||
{item.meta?.image && <SearchResultImage src={item.meta?.image} />}
|
||||
{item?.name}
|
||||
{item.image && <SearchResultImage src={item.image?.url} />}
|
||||
{item.content?.headline || item.content?.name ||
|
||||
item.content?.itemReviewed.name || item?.name}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export const imageTable = sqliteTable("image", {
|
||||
),
|
||||
url: text().notNull(),
|
||||
average: text().notNull(),
|
||||
blurhash: text().notNull(),
|
||||
thumbhash: text().notNull(),
|
||||
mime: text().notNull(),
|
||||
});
|
||||
|
||||
|
||||
@@ -104,12 +104,13 @@ export function debounce<T extends (...args: Parameters<T>) => void>(
|
||||
export function parseRating(rating: string | number) {
|
||||
if (typeof rating === "string") {
|
||||
try {
|
||||
return parseInt(rating);
|
||||
const res = parseInt(rating);
|
||||
if (!Number.isNaN(res)) return res;
|
||||
} catch (_e) {
|
||||
// This is okay
|
||||
}
|
||||
|
||||
return [...rating.matchAll(/⭐/g)].length;
|
||||
return rating.length / 2;
|
||||
}
|
||||
return rating;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createLogger } from "@lib/log/index.ts";
|
||||
import { generateThumbhash } from "@lib/thumbhash.ts";
|
||||
import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts";
|
||||
import path from "node:path";
|
||||
import { ensureDir } from "fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { DATA_DIR } from "@lib/env.ts";
|
||||
import { db } from "@lib/db/sqlite.ts";
|
||||
import { imageTable } from "@lib/db/schema.ts";
|
||||
@@ -13,7 +13,7 @@ import sharp from "npm:sharp@next";
|
||||
const log = createLogger("cache/image");
|
||||
|
||||
const imageDir = path.join(DATA_DIR, "images");
|
||||
await ensureDir(imageDir);
|
||||
await mkdir(imageDir, { recursive: true });
|
||||
|
||||
async function getRemoteImage(imageUrl: string) {
|
||||
try {
|
||||
@@ -100,7 +100,7 @@ async function getLocalImagePath(
|
||||
hostname,
|
||||
pathname.split("/").filter((s) => s.length).join("-"),
|
||||
);
|
||||
await ensureDir(imagePath);
|
||||
await mkdir(imagePath, { recursive: true });
|
||||
|
||||
if (width || height) {
|
||||
imagePath = path.join(imagePath, `${width ?? "-"}x${height ?? "-"}`);
|
||||
@@ -313,7 +313,7 @@ export async function getImage(url: string) {
|
||||
// Store in database
|
||||
const [newImage] = await db.insert(imageTable).values({
|
||||
url: url,
|
||||
blurhash: thumbhash.hash,
|
||||
thumbhash: thumbhash.hash,
|
||||
average: thumbhash.average,
|
||||
mime: imageContent.mediaType,
|
||||
}).returning();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as env from "@lib/env.ts";
|
||||
import { ensureDir } from "fs";
|
||||
import { join } from "node:path";
|
||||
import { getLogLevel, LOG_LEVEL } from "@lib/log/types.ts";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
|
||||
export const LOG_DIR = join(env.DATA_DIR, "logs");
|
||||
|
||||
// Ensure the log directory exists
|
||||
await ensureDir(LOG_DIR);
|
||||
await mkdir(LOG_DIR, { recursive: true });
|
||||
|
||||
export let logLevel = getLogLevel(env.LOG_LEVEL);
|
||||
export function setLogLevel(level: LOG_LEVEL) {
|
||||
|
||||
35
lib/marka.ts
35
lib/marka.ts
@@ -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
80
lib/marka/index.ts
Normal 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
126
lib/marka/schema.ts
Normal 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;
|
||||
};
|
||||
@@ -44,11 +44,3 @@ export function renderMarkdown(doc: string) {
|
||||
allowMath: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function createDocument(
|
||||
path: string,
|
||||
entry: string,
|
||||
mimetype = "image/jpeg",
|
||||
) {
|
||||
console.log("creating", { path, entry, mimetype });
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { OPENAI_API_KEY } from "@lib/env.ts";
|
||||
import { hashString } from "@lib/helpers.ts";
|
||||
import { createCache } from "@lib/cache.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 });
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "npm:zod";
|
||||
import { RecipeResource } from "./marka/schema.ts";
|
||||
|
||||
export const IngredientSchema = z.object({
|
||||
quantity: z.string().describe(
|
||||
@@ -17,7 +18,6 @@ export const IngredientGroupSchema = z.object({
|
||||
export type IngredientGroup = z.infer<typeof IngredientGroupSchema>;
|
||||
|
||||
const recipeSchema = z.object({
|
||||
_type: z.literal("Recipe"),
|
||||
name: z.string().describe(
|
||||
"Title of the Recipe, without the name of the website or author",
|
||||
),
|
||||
@@ -29,6 +29,9 @@ const recipeSchema = z.object({
|
||||
_type: z.literal("Person"),
|
||||
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())
|
||||
.describe("List of ingredients"),
|
||||
recipeInstructions: z.array(z.string()).describe("List of instructions"),
|
||||
@@ -48,7 +51,7 @@ export const recipeResponseSchema = z.union([recipeSchema, noRecipeSchema]);
|
||||
|
||||
export function isValidRecipe(
|
||||
recipe:
|
||||
| Recipe
|
||||
| RecipeResource
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as openai from "@lib/openai.ts";
|
||||
import * as tmdb from "@lib/tmdb.ts";
|
||||
import { GenericResource } from "@lib/types.ts";
|
||||
import { parseRating } from "@lib/helpers.ts";
|
||||
import { createCache } from "@lib/cache.ts";
|
||||
import { GenericResource, ReviewResource } from "./marka/schema.ts";
|
||||
|
||||
type RecommendationResource = {
|
||||
id: string;
|
||||
@@ -18,42 +18,48 @@ type RecommendationResource = {
|
||||
const cache = createCache<RecommendationResource>("recommendations");
|
||||
|
||||
export async function createRecommendationResource(
|
||||
res: GenericResource,
|
||||
res: ReviewResource,
|
||||
description?: string,
|
||||
) {
|
||||
const cacheId = `${res.type}:${res.id.replaceAll(":", "")}`;
|
||||
const cacheId = `${res.type}:${res.name.replaceAll(":", "")}`;
|
||||
const resource = cache.get(cacheId) || {
|
||||
id: res.id,
|
||||
id: res.name,
|
||||
type: res.type,
|
||||
rating: -1,
|
||||
};
|
||||
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) {
|
||||
resource.keywords = keywords;
|
||||
}
|
||||
}
|
||||
|
||||
const { author, date, rating } = res.meta || {};
|
||||
const { author, datePublished, reviewRating } = res.content;
|
||||
|
||||
if (res?.tags) {
|
||||
resource.tags = res.tags;
|
||||
if (res?.content?.keywords) {
|
||||
resource.keywords = res.content.keywords;
|
||||
}
|
||||
|
||||
if (typeof rating !== "undefined") {
|
||||
resource.rating = parseRating(rating);
|
||||
if (typeof reviewRating?.ratingValue !== "undefined") {
|
||||
resource.rating = parseRating(reviewRating?.ratingValue);
|
||||
}
|
||||
|
||||
if (author) {
|
||||
resource.author = author;
|
||||
if (author?.name) {
|
||||
resource.author = author.name;
|
||||
}
|
||||
|
||||
if (description) {
|
||||
resource.description = description;
|
||||
}
|
||||
|
||||
if (date) {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
if (datePublished) {
|
||||
const d = typeof datePublished === "string"
|
||||
? new Date(datePublished)
|
||||
: datePublished;
|
||||
resource.year = d.getFullYear();
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Movie } from "./movies.ts";
|
||||
|
||||
export type Series = Movie;
|
||||
@@ -1,12 +1,8 @@
|
||||
import { resources } from "@lib/resources.ts";
|
||||
import fuzzysort from "npm:fuzzysort";
|
||||
import { GenericResource } from "@lib/types.ts";
|
||||
import { extractHashTags } from "@lib/string.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { Article } from "@lib/resource/articles.ts";
|
||||
import { Recipe } from "@lib/resource/recipes.ts";
|
||||
import { Series } from "@lib/resource/series.ts";
|
||||
import { fetchResource } from "./marka.ts";
|
||||
import { listResources } from "./marka/index.ts";
|
||||
import { GenericResource } from "./marka/schema.ts";
|
||||
|
||||
type ResourceType = keyof typeof resources;
|
||||
|
||||
@@ -48,8 +44,8 @@ export function parseResourceUrl(_url: string | URL): SearchParams | undefined {
|
||||
}
|
||||
|
||||
const isResource = (
|
||||
item: Movie | Series | Article | Recipe | boolean,
|
||||
): item is Movie | Series | Article | Recipe => {
|
||||
item: GenericResource | boolean | undefined,
|
||||
): item is GenericResource => {
|
||||
return !!item;
|
||||
};
|
||||
|
||||
@@ -57,10 +53,10 @@ export async function searchResource(
|
||||
{ q, tags = [], types, rating }: SearchParams,
|
||||
): Promise<GenericResource[]> {
|
||||
const resources = (await Promise.all([
|
||||
(!types || types.includes("movie")) && fetchResource("movies"),
|
||||
(!types || types.includes("series")) && fetchResource("series"),
|
||||
(!types || types.includes("article")) && fetchResource("articles"),
|
||||
(!types || types.includes("recipe")) && fetchResource("recipes"),
|
||||
(!types || types.includes("movie")) && listResources("movies"),
|
||||
(!types || types.includes("series")) && listResources("series"),
|
||||
(!types || types.includes("article")) && listResources("articles"),
|
||||
(!types || types.includes("recipe")) && listResources("recipes"),
|
||||
])).flat().filter(isResource);
|
||||
|
||||
const results: Record<string, GenericResource> = {};
|
||||
@@ -68,27 +64,35 @@ export async function searchResource(
|
||||
for (const resource of resources) {
|
||||
if (
|
||||
!(resource.name in results) &&
|
||||
tags?.length && resource.tags.length &&
|
||||
tags.every((t) => resource.tags.includes(t))
|
||||
tags?.length && resource.content.keywords?.length &&
|
||||
tags.every((t) => resource.content.keywords?.includes(t))
|
||||
) {
|
||||
results[resource.id] = resource;
|
||||
results[resource.name] = resource;
|
||||
}
|
||||
|
||||
if (
|
||||
!(resource.id in results) &&
|
||||
rating && resource?.meta?.rating && resource.meta.rating >= rating
|
||||
!(resource.name in results) &&
|
||||
rating && resource?.content?.reviewRating &&
|
||||
resource.content?.reviewRating?.ratingValue >= rating
|
||||
) {
|
||||
results[resource.id] = resource;
|
||||
results[resource.name] = resource;
|
||||
}
|
||||
}
|
||||
|
||||
if (q.length && q !== "*") {
|
||||
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,
|
||||
});
|
||||
for (const result of fuzzyResult) {
|
||||
results[result.obj.id] = result.obj;
|
||||
results[result.obj.name] = result.obj;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { 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";
|
||||
}
|
||||
} else if (entry.type === "photo") {
|
||||
const photoUrl = `${
|
||||
task.noteName.replace(/\.md$/, "")
|
||||
const photoUrl = `${task.noteName.replace(/\.md$/, "")
|
||||
}/photo-${photoIndex++}.jpg`;
|
||||
|
||||
finalNote += `**Photo**:\n ${photoUrl}\n\n`;
|
||||
@@ -62,13 +61,13 @@ export async function endTask(chatId: string): Promise<string | null> {
|
||||
|
||||
try {
|
||||
for (const entry of photoTasks) {
|
||||
await createDocument(entry.path, entry.content, "image/jpeg");
|
||||
await createResource(entry.path, entry.content);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error("Error creating photo document:", err);
|
||||
}
|
||||
try {
|
||||
await createDocument(task.noteName, finalNote, "text/markdown");
|
||||
await createResource(task.noteName, finalNote);
|
||||
} catch (error) {
|
||||
log.error("Error creating document:", error);
|
||||
return error instanceof Error
|
||||
|
||||
20
lib/types.ts
20
lib/types.ts
@@ -1,4 +1,4 @@
|
||||
import { resources } from "@lib/resources.ts";
|
||||
import { GenericResource, GenericResourceSchema } from "./marka/schema.ts";
|
||||
|
||||
export interface TMDBMovie {
|
||||
adult: boolean;
|
||||
@@ -33,22 +33,6 @@ export interface TMDBSeries {
|
||||
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 {
|
||||
sub: string;
|
||||
name: string;
|
||||
@@ -61,7 +45,7 @@ export interface GiteaOauthUser {
|
||||
export type SearchResult = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: keyof typeof resources;
|
||||
type: GenericResource["content"]["_type"];
|
||||
date?: string;
|
||||
rating: number;
|
||||
tags: string[];
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function App({ Component }: PageProps) {
|
||||
<Component />
|
||||
</Partial>
|
||||
</body>
|
||||
<script src="/thumbnails.js" type="module" async defer />
|
||||
<script src="/thumbhash.js" type="module" async defer />
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { AccessDeniedError } from "@lib/errors.ts";
|
||||
import { getLogs, Log } from "@lib/log/index.ts";
|
||||
import { formatDate } from "@lib/string.ts";
|
||||
import { renderMarkdown } from "@lib/documents.ts";
|
||||
import { renderMarkdown } from "@lib/markdown.ts";
|
||||
|
||||
const renderLog = (t: unknown) =>
|
||||
renderMarkdown(`\`\`\`js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_, ctx) {
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Defuddle } from "defuddle/node";
|
||||
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
|
||||
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
|
||||
import * as openai from "@lib/openai.ts";
|
||||
|
||||
import { Article } from "@lib/resource/articles.ts";
|
||||
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
|
||||
import {
|
||||
extractYoutubeId,
|
||||
@@ -12,8 +10,9 @@ import {
|
||||
toUrlSafeString,
|
||||
} from "@lib/string.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 { ArticleResource } from "@lib/marka/schema.ts";
|
||||
|
||||
const log = createLogger("api/article");
|
||||
|
||||
@@ -93,7 +92,7 @@ async function processCreateYoutubeVideo(
|
||||
|
||||
const id = newId || youtubeId;
|
||||
|
||||
const newArticle: Article = {
|
||||
const newArticle: ArticleResource["content"] = {
|
||||
_type: "Article",
|
||||
headline: video.snippet.title,
|
||||
articleBody: video.snippet.description,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET() {
|
||||
|
||||
@@ -80,13 +80,9 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
|
||||
});
|
||||
}
|
||||
|
||||
const imageUrl = params.image.startsWith("resources")
|
||||
? `https://marka.max-richter.dev/${params.image.replace(/^\//, "")}`
|
||||
: params.image;
|
||||
log.debug("Processing image request:", { params });
|
||||
|
||||
log.debug("Processing image request:", { imageUrl, params });
|
||||
|
||||
const image = await getImageContent(imageUrl, params);
|
||||
const image = await getImageContent(params.image, params);
|
||||
|
||||
// Generate ETag based on image content
|
||||
const eTag = await generateETag(image.content);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import * as tmdb from "@lib/tmdb.ts";
|
||||
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
||||
import { isString, safeFileName } from "@lib/string.ts";
|
||||
import { 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 = {
|
||||
async GET(_, ctx) {
|
||||
@@ -50,7 +50,7 @@ export const handler: Handlers = {
|
||||
);
|
||||
}
|
||||
|
||||
const movie: Movie = {
|
||||
const movie: ReviewResource["content"] = {
|
||||
_type: "Review",
|
||||
image: `resources/${finalPath}`,
|
||||
datePublished: releaseDate,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
NotFoundError,
|
||||
} from "@lib/errors.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 (
|
||||
req: Request,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_, ctx) {
|
||||
|
||||
@@ -3,15 +3,15 @@ import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
|
||||
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
|
||||
import * as openai from "@lib/openai.ts";
|
||||
import { createLogger } from "@lib/log/index.ts";
|
||||
import { Recipe } from "@lib/resource/recipes.ts";
|
||||
import recipeSchema from "@lib/recipeSchema.ts";
|
||||
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
|
||||
import { safeFileName } from "@lib/string.ts";
|
||||
import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts";
|
||||
import z from "zod";
|
||||
import { createResource } from "@lib/marka.ts";
|
||||
import { createResource } from "@lib/marka/index.ts";
|
||||
import { webScrape } from "@lib/webScraper.ts";
|
||||
import { Defuddle } from "defuddle/node";
|
||||
import { RecipeResource } from "@lib/marka/schema.ts";
|
||||
|
||||
const log = createLogger("api/article");
|
||||
|
||||
@@ -51,7 +51,7 @@ async function processCreateRecipeFromUrl(
|
||||
recipe = await openai.extractRecipe(result.content);
|
||||
}
|
||||
|
||||
const id = safeFileName(recipe?.title || "");
|
||||
const id = safeFileName(recipe?.name || "");
|
||||
|
||||
if (!recipe) {
|
||||
streamResponse.enqueue("failed to parse recipe");
|
||||
@@ -59,23 +59,13 @@ async function processCreateRecipeFromUrl(
|
||||
return;
|
||||
}
|
||||
|
||||
const newRecipe: Recipe = {
|
||||
const newRecipe: RecipeResource["content"] = {
|
||||
...recipe,
|
||||
_type: "Recipe",
|
||||
name: recipe?.title,
|
||||
description: recipe?.description,
|
||||
recipeIngredient: recipe?.ingredients || [],
|
||||
recipeInstructions: recipe?.instructions || [],
|
||||
keywords: recipe.tags || [],
|
||||
image: recipe?.image,
|
||||
totalTime: recipe?.totalTime
|
||||
? `${recipe?.totalTime?.toString()} minutes`
|
||||
: undefined,
|
||||
url: fetchUrl,
|
||||
author: {
|
||||
_type: "Person",
|
||||
name: recipe?.author,
|
||||
},
|
||||
recipeYield: recipe?.servings,
|
||||
};
|
||||
|
||||
if (newRecipe?.image && newRecipe.image.length > 5) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET() {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { createStreamResponse } from "@lib/helpers.ts";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import * as tmdb from "@lib/tmdb.ts";
|
||||
import {
|
||||
createRecommendationResource,
|
||||
getRecommendation,
|
||||
} from "@lib/recommendation.ts";
|
||||
import { AccessDeniedError } from "@lib/errors.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { listResources } from "@lib/marka/index.ts";
|
||||
import { ReviewResource } from "@lib/marka/schema.ts";
|
||||
|
||||
async function processUpdateRecommendations(
|
||||
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.reviewRating) return false;
|
||||
if (!m.content.tmdbId) return false;
|
||||
return true;
|
||||
}) as Movie[];
|
||||
}) as ReviewResource[];
|
||||
|
||||
streamResponse.enqueue("Fetched all movies");
|
||||
|
||||
@@ -27,22 +27,23 @@ async function processUpdateRecommendations(
|
||||
const total = movies.length;
|
||||
|
||||
await Promise.all(movies.map(async (movie) => {
|
||||
if (!movie.meta.tmdbId) return;
|
||||
if (!movie.meta.rating) return;
|
||||
const recommendation = getRecommendation(movie.id, movie.type);
|
||||
if (!movie.content.tmdbId) return;
|
||||
if (!movie.content.reviewRating) return;
|
||||
const recommendation = getRecommendation(movie.name, movie.type);
|
||||
if (recommendation) {
|
||||
done++;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const movieDetails = await tmdb.getMovie(movie.meta.tmdbId);
|
||||
const movieDetails = await tmdb.getMovie(movie.content.tmdbId);
|
||||
await createRecommendationResource(movie, movieDetails.overview);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
done++;
|
||||
streamResponse.enqueue(
|
||||
`${Math.floor((done / total) * 100)}% [${done + 1}/${total}] ${movie.id}`,
|
||||
`${Math.floor((done / total) * 100)}% [${done + 1
|
||||
}/${total}] ${movie.name}`,
|
||||
);
|
||||
})).catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
@@ -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 { isString, safeFileName } from "@lib/string.ts";
|
||||
import { AccessDeniedError } from "@lib/errors.ts";
|
||||
import { Series } from "@lib/resource/series.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 = {
|
||||
async GET(_, ctx) {
|
||||
@@ -49,7 +49,7 @@ export const handler: Handlers = {
|
||||
);
|
||||
}
|
||||
|
||||
const series: Series = {
|
||||
const series: ReviewResource["content"] = {
|
||||
_type: "Review",
|
||||
image: `resources/${finalPath}`,
|
||||
datePublished: releaseDate,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
} 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 => {
|
||||
return typeof input === "string";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { json } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET() {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { MainLayout } from "@components/layouts/main.tsx";
|
||||
import { Article } from "@lib/resource/articles.ts";
|
||||
import { KMenu } from "@islands/KMenu.tsx";
|
||||
import { YoutubePlayer } from "@components/Youtube.tsx";
|
||||
import { HashTags } from "@components/HashTags.tsx";
|
||||
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 PageHero from "@components/PageHero.tsx";
|
||||
import { Star } from "@components/Stars.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) {
|
||||
const article = await fetchResource(`articles/${ctx.params.name}.md`);
|
||||
if (!article) {
|
||||
@@ -30,11 +30,9 @@ export default function Greet(
|
||||
const { author = "", date = "", articleBody = "" } = article?.content || {};
|
||||
|
||||
const content = renderMarkdown(
|
||||
removeImage(articleBody, article.content.image),
|
||||
removeImage(articleBody, article.image?.url),
|
||||
);
|
||||
|
||||
console.log({ article });
|
||||
|
||||
return (
|
||||
<MainLayout
|
||||
url={props.url}
|
||||
@@ -46,8 +44,8 @@ export default function Greet(
|
||||
<MetaTags resource={article} />
|
||||
|
||||
<PageHero
|
||||
image={article.content.image}
|
||||
thumbnail={article.content.thumbnail}
|
||||
image={article.image?.url}
|
||||
thumbhash={article.image?.thumbhash}
|
||||
>
|
||||
<PageHero.Header>
|
||||
<PageHero.BackLink href="/articles" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
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 { Grid } from "@components/Grid.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 { ResourceCard } from "@components/Card.tsx";
|
||||
import { Link } from "@islands/Link.tsx";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { listResources } from "@lib/marka/index.ts";
|
||||
|
||||
export const handler: Handlers<
|
||||
{ articles: Article[] | null; searchResults?: GenericResource[] }
|
||||
{ articles: ArticleResource[] | null; searchResults?: GenericResource[] }
|
||||
> = {
|
||||
async GET(req, ctx) {
|
||||
const { content: articles } = await fetchResource("articles");
|
||||
const articles = await listResources("articles");
|
||||
const searchParams = parseResourceUrl(req.url);
|
||||
const searchResults = searchParams &&
|
||||
await searchResource({ ...searchParams, types: ["article"] });
|
||||
@@ -25,7 +25,7 @@ export const handler: Handlers<
|
||||
|
||||
export default function Greet(
|
||||
props: PageProps<
|
||||
{ articles: Article[] | null; searchResults: GenericResource[] }
|
||||
{ articles: ArticleResource[] | null; searchResults: GenericResource[] }
|
||||
>,
|
||||
) {
|
||||
const { articles, searchResults } = props.data;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PageProps, RouteContext } from "$fresh/server.ts";
|
||||
import { MainLayout } from "@components/layouts/main.tsx";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
||||
import { ReviewResource, ReviewSchema } from "@lib/marka/schema.ts";
|
||||
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
|
||||
import { KMenu } from "@islands/KMenu.tsx";
|
||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { Recommendations } from "@islands/Recommendations.tsx";
|
||||
@@ -9,20 +9,22 @@ import PageHero from "@components/PageHero.tsx";
|
||||
import { Star } from "@components/Stars.tsx";
|
||||
import { MetaTags } from "@components/MetaTags.tsx";
|
||||
import { parseRating } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { fetchResource } from "@lib/marka/index.ts";
|
||||
|
||||
export default async function Greet(
|
||||
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
|
||||
props: PageProps<{ movie: ReviewResource; session: Record<string, string> }>,
|
||||
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;
|
||||
|
||||
if (!movie) {
|
||||
return ctx.renderNotFound();
|
||||
}
|
||||
|
||||
const { author = "", date = "" } = movie.content;
|
||||
const { author = "", datePublished = "" } = movie.content;
|
||||
|
||||
const content = renderMarkdown(
|
||||
removeImage(movie.content.reviewBody || "", movie.content.image),
|
||||
@@ -34,8 +36,8 @@ export default async function Greet(
|
||||
<KMenu type="main" context={movie} />
|
||||
<MetaTags resource={movie} />
|
||||
<PageHero
|
||||
image={movie.content.image}
|
||||
thumbnail={movie.content.thumbnail}
|
||||
image={movie.image?.url}
|
||||
thumbhash={movie.image?.thumbhash}
|
||||
>
|
||||
<PageHero.Header>
|
||||
<PageHero.BackLink href="/movies" />
|
||||
@@ -60,7 +62,7 @@ export default async function Greet(
|
||||
>
|
||||
{movie.content.reviewRating && (
|
||||
<Star
|
||||
rating={parseRating(movie.content.reviewRating?.ratingValue)}
|
||||
rating={parseRating(movie.content?.reviewRating?.ratingValue)}
|
||||
/>
|
||||
)}
|
||||
</PageHero.Subline>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Grid } from "@components/Grid.tsx";
|
||||
import { IconArrowLeft } from "@components/icons.tsx";
|
||||
@@ -7,15 +7,15 @@ import { KMenu } from "@islands/KMenu.tsx";
|
||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { GenericResource } from "@lib/types.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";
|
||||
|
||||
export default async function Greet(
|
||||
export default async function MovieIndex(
|
||||
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 searchResults = searchParams &&
|
||||
await searchResource({ ...searchParams, types: ["movie"] });
|
||||
@@ -47,8 +47,8 @@ export default async function Greet(
|
||||
<h3 class="text-2xl text-white font-light">🍿 Movies</h3>
|
||||
</header>
|
||||
<Grid>
|
||||
{movies?.map((doc) => {
|
||||
return <ResourceCard res={doc} />;
|
||||
{movies?.map((doc, i) => {
|
||||
return <ResourceCard key={i} res={doc} />;
|
||||
})}
|
||||
</Grid>
|
||||
</MainLayout>
|
||||
|
||||
@@ -8,10 +8,10 @@ import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { KMenu } from "@islands/KMenu.tsx";
|
||||
import PageHero from "@components/PageHero.tsx";
|
||||
import { Star } from "@components/Stars.tsx";
|
||||
import { renderMarkdown } from "@lib/documents.ts";
|
||||
import { renderMarkdown } from "@lib/markdown.ts";
|
||||
import { isValidRecipe } from "@lib/recipeSchema.ts";
|
||||
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> = {
|
||||
async GET(_, ctx) {
|
||||
@@ -86,8 +86,8 @@ export default function Page(
|
||||
<MetaTags resource={recipe} />
|
||||
|
||||
<PageHero
|
||||
image={recipe.content?.image}
|
||||
thumbnail={recipe.content?.thumbnail}
|
||||
image={recipe.image?.url}
|
||||
thumbhash={recipe.image?.thumbhash}
|
||||
>
|
||||
<PageHero.Header>
|
||||
<PageHero.BackLink href="/recipes" />
|
||||
|
||||
@@ -7,14 +7,14 @@ import { KMenu } from "@islands/KMenu.tsx";
|
||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { GenericResource } from "@lib/types.ts";
|
||||
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";
|
||||
|
||||
export const handler: Handlers<
|
||||
{ recipes: Recipe[] | null; searchResults?: GenericResource[] }
|
||||
> = {
|
||||
async GET(req, ctx) {
|
||||
const { content: recipes } = await fetchResource("recipes");
|
||||
const recipes = await listResources("recipes");
|
||||
const searchParams = parseResourceUrl(req.url);
|
||||
const searchResults = searchParams &&
|
||||
await searchResource({ ...searchParams, types: ["recipe"] });
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { MainLayout } from "@components/layouts/main.tsx";
|
||||
import { HashTags } from "@components/HashTags.tsx";
|
||||
import { removeImage, renderMarkdown } from "@lib/documents.ts";
|
||||
import { Series } from "@lib/resource/series.ts";
|
||||
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
|
||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { KMenu } from "@islands/KMenu.tsx";
|
||||
import PageHero from "@components/PageHero.tsx";
|
||||
import { Star } from "@components/Stars.tsx";
|
||||
import { MetaTags } from "@components/MetaTags.tsx";
|
||||
import { parseRating } from "@lib/helpers.ts";
|
||||
import { fetchResource } from "@lib/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) {
|
||||
const serie = await fetchResource(`series/${ctx.params.name}.md`);
|
||||
|
||||
@@ -44,8 +44,8 @@ export default function Greet(
|
||||
|
||||
<MetaTags resource={serie} />
|
||||
<PageHero
|
||||
image={serie.content?.image}
|
||||
thumbnail={serie.content?.thumbnail}
|
||||
image={serie.image?.url}
|
||||
thumbhash={serie.image?.thumbhash}
|
||||
>
|
||||
<PageHero.Header>
|
||||
<PageHero.BackLink href="/series" />
|
||||
|
||||
@@ -2,19 +2,18 @@ import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { MainLayout } from "@components/layouts/main.tsx";
|
||||
import { Grid } from "@components/Grid.tsx";
|
||||
import { IconArrowLeft } from "@components/icons.tsx";
|
||||
import { Series } from "@lib/resource/series.ts";
|
||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { KMenu } from "@islands/KMenu.tsx";
|
||||
import { ResourceCard } from "@components/Card.tsx";
|
||||
import { GenericResource } from "@lib/types.ts";
|
||||
import { fetchResource } from "@lib/marka.ts";
|
||||
import { listResources } from "@lib/marka/index.ts";
|
||||
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
||||
import { GenericResource, ReviewResource } from "@lib/marka/schema.ts";
|
||||
|
||||
export const handler: Handlers<
|
||||
{ series: Series[] | null; searchResults?: GenericResource[] }
|
||||
{ series: ReviewResource[] | null; searchResults?: GenericResource[] }
|
||||
> = {
|
||||
async GET(req, ctx) {
|
||||
const { content: series } = await fetchResource("series");
|
||||
const series = await listResources("series");
|
||||
const searchParams = parseResourceUrl(req.url);
|
||||
const searchResults = searchParams &&
|
||||
await searchResource({ ...searchParams, types: ["series"] });
|
||||
@@ -24,7 +23,7 @@ export const handler: Handlers<
|
||||
|
||||
export default function Greet(
|
||||
props: PageProps<
|
||||
{ series: Series[] | null; searchResults: GenericResource[] }
|
||||
{ series: ReviewResource[] | null; searchResults: GenericResource[] }
|
||||
>,
|
||||
) {
|
||||
const { series, searchResults } = props.data;
|
||||
@@ -50,8 +49,8 @@ export default function Greet(
|
||||
<h3 class="text-2xl text-white font-light">🎥 Series</h3>
|
||||
</header>
|
||||
<Grid>
|
||||
{series?.map((doc) => {
|
||||
return <ResourceCard sublink="series" res={doc} />;
|
||||
{series?.map((doc, i) => {
|
||||
return <ResourceCard key={i} sublink="series" res={doc} />;
|
||||
})}
|
||||
</Grid>
|
||||
</MainLayout>
|
||||
|
||||
@@ -226,7 +226,7 @@ function rgbaToDataURL(w, h, rgba) {
|
||||
return "data:image/png;base64," + btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function updateThumbnailImages() {
|
||||
function updateThumbhashImages() {
|
||||
document.querySelectorAll("[data-thumb]").forEach((entry) => {
|
||||
const hash = entry.getAttribute("data-thumb");
|
||||
|
||||
@@ -257,7 +257,7 @@ function updateThumbnailImages() {
|
||||
child.style.opacity = 1;
|
||||
child.style.filter = "blur(0px)";
|
||||
setTimeout(() => {
|
||||
entry.style.background = "";
|
||||
//entry.style.background = "";
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
@@ -265,5 +265,5 @@ function updateThumbnailImages() {
|
||||
});
|
||||
}
|
||||
|
||||
globalThis.addEventListener("load", updateThumbnailImages);
|
||||
globalThis.addEventListener("loading-finished", updateThumbnailImages);
|
||||
globalThis.addEventListener("load", updateThumbhashImages);
|
||||
globalThis.addEventListener("loading-finished", updateThumbhashImages);
|
||||
Reference in New Issue
Block a user