Compare commits
2 Commits
81ebc8f5e0
...
21841b4dc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21841b4dc4
|
||
|
|
e6b90cb785
|
@@ -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, "")}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
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,
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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 "";
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
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,
|
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 { 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 });
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 {
|
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[];
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"] });
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user