Compare commits

..

3 Commits

Author SHA1 Message Date
Max Richter
8d712322c0 fix: make it work with new vite 2026-01-10 19:28:09 +01:00
Max Richter
694feb083d fix: remove all linter errors 2026-01-10 15:06:43 +01:00
Max Richter
e55f787a29 feat: trying to add hashes to scripts 2026-01-10 13:03:29 +01:00
121 changed files with 4053 additions and 1905 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,3 @@
# dotenv environment variable files
.env
.env.development.local
.env.test.local
@@ -10,3 +9,4 @@ data-dev/
_fresh/
node_modules/
mise.toml

View File

@@ -1,4 +1,4 @@
FROM denoland/deno:2.5.4 AS build
FROM denoland/deno:2.6.4 AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ffmpeg && \
@@ -15,7 +15,7 @@ COPY . .
ENV DATA_DIR=/app/data
RUN mkdir -p $DATA_DIR && \
deno install --allow-import --allow-ffi --allow-scripts=npm:sharp -e main.ts &&\
deno install --allow-import --allow-ffi -e main.ts &&\
sed -i -e 's/"deno"/"no-deno"/' node_modules/@libsql/client/package.json &&\
deno task build

View File

@@ -5,18 +5,13 @@ Started" guide here: https://fresh.deno.dev/docs/getting-started
### Usage
Make sure to install Deno: https://deno.land/manual/getting_started/installation
Make sure to install Deno:
https://docs.deno.com/runtime/getting_started/installation
Then start the project:
Then start the project in development mode:
```
deno task start
deno task dev
```
This will watch the project directory and restart as necessary.
## FIX
```
sed -i -e 's/"deno"/"no-deno"/' node_modules/@libsql/client/package.json
```

1
assets/styles.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

2
client.ts Normal file
View File

@@ -0,0 +1,2 @@
// Import CSS files here for hot module reloading to work.
import "./assets/styles.css";

View File

@@ -1,12 +1,10 @@
import { JSX } from "preact";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { ButtonHTMLAttributes } from "preact";
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
export function Button(props: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
disabled={!IS_BROWSER || props.disabled}
class={`px-2 py-1 ${props.class ? props.class : " "}`}
class={`cursor-pointer px-2 py-1 ${props.class ? props.class : ""}`}
/>
);
}

View File

@@ -1,7 +1,6 @@
import { isYoutubeLink } from "@lib/string.ts";
import { IconBrandYoutube } from "@components/icons.tsx";
import { SmallRating } from "@components/Rating.tsx";
import { Link } from "@islands/Link.tsx";
import { parseRating } from "@lib/helpers.ts";
import { GenericResource, getNameOfResource } from "@lib/marka/schema.ts";
@@ -24,7 +23,7 @@ export function Card(
rating?: number;
},
) {
const backgroundStyle: preact.JSX.CSSProperties = {
const backgroundStyle: preact.CSSProperties = {
backgroundSize: "cover",
backgroundColor: backgroundColor,
};
@@ -36,7 +35,7 @@ export function Card(
}
return (
<Link
<a
href={link}
style={backgroundStyle}
data-thumb={thumbhash}
@@ -88,7 +87,7 @@ export function Card(
)}
</div>
<div class="absolute inset-x-0 bottom-0 h-3/4" />
</Link>
</a>
);
}

View File

@@ -1,67 +1,5 @@
import { Signal, useSignal } from "@preact/signals";
import { useId, useState } from "preact/hooks";
interface CheckboxProps {
label: string;
isChecked?: boolean;
onChange: (isChecked: boolean) => void;
}
const Checkbox2: preact.FunctionalComponent<CheckboxProps> = (
{ label, isChecked = false, onChange },
) => {
const [checked, setChecked] = useState(isChecked);
const toggleCheckbox = () => {
const newChecked = !checked;
setChecked(newChecked);
onChange(newChecked);
};
return (
<div
class="flex items-center rounded-xl p-1 pl-4"
style={{ background: "var(--background)", color: "var(--foreground)" }}
>
<span>
{label}
</span>
<label
class="relative flex cursor-pointer items-center rounded-full p-3"
for="checkbox"
data-ripple-dark="true"
>
<input
type="checkbox"
class="before:content[''] peer relative h-5 w-5 cursor-pointer appearance-none rounded-md border border-blue-gray-200 transition-all before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:bg-blue-gray-500 before:opacity-0 before:transition-opacity hover:before:opacity-10"
id="checkbox"
checked
/>
<div
class={`pointer-events-none absolute top-2/4 left-2/4 -translate-y-2/4 -translate-x-2/4 text-white opacity-${
checked ? 100 : 0
} transition-opacity`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5"
viewBox="0 0 20 20"
fill="white"
stroke="currentColor"
stroke-width="1"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
>
</path>
</svg>
</div>
</label>
</div>
);
};
import { useId } from "preact/hooks";
const Checkbox = (
{ label, checked = useSignal(false) }: {

View File

@@ -1,4 +1,4 @@
import { asset } from "$fresh/runtime.ts";
import { asset } from "fresh/runtime";
export const Emoji = (props: { class?: string; name: string }) => {
return props.name

View File

@@ -1,5 +1,4 @@
import { asset } from "$fresh/runtime.ts";
import * as CSS from "csstype";
import { asset } from "fresh/runtime";
interface ResponsiveAttributes {
srcset: string;
@@ -38,7 +37,7 @@ const Image = (
fill?: boolean;
width?: number | string;
height?: number | string;
style?: CSS.HtmlAttributes;
style?: preact.CSSProperties;
},
) => {
const responsiveAttributes = generateResponsiveAttributes(

View File

@@ -1,4 +1,4 @@
import { Head } from "$fresh/runtime.ts";
import { Head } from "fresh/runtime";
import { GenericResource, getNameOfResource } from "@lib/marka/schema.ts";
import { formatDate } from "@lib/string.ts";
@@ -60,6 +60,7 @@ export function MetaTags({ resource }: { resource: GenericResource }) {
/>
<script
type="application/ld+json"
// deno-lint-ignore react-no-danger
dangerouslySetInnerHTML={{ __html: jsonLd }}
/>
</Head>

View File

@@ -21,7 +21,7 @@ function Wrapper(
return (
<div
class={`flex justify-between flex-col relative w-full ${
image ? "min-h-[400px]" : "min-h-[200px]"
image ? "min-h-100" : "min-h-50"
} rounded-3xl overflow-hidden`}
>
<HeroContext.Provider value={{ image }}>
@@ -62,7 +62,7 @@ function Title(
>
{children}
{link &&
<IconExternalLink />}
<IconExternalLink class="h-6 w-6" />}
</h2>
</OuterTag>
);
@@ -110,7 +110,7 @@ function Subline(
const ctx = useContext(HeroContext);
return (
<div
class={`relative flex items-center z-10 flex gap-5 font-sm text-light mt-3`}
class={`relative items-center z-10 flex gap-5 font-sm text-light mt-3`}
style={{ color: ctx.image ? "#1F1F1F" : "white" }}
>
{children}

View File

@@ -10,9 +10,9 @@ export const Star = (
>
{Array.from({ length: max }).map((_, i) => {
if ((i + 1) <= rating) {
return <IconStarFilled class="w-4 h-4" />;
return <IconStarFilled key={i} class="w-4 h-4" />;
}
return <IconStar class="w-4 h-4" />;
return <IconStar key={i} class="w-4 h-4" />;
})}
</div>
);

View File

@@ -1,21 +1,23 @@
export { default as IconStar } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/star.tsx";
export { default as IconStarFilled } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/star-filled.tsx";
export { default as IconExternalLink } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx";
export { default as IconArrowNarrowLeft } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-narrow-left.tsx";
export { default as IconEdit } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/edit.tsx";
export { default as IconArrowLeft } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
export { default as IconError404 } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/error-404.tsx";
export { default as IconSquareRoundedPlus } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/square-rounded-plus.tsx";
export { default as IconReportSearch } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/report-search.tsx";
export { default as IconRefresh } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/refresh.tsx";
export { default as IconCirclePlus } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/circle-plus.tsx";
export { default as IconCircleMinus } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/circle-minus.tsx";
export { default as IconLoader2 } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/loader-2.tsx";
export { default as IconLogin } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/login.tsx";
export { default as IconLogout } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/logout.tsx";
export { default as IconSearch } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/search.tsx";
export { default as IconGhost } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/ghost.tsx";
export { default as IconBrandYoutube } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/brand-youtube.tsx";
export { default as IconWand } from "https://deno.land/x/tabler_icons_tsx@0.0.5/tsx/wand.tsx";
export { default as IconMenu2 } from "https://deno.land/x/tabler_icons_tsx@0.0.5/tsx/menu-2.tsx";
export { default as IconAlertCircle } from "https://deno.land/x/tabler_icons_tsx@0.0.5/tsx/alert-circle.tsx";
export {
TbAlertCircle as IconAlertCircle,
TbArrowLeft as IconArrowLeft,
TbArrowNarrowLeft as IconArrowNarrowLeft,
TbBrandYoutube as IconBrandYoutube,
TbCircleMinus as IconCircleMinus,
TbCirclePlus as IconCirclePlus,
TbEdit as IconEdit,
TbError404 as IconError404,
TbExternalLink as IconExternalLink,
TbGhost as IconGhost,
TbLoader2 as IconLoader2,
TbLogin as IconLogin,
TbLogout as IconLogout,
TbMenu2 as IconMenu2,
TbRefresh as IconRefresh,
TbReportSearch as IconReportSearch,
TbSearch as IconSearch,
TbSquareRoundedPlus as IconSquareRoundedPlus,
TbStar as IconStar,
TbStarFilled as IconStarFilled,
TbWand as IconWand,
} from "@preact-icons/tb";

View File

@@ -36,5 +36,5 @@ export const MainLayout = (
);
}
return children;
return <>{children}</>;
};

View File

@@ -4,10 +4,10 @@ services:
context: .
dockerfile: Dockerfile
volumes:
- .:/app # Mount the local directory to /app in the container
working_dir: /app # Set the working directory inside the container to /app
command: run --env-file -A --watch=static/,routes/ dev.ts # Custom start command
- .:/app
working_dir: /app
command: deno task dev --host 0.0.0.0
ports:
- "8000:8000" # Expose the container port
- "8000:8000"
environment:
- DATA_DIR=/app/data # Set the environment variable inside the container
- DATA_DIR=/app/data

116
deno.json
View File

@@ -1,58 +1,100 @@
{
"lock": false,
"nodeModulesDir": "auto",
"unstable": ["cron"],
"nodeModulesDir": "manual",
"tasks": {
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"dev": "deno run --env-file -A --watch=static/,routes/ dev.ts",
"start": "deno run --env-file -A main.ts",
"check": "deno fmt --check . && deno lint . && deno check",
"dev": "vite",
"db": "deno run --env-file -A npm:drizzle-kit",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
"build": "vite build",
"start": "deno serve -A _fresh/server.js",
"update": "deno run -A -r jsr:@fresh/update ."
},
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"lint": {
"rules": {
"tags": [
"fresh",
"recommended"
]
}
},
"exclude": [
"**/_fresh/*"
],
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
"@cmd-johnson/oauth2-client": "jsr:@cmd-johnson/oauth2-client@^2.0.0",
"@components": "./components",
"@components/": "./components/",
"@denosaurs/emoji": "jsr:@denosaurs/emoji@^0.3.1",
"@deno/gfm": "jsr:@deno/gfm@^0.11.0",
"@islands": "./islands",
"@islands/": "./islands/",
"@lib": "./lib",
"@lib/": "./lib/",
"@libsql/client": "npm:@libsql/client@^0.14.0",
"@openai/openai": "jsr:@openai/openai@^6.7.0",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"@std/http": "jsr:@std/http@^1.0.12",
"@std/yaml": "jsr:@std/yaml@^1.0.5",
"csstype": "npm:csstype@^3.1.3",
"@/": "./",
"@libsql/client": "npm:@libsql/client@^0.17.0",
"@openai/openai": "jsr:@openai/openai@^6.16.0",
"@preact-icons/tb": "jsr:@preact-icons/tb@^1.0.14",
"@std/http": "jsr:@std/http@^1.0.23",
"@std/media-types": "jsr:@std/media-types@^1.1.0",
"@zaubrik/djwt": "jsr:@zaubrik/djwt@^3.0.2",
"defuddle": "npm:defuddle@^0.6.6",
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
"drizzle-orm": "npm:drizzle-orm@^0.38.3",
"drizzle-kit": "npm:drizzle-kit@^0.31.8",
"drizzle-orm": "npm:drizzle-orm@^0.45.1",
"fresh": "jsr:@fresh/core@^2.2.0",
"fuzzysort": "npm:fuzzysort@^3.1.0",
"jsdom": "npm:jsdom@^24.1.3",
"gfm": "jsr:@deno/gfm@0.11.0",
"jsdom": "npm:jsdom@^27.4.0",
"moviedb-promise": "npm:moviedb-promise@^4.0.7",
"parse-ingredient": "npm:parse-ingredient@^1.3.1",
"playwright": "npm:playwright@^1.49.1",
"playwright-extra": "npm:playwright-extra@^4.3.6",
"preact": "https://esm.sh/preact@10.22.0",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
"preact/": "https://esm.sh/preact@10.22.0/",
"gfm": "jsr:@deno/gfm",
"preact": "npm:preact@^10.27.2",
"@preact/signals": "npm:@preact/signals@^2.5.0",
"@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.8",
"puppeteer-extra-plugin-stealth": "npm:puppeteer-extra-plugin-stealth@^2.11.2",
"tailwindcss": "npm:tailwindcss@^3.4.17",
"tailwindcss/": "npm:/tailwindcss@^3.4.17/",
"tailwindcss/plugin": "npm:/tailwindcss@^3.4.17/plugin.js",
"camelcase-css": "npm:camelcase-css",
"sharp": "npm:sharp@^0.34.5",
"thumbhash": "npm:thumbhash@^0.1.1",
"tsx": "npm:tsx@^4.19.2",
"turndown": "npm:turndown@^7.2.2",
"yaml": "https://deno.land/std@0.197.0/yaml/mod.ts",
"zod": "npm:zod@^3.24.1",
"fs": "https://deno.land/std/fs/mod.ts"
"vite": "npm:vite@^7.1.3",
"tailwindcss": "npm:tailwindcss@^4.1.10",
"@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.12",
"zod": "npm:zod@^4.3.5"
},
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"exclude": ["**/_fresh/*"]
"compilerOptions": {
"lib": [
"dom",
"dom.asynciterable",
"dom.iterable",
"deno.ns"
],
"jsx": "precompile",
"jsxImportSource": "preact",
"jsxPrecompileSkipElements": [
"a",
"img",
"source",
"body",
"html",
"head",
"title",
"meta",
"script",
"link",
"style",
"base",
"noscript",
"template"
],
"types": [
"vite/client"
]
},
"allowScripts": {
"allow": [
"npm:sharp@0.34.5"
],
"deny": [
"npm:esbuild@0.18.20",
"npm:esbuild@0.25.12",
"npm:esbuild@0.25.7",
"npm:esbuild@0.27.2"
]
}
}

2910
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
dev.ts
View File

@@ -1,8 +0,0 @@
#!/usr/bin/env -S deno run -A --watch=static/,routes/
import dev from "$fresh/dev.ts";
import config from "./fresh.config.ts";
await dev(import.meta.url, "./main.ts", config);
Deno.exit(0);

View File

@@ -0,0 +1,7 @@
DROP INDEX "key_idx";--> statement-breakpoint
DROP INDEX "scope_idx";--> statement-breakpoint
DROP INDEX "name_idx";--> statement-breakpoint
ALTER TABLE `image` ALTER COLUMN "created_at" TO "created_at" integer DEFAULT (unixepoch());--> statement-breakpoint
CREATE INDEX `key_idx` ON `cache` (`key`);--> statement-breakpoint
CREATE INDEX `scope_idx` ON `cache` (`scope`);--> statement-breakpoint
CREATE INDEX `name_idx` ON `document` (`name`);

View File

@@ -0,0 +1,309 @@
{
"version": "6",
"dialect": "sqlite",
"id": "11bbbc9d-3c0c-4fb9-893f-c87d5b8660d0",
"prevId": "685b57ca-45e0-4373-baee-fc3abb4f2d74",
"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": "(unixepoch())"
},
"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": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -78,6 +78,13 @@
"when": 1762099260474,
"tag": "0010_youthful_tyrannus",
"breakpoints": true
},
{
"idx": 11,
"version": "6",
"when": 1768058169808,
"tag": "0011_reflective_frank_castle",
"breakpoints": true
}
]
}

View File

@@ -1,5 +0,0 @@
import { defineConfig } from "$fresh/server.ts";
import tailwind from "$fresh/plugins/tailwind.ts";
export default defineConfig({
plugins: [tailwind()],
});

View File

@@ -1,145 +0,0 @@
// DO NOT EDIT. This file is generated by Fresh.
// This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`.
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $_layout from "./routes/_layout.tsx";
import * as $_middleware from "./routes/_middleware.ts";
import * as $admin_cache_index from "./routes/admin/cache/index.tsx";
import * as $admin_log_index from "./routes/admin/log/index.tsx";
import * as $admin_performance_index from "./routes/admin/performance/index.tsx";
import * as $api_articles_name_ from "./routes/api/articles/[name].ts";
import * as $api_articles_create_index from "./routes/api/articles/create/index.ts";
import * as $api_articles_enhance_name_ from "./routes/api/articles/enhance/[name].ts";
import * as $api_articles_index from "./routes/api/articles/index.ts";
import * as $api_auth_callback from "./routes/api/auth/callback.ts";
import * as $api_auth_login from "./routes/api/auth/login.ts";
import * as $api_auth_logout from "./routes/api/auth/logout.ts";
import * as $api_cache from "./routes/api/cache.ts";
import * as $api_images_index from "./routes/api/images/index.ts";
import * as $api_index from "./routes/api/index.ts";
import * as $api_logs from "./routes/api/logs.ts";
import * as $api_movies_name_ from "./routes/api/movies/[name].ts";
import * as $api_movies_enhance_name_ from "./routes/api/movies/enhance/[name].ts";
import * as $api_movies_index from "./routes/api/movies/index.ts";
import * as $api_query_index from "./routes/api/query/index.ts";
import * as $api_recipes_name_ from "./routes/api/recipes/[name].ts";
import * as $api_recipes_create_index from "./routes/api/recipes/create/index.ts";
import * as $api_recipes_create_parseJsonLd from "./routes/api/recipes/create/parseJsonLd.ts";
import * as $api_recipes_index from "./routes/api/recipes/index.ts";
import * as $api_recommendation_all from "./routes/api/recommendation/all.ts";
import * as $api_recommendation_data from "./routes/api/recommendation/data.ts";
import * as $api_recommendation_index from "./routes/api/recommendation/index.ts";
import * as $api_recommendation_movie_id_ from "./routes/api/recommendation/movie/[id].ts";
import * as $api_series_name_ from "./routes/api/series/[name].ts";
import * as $api_series_enhance_name_ from "./routes/api/series/enhance/[name].ts";
import * as $api_series_index from "./routes/api/series/index.ts";
import * as $api_tmdb_id_ from "./routes/api/tmdb/[id].ts";
import * as $api_tmdb_credits_id_ from "./routes/api/tmdb/credits/[id].ts";
import * as $api_tmdb_query from "./routes/api/tmdb/query.ts";
import * as $articles_name_ from "./routes/articles/[name].tsx";
import * as $articles_index from "./routes/articles/index.tsx";
import * as $index from "./routes/index.tsx";
import * as $movies_name_ from "./routes/movies/[name].tsx";
import * as $movies_index from "./routes/movies/index.tsx";
import * as $recipes_name_ from "./routes/recipes/[name].tsx";
import * as $recipes_index from "./routes/recipes/index.tsx";
import * as $series_name_ from "./routes/series/[name].tsx";
import * as $series_index from "./routes/series/index.tsx";
import * as $Counter from "./islands/Counter.tsx";
import * as $IngredientsList from "./islands/IngredientsList.tsx";
import * as $KMenu from "./islands/KMenu.tsx";
import * as $KMenu_commands from "./islands/KMenu/commands.ts";
import * as $KMenu_commands_add_movie_infos from "./islands/KMenu/commands/add_movie_infos.ts";
import * as $KMenu_commands_add_series_infos from "./islands/KMenu/commands/add_series_infos.ts";
import * as $KMenu_commands_create_article from "./islands/KMenu/commands/create_article.ts";
import * as $KMenu_commands_create_movie from "./islands/KMenu/commands/create_movie.ts";
import * as $KMenu_commands_create_recipe from "./islands/KMenu/commands/create_recipe.ts";
import * as $KMenu_commands_create_recommendations from "./islands/KMenu/commands/create_recommendations.ts";
import * as $KMenu_commands_create_series from "./islands/KMenu/commands/create_series.ts";
import * as $KMenu_commands_enhance_article_infos from "./islands/KMenu/commands/enhance_article_infos.ts";
import * as $KMenu_types from "./islands/KMenu/types.ts";
import * as $KMenuButton from "./islands/KMenuButton.tsx";
import * as $Link from "./islands/Link.tsx";
import * as $Recommendations from "./islands/Recommendations.tsx";
import * as $Search from "./islands/Search.tsx";
import type { Manifest } from "$fresh/server.ts";
const manifest = {
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/_layout.tsx": $_layout,
"./routes/_middleware.ts": $_middleware,
"./routes/admin/cache/index.tsx": $admin_cache_index,
"./routes/admin/log/index.tsx": $admin_log_index,
"./routes/admin/performance/index.tsx": $admin_performance_index,
"./routes/api/articles/[name].ts": $api_articles_name_,
"./routes/api/articles/create/index.ts": $api_articles_create_index,
"./routes/api/articles/enhance/[name].ts": $api_articles_enhance_name_,
"./routes/api/articles/index.ts": $api_articles_index,
"./routes/api/auth/callback.ts": $api_auth_callback,
"./routes/api/auth/login.ts": $api_auth_login,
"./routes/api/auth/logout.ts": $api_auth_logout,
"./routes/api/cache.ts": $api_cache,
"./routes/api/images/index.ts": $api_images_index,
"./routes/api/index.ts": $api_index,
"./routes/api/logs.ts": $api_logs,
"./routes/api/movies/[name].ts": $api_movies_name_,
"./routes/api/movies/enhance/[name].ts": $api_movies_enhance_name_,
"./routes/api/movies/index.ts": $api_movies_index,
"./routes/api/query/index.ts": $api_query_index,
"./routes/api/recipes/[name].ts": $api_recipes_name_,
"./routes/api/recipes/create/index.ts": $api_recipes_create_index,
"./routes/api/recipes/create/parseJsonLd.ts":
$api_recipes_create_parseJsonLd,
"./routes/api/recipes/index.ts": $api_recipes_index,
"./routes/api/recommendation/all.ts": $api_recommendation_all,
"./routes/api/recommendation/data.ts": $api_recommendation_data,
"./routes/api/recommendation/index.ts": $api_recommendation_index,
"./routes/api/recommendation/movie/[id].ts": $api_recommendation_movie_id_,
"./routes/api/series/[name].ts": $api_series_name_,
"./routes/api/series/enhance/[name].ts": $api_series_enhance_name_,
"./routes/api/series/index.ts": $api_series_index,
"./routes/api/tmdb/[id].ts": $api_tmdb_id_,
"./routes/api/tmdb/credits/[id].ts": $api_tmdb_credits_id_,
"./routes/api/tmdb/query.ts": $api_tmdb_query,
"./routes/articles/[name].tsx": $articles_name_,
"./routes/articles/index.tsx": $articles_index,
"./routes/index.tsx": $index,
"./routes/movies/[name].tsx": $movies_name_,
"./routes/movies/index.tsx": $movies_index,
"./routes/recipes/[name].tsx": $recipes_name_,
"./routes/recipes/index.tsx": $recipes_index,
"./routes/series/[name].tsx": $series_name_,
"./routes/series/index.tsx": $series_index,
},
islands: {
"./islands/Counter.tsx": $Counter,
"./islands/IngredientsList.tsx": $IngredientsList,
"./islands/KMenu.tsx": $KMenu,
"./islands/KMenu/commands.ts": $KMenu_commands,
"./islands/KMenu/commands/add_movie_infos.ts":
$KMenu_commands_add_movie_infos,
"./islands/KMenu/commands/add_series_infos.ts":
$KMenu_commands_add_series_infos,
"./islands/KMenu/commands/create_article.ts":
$KMenu_commands_create_article,
"./islands/KMenu/commands/create_movie.ts": $KMenu_commands_create_movie,
"./islands/KMenu/commands/create_recipe.ts": $KMenu_commands_create_recipe,
"./islands/KMenu/commands/create_recommendations.ts":
$KMenu_commands_create_recommendations,
"./islands/KMenu/commands/create_series.ts": $KMenu_commands_create_series,
"./islands/KMenu/commands/enhance_article_infos.ts":
$KMenu_commands_enhance_article_infos,
"./islands/KMenu/types.ts": $KMenu_types,
"./islands/KMenuButton.tsx": $KMenuButton,
"./islands/Link.tsx": $Link,
"./islands/Recommendations.tsx": $Recommendations,
"./islands/Search.tsx": $Search,
},
baseUrl: import.meta.url,
} satisfies Manifest;
export default manifest;

View File

@@ -1,6 +1,6 @@
import type { Signal } from "@preact/signals";
import { Button } from "@components/Button.tsx";
import { IconCircleMinus, IconCirclePlus } from "@components/icons.tsx";
import { TbCircleMinus, TbCirclePlus } from "@preact-icons/tb";
interface CounterProps {
count: Signal<number>;
@@ -10,21 +10,21 @@ export default function Counter(props: CounterProps) {
props.count.value = Math.max(1, props.count.value);
return (
<div class="flex items-center px-1 py-2 rounded-xl">
<Button
class=""
onClick={() => props.count.value -= 1}
>
<IconCircleMinus />
<Button onClick={() => props.count.value -= 1}>
<TbCircleMinus class="h-6 w-6" />
</Button>
<input
class="text-3xl bg-transparent inline text-center -mx-4"
type="number"
size={props.count.toString().length}
value={props.count}
onInput={(ev) => props.count.value = ev.target?.value}
onInput={(ev) => {
const target = ev.target as HTMLInputElement;
props.count.value = Math.max(1, Number(target.value));
}}
/>
<Button onClick={() => props.count.value += 1}>
<IconCirclePlus />
<TbCirclePlus class="h-6 w-6" />
</Button>
</div>
);

View File

@@ -1,6 +1,5 @@
import { Signal } from "@preact/signals";
import type { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
import { FunctionalComponent } from "preact";
import { unitsOfMeasure } from "@lib/parseIngredient.ts";
function formatAmount(num: number) {
@@ -50,14 +49,12 @@ const Ingredient = (
);
};
export const IngredientsList: FunctionalComponent<
{
export const IngredientsList = (
{ ingredients, amount, portion }: {
ingredients: (Ingredient | IngredientGroup)[];
amount: Signal<number>;
portion?: number;
}
> = (
{ ingredients, amount, portion },
},
) => {
return (
<table class="w-full border-collapse table-auto">

View File

@@ -4,7 +4,6 @@ import { useEventListener } from "@lib/hooks/useEventListener.ts";
import { menus } from "@islands/KMenu/commands.ts";
import { MenuEntry } from "@islands/KMenu/types.ts";
import * as icons from "@components/icons.tsx";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { isKMenuOpen } from "@lib/kmenu.ts";
const KMenuEntry = (
{ entry, activeIndex, index }: {
@@ -155,17 +154,17 @@ export const KMenu = (
} else {
input.current?.blur();
}
}, IS_BROWSER ? document?.body : undefined);
}, typeof document !== "undefined" ? document?.body : undefined);
return (
<div
class={`${visible.value ? "opacity-100" : "opacity-0"} pointer-events-${
visible.value ? "auto" : "none"
class={`${visible.value ? "opacity-100" : "opacity-0"} ${
visible.value ? "pointer-events-auto" : "pointer-events-none"
} transition grid place-items-center w-full h-full fixed top-0 left-0 z-50`}
style={{ background: "#141217ee" }}
>
<div
class={`relative w-1/2 max-h-64 max-w-[400px] rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10 bg-gray-700`}
class={`relative w-1/2 max-h-64 max-w-100 rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10 bg-gray-700`}
style={{ background: "#2B2930", color: "#818181" }}
>
<div

View File

@@ -5,7 +5,6 @@ import { createNewArticle } from "@islands/KMenu/commands/create_article.ts";
import { getCookie } from "@lib/string.ts";
import { addSeriesInfo } from "@islands/KMenu/commands/add_series_infos.ts";
import { createNewSeries } from "@islands/KMenu/commands/create_series.ts";
import { updateAllRecommendations } from "@islands/KMenu/commands/create_recommendations.ts";
import { createNewRecipe } from "@islands/KMenu/commands/create_recipe.ts";
import { enhanceArticleInfo } from "@islands/KMenu/commands/enhance_article_infos.ts";

View File

@@ -42,8 +42,12 @@ export const addMovieInfos: MenuEntry = {
globalThis.location.reload();
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
})),
};
@@ -53,8 +57,12 @@ export const addMovieInfos: MenuEntry = {
state.activeState.value = "normal";
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
visible: () => {
const loc = globalThis["location"];

View File

@@ -42,8 +42,12 @@ export const addSeriesInfo: MenuEntry = {
//window.location.reload();
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
})),
};
@@ -53,8 +57,12 @@ export const addSeriesInfo: MenuEntry = {
state.activeState.value = "normal";
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
visible: () => {
const loc = globalThis["location"];

View File

@@ -66,8 +66,12 @@ export const createNewMovie: MenuEntry = {
globalThis.location.href = "/movies/" + movie.name;
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
};
}),
@@ -75,8 +79,12 @@ export const createNewMovie: MenuEntry = {
state.activeMenu.value = "input_link";
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
}, 500);
const unsub = state.commandInput.subscribe((value) => {

View File

@@ -68,8 +68,12 @@ export const createNewSeries: MenuEntry = {
globalThis.location.href = "/series/" + series.name;
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
};
}),
@@ -78,8 +82,12 @@ export const createNewSeries: MenuEntry = {
state.activeMenu.value = "input_link";
} catch (e) {
state.activeState.value = "error";
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
}, 500);
const unsub = state.commandInput.subscribe((value) => {

View File

@@ -8,7 +8,7 @@ export function Link(
props: {
href?: string;
class?: string;
style?: preact.JSX.CSSProperties;
style?: preact.CSSProperties;
children: preact.ComponentChildren;
"data-thumb"?: string;
},

View File

@@ -4,7 +4,7 @@ import { IconLoader2, IconSearch } from "@components/icons.tsx";
import { useEventListener } from "@lib/hooks/useEventListener.ts";
import { resources } from "@lib/resources.ts";
import { getCookie } from "@lib/string.ts";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { IS_BROWSER } from "fresh/runtime";
import Checkbox from "@components/Checkbox.tsx";
import { Rating } from "@components/Rating.tsx";
import { useSignal } from "@preact/signals";

View File

@@ -1,4 +1,4 @@
import { OAuth2Client } from "https://deno.land/x/oauth2_client@v1.0.2/mod.ts";
import { OAuth2Client } from "@cmd-johnson/oauth2-client";
import {
GITEA_CLIENT_ID,
GITEA_CLIENT_SECRET,

View File

@@ -43,7 +43,7 @@ export const performanceTable = sqliteTable("performance", {
export const imageTable = sqliteTable("image", {
createdAt: integer("created_at", { mode: "timestamp" }).default(
sql`(current_timestamp)`,
sql`(unixepoch())`,
),
url: text().notNull(),
average: text().notNull(),

View File

@@ -4,7 +4,6 @@ import path from "node:path";
const DB_FILE = "file:" + path.resolve(DATA_DIR, "db.sqlite");
// You can specify any property from the libsql connection options
export const db = drizzle({
connection: {
url: DB_FILE,

View File

@@ -1,8 +1,9 @@
import { FreshContext } from "$fresh/server.ts";
import { Context } from "fresh";
import { State } from "../utils.ts";
class DomainError extends Error {
status = 500;
render?: (ctx: FreshContext) => void;
render?: (ctx: Context<State>) => void;
constructor(public statusText = "Internal Server Error") {
super();
}

View File

@@ -1,14 +1,14 @@
import { rgbToHex } from "@lib/string.ts";
import { createLogger } from "@lib/log/index.ts";
import { generateThumbhash } from "@lib/thumbhash.ts";
import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts";
import { parseMediaType } from "@std/media-types";
import path from "node:path";
import { mkdir } from "node:fs/promises";
import { DATA_DIR } from "@lib/env.ts";
import { db } from "@lib/db/sqlite.ts";
import { imageTable } from "@lib/db/schema.ts";
import { eq } from "drizzle-orm";
import sharp from "npm:sharp@next";
import sharp from "sharp";
const log = createLogger("cache/image");
@@ -219,7 +219,11 @@ async function createThumbhash(
.raw()
.toBuffer();
const [hash, average] = generateThumbhash(resizedImage, 100, 100);
const [hash, average] = generateThumbhash(
new Uint8Array(resizedImage),
100,
100,
);
return {
hash: btoa(String.fromCharCode(...hash)),

View File

@@ -36,9 +36,10 @@ export async function getLogs() {
.map((line) => {
const [date, ...rest] = line.split(" | ");
const parsed = JSON.parse(rest.join(" | ")) as Log;
const dateObj = new Date(date);
return {
...parsed,
date: new Date(date),
date: isNaN(dateObj.getTime()) ? new Date() : dateObj,
} as Log;
});

View File

@@ -1,7 +1,7 @@
import { render } from "gfm";
import "https://esm.sh/prismjs@1.29.0/components/prism-typescript?no-check";
import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check";
import "https://esm.sh/prismjs@1.29.0/components/prism-rust?no-check";
import "prismjs/components/prism-typescript.js";
import "prismjs/components/prism-bash.js";
import "prismjs/components/prism-rust.js";
export type Document = {
name: string;

View File

@@ -8,7 +8,7 @@ import { articleMetadataSchema } from "./marka/schema.ts";
const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY });
interface MovieRecommendation {
export interface MovieRecommendation {
year: number;
title: string;
}
@@ -181,14 +181,14 @@ respond with a plain unordered list each item starting with the year the movie w
if (!res) return;
const recommendations = res.split("\n").map((entry) => {
const recommendations = res.split("\n").map((entry: string) => {
const [year, ...title] = entry.split("-");
return {
year: parseInt(year.trim()),
title: title.join(" ").replaceAll('"', "").trim(),
};
}).filter((y) => !Number.isNaN(y.year));
}).filter((y: { year: number }) => !Number.isNaN(y.year));
cache.set(cacheId, recommendations);

View File

@@ -1,6 +1,6 @@
import { firefox } from "npm:playwright-extra";
import { firefox } from "playwright-extra";
import { createStreamResponse } from "@lib/helpers.ts";
import StealthPlugin from "npm:puppeteer-extra-plugin-stealth";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import * as env from "@lib/env.ts";
firefox.use(StealthPlugin());

View File

@@ -1,105 +0,0 @@
/**
* Interface zur Beschreibung eines eingereihten Promises in der `PromiseQueue`.
*/
interface QueuedPromise<T = any> {
promise: () => Promise<T>;
resolve: (value: T) => void;
reject: (reason?: any) => void;
}
/**
* Eine einfache Promise Queue, die es ermöglicht mehrere Aufgaben in kontrollierter
* Reihenfolge abzuarbeiten.
*
* Lizenz: CC BY-NC-SA 4.0
* (c) Peter Müller <peter@crycode.de> (https://crycode.de/promise-queue-in-typescript)
*/
export class PromiseQueue {
/**
* Eingereihte Promises.
*/
private queue: QueuedPromise[] = [];
/**
* Indikator, dass aktuell ein Promise abgearbeitet wird.
*/
private working = false;
/**
* Ein Promise einreihen.
* Dies fügt das Promise der Warteschlange hinzu. Wenn die Warteschlange leer
* ist, dann wird das Promise sofort gestartet.
* @param promise Funktion, die das Promise zurückgibt.
* @returns Ein Promise, welches eingelöst (oder zurückgewiesen) wird sobald das eingereihte Promise abgearbeitet ist.
*/
public enqueue<T = void>(promise: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({
promise,
resolve,
reject,
});
this.dequeue();
});
}
/**
* Das erste Promise aus der Warteschlange holen und starten, sofern nicht
* bereits ein Promise aktiv ist.
* @returns `true` wenn ein Promise aus der Warteschlange gestartet wurde oder `false` wenn bereits ein Promise aktiv oder die Warteschlange leer ist.
*/
private dequeue(): boolean {
if (this.working) {
return false;
}
const item = this.queue.shift();
if (!item) {
return false;
}
try {
this.working = true;
item.promise()
.then((value) => {
item.resolve(value);
})
.catch((err) => {
item.reject(err);
})
.finally(() => {
this.working = false;
this.dequeue();
});
} catch (err) {
item.reject(err);
this.working = false;
this.dequeue();
}
return true;
}
}
export class ConcurrentPromiseQueue {
/**
* Eingereihte Promises.
*/
private queues: PromiseQueue[] = [];
constructor(concurrency: number = 1) {
this.queues = Array.from({ length: concurrency }).map(() => {
return new PromiseQueue();
});
}
private queueIndex = 0;
private getQueue() {
this.queueIndex = (this.queueIndex + 1) % this.queues.length;
return this.queues[this.queueIndex];
}
public enqueue<T = void>(promise: () => Promise<T>): Promise<T> {
return this.getQueue().enqueue(promise);
}
}

View File

@@ -1,4 +1,4 @@
import { z } from "npm:zod";
import { z } from "zod";
import { RecipeResource } from "./marka/schema.ts";
export const IngredientSchema = z.object({

View File

@@ -1,4 +1,5 @@
import * as openai from "@lib/openai.ts";
import { type MovieRecommendation } from "@lib/openai.ts";
import * as tmdb from "@lib/tmdb.ts";
import { parseRating } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts";
@@ -60,8 +61,10 @@ export async function createRecommendationResource(
const d = typeof datePublished === "string"
? new Date(datePublished)
: datePublished;
if (!isNaN(d.getTime())) {
resource.year = d.getFullYear();
}
}
cache.set(cacheId, JSON.stringify(resource));
}
@@ -83,10 +86,12 @@ export async function getSimilarMovies(id: string) {
);
if (!recommendations) return;
const movies = await Promise.all(recommendations.map(async (rec) => {
const movies = await Promise.all(
recommendations.map(async (rec: MovieRecommendation) => {
const m = await tmdb.searchMovie(rec.title, rec.year);
return m?.results?.[0];
}));
}),
);
return movies.filter(Boolean);
}
@@ -96,5 +101,7 @@ export async function getAllRecommendations(): Promise<
> {
const keys = cache.keys();
const res = await Promise.all(keys.map((k) => cache.get(k)));
return res.filter((s) => !!s).map((r) => JSON.parse(r));
return res.filter((s) => !!s).map((r) =>
typeof r === "string" ? JSON.parse(r) : r
);
}

View File

@@ -186,3 +186,7 @@ export function removeMarkdownFormatting(text: string): string {
return text;
}
export function fileExtension(fname: string) {
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
}

View File

@@ -1,110 +0,0 @@
import { transcribe } from "@lib/openai.ts";
import { createResource } from "@lib/marka/index.ts";
import { createLogger } from "./log/index.ts";
import { convertOggToMp3 } from "./helpers.ts";
const log = createLogger("taskManager");
// In-memory task state
const activeTasks: Record<
string,
{
noteName: string;
entries: Array<
{ type: string; content: string | ArrayBufferLike; fileName?: string }
>;
}
> = {};
export function startTask(chatId: string, noteName: string) {
activeTasks[chatId] = { noteName, entries: [] };
log.info(`Started note: ${noteName}`);
}
export async function endTask(chatId: string): Promise<string | null> {
const task = activeTasks[chatId];
if (!task) return null;
log.info("Ending note", task.noteName);
let finalNote = `# ${task.noteName}\n\n`;
const photoTasks: { content: ArrayBuffer; path: string }[] = [];
let photoIndex = 0;
for (const entry of task.entries) {
if (entry.type === "text") {
finalNote += entry.content + "\n\n";
} else if (entry.type === "voice") {
try {
log.info("Converting OGG to MP3");
const mp3Data = await convertOggToMp3(entry.content as ArrayBuffer);
log.info("Finished converting OGG to MP3, transcribing...");
const transcript = await transcribe(mp3Data);
finalNote += `**Voice Transcript:**\n${transcript}\n\n`;
log.info("Finished transcribing");
} catch (error) {
log.error(error);
finalNote += "**[Voice message could not be transcribed]**\n\n";
}
} else if (entry.type === "photo") {
const photoUrl = `${
task.noteName.replace(/\.md$/, "")
}/photo-${photoIndex++}.jpg`;
finalNote += `**Photo**:\n ${photoUrl}\n\n`;
photoTasks.push({
content: entry.content as ArrayBuffer,
path: photoUrl,
});
}
}
try {
for (const entry of photoTasks) {
await createResource(entry.path, entry.content);
}
} catch (err) {
log.error("Error creating photo document:", err);
}
try {
await createResource(task.noteName, finalNote);
} catch (error) {
log.error("Error creating document:", error);
return error instanceof Error
? error.toString()
: "Error creating document";
}
delete activeTasks[chatId];
log.debug({ finalNote });
return finalNote;
}
export function addTextEntry(chatId: string, text: string) {
const task = activeTasks[chatId];
if (!task) return;
const entry = { type: "text", content: text };
log.debug("New Entry", entry);
task.entries.push(entry);
}
export function addVoiceEntry(chatId: string, buffer: ArrayBufferLike) {
const task = activeTasks[chatId];
if (!task) return;
const entry = { type: "voice", content: buffer };
log.debug("New Entry", entry);
task.entries.push(entry);
}
export function addPhotoEntry(
chatId: string,
buffer: ArrayBufferLike,
fileName: string,
) {
const task = activeTasks[chatId];
if (!task) return;
const entry = { type: "photo", content: buffer, fileName };
log.debug("New Entry", entry);
task.entries.push(entry);
}

View File

@@ -1,65 +0,0 @@
import { Bot } from "https://deno.land/x/grammy@v1.36.1/mod.ts";
import { TELEGRAM_API_KEY } from "@lib/env.ts";
import { createLogger } from "./log/index.ts";
const bot = new Bot(TELEGRAM_API_KEY);
const log = createLogger("telegram");
import * as manager from "./taskManager.ts";
async function downloadFile(filePath: string): Promise<Uint8Array> {
log.info(`Downloading file from path: ${filePath}`);
const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to download file: " + response.statusText);
}
log.info("File downloaded successfully");
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
}
bot.command("start", async (ctx) => {
log.info("Received /start command");
const [_, noteName] = ctx.message?.text?.split(" ") || [];
if (!noteName) {
return ctx.reply("Please provide a note name. Usage: /start NoteName");
}
manager.startTask(ctx.chat.id.toString(), noteName);
await ctx.reply(`Started note: ${noteName}`);
});
bot.command("end", async (ctx) => {
log.info("Received /end command");
const finalNote = await manager.endTask(ctx.chat.id.toString());
if (!finalNote) return ctx.reply("No active note found.");
try {
await ctx.reply("Note complete. Here is your markdown:");
await ctx.reply(finalNote, { parse_mode: "MarkdownV2" });
} catch (err) {
console.error("Error sending final note:", err);
}
});
bot.on("message:text", (ctx) => {
log.info("Received text message");
manager.addTextEntry(ctx.chat.id.toString(), ctx.message.text);
});
bot.on("message:voice", async (ctx) => {
log.info("Received photo message");
log.info("Received voice message");
const file = await ctx.getFile();
const buffer = await downloadFile(file.file_path!);
manager.addVoiceEntry(ctx.chat.id.toString(), buffer.buffer);
});
bot.on("message:photo", async (ctx) => {
const file = await ctx.getFile();
const buffer = await downloadFile(file.file_path!);
const fileName = file.file_path!.split("/").pop()!;
manager.addPhotoEntry(ctx.chat.id.toString(), buffer.buffer, fileName);
});
bot.start();

View File

@@ -1,6 +1,10 @@
import * as thumbhash from "thumbhash";
export function generateThumbhash(buffer: Uint8Array, w: number, h: number) {
export function generateThumbhash(
buffer: ArrayLike<number>,
w: number,
h: number,
) {
const hash = thumbhash.rgbaToThumbHash(w, h, buffer);
return [hash, thumbhash.thumbHashToAverageRGBA(hash)] as const;
}

View File

@@ -47,7 +47,7 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void {
document
.querySelectorAll("img[srcset], source[srcset]")
.forEach((el: HTMLImageElement) => {
.forEach((el) => {
const v = el.getAttribute("srcset");
if (!v) return;
const abs = absolutizeSrcset(v, base);
@@ -55,7 +55,7 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void {
});
document.querySelectorAll("[style]").forEach(
(el: HTMLElement) => {
(el) => {
const v = el.getAttribute("style");
if (!v) return;
const abs = absolutizeCssUrls(v, base);
@@ -73,7 +73,7 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void {
document
.querySelectorAll('meta[http-equiv="refresh" i][content]')
.forEach((meta: HTMLMetaElement) => {
.forEach((meta) => {
const content = meta.getAttribute("content") || "";
const abs = absolutizeMetaRefresh(content, base);
if (abs !== content) meta.setAttribute("content", abs);
@@ -168,10 +168,20 @@ function absolutizeMetaRefresh(content: string, base: string): string {
const turndownService = new TurndownService();
export interface WebScrapeResult {
title?: string;
image?: string;
published?: string;
content: string;
schemaOrgData?: { author?: { name?: string } };
markdown: string;
dom: JSDOM["window"]["document"];
}
export async function webScrape(
url: string,
streamResponse: ReturnType<typeof createStreamResponse>,
): JSDOM {
) {
const u = new URL(url);
const html = await fetchHtmlWithPlaywright(url, streamResponse);
const dom = new JSDOM(html);

View File

@@ -56,12 +56,10 @@ export interface ContentDetails {
definition: string;
caption: string;
licensedContent: boolean;
contentRating: ContentRating;
contentRating: Record<string, unknown>;
projection: string;
}
export interface ContentRating {}
export interface Statistics {
viewCount: string;
likeCount: string;

16
main.ts
View File

@@ -1,12 +1,8 @@
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import { App, staticFiles } from "fresh";
import { type State } from "./utils.ts";
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import config from "./fresh.config.ts";
// import "@lib/telegram.ts";
export const app = new App<State>();
await start(manifest, config);
app.use(staticFiles());
app.fsRoutes();

View File

@@ -1,24 +1,16 @@
// deno-lint-ignore-file react-no-danger
import { PageProps } from "$fresh/server.ts";
import { Partial } from "$fresh/runtime.ts";
export default function App({ Component }: PageProps) {
const globalCss = Deno.readTextFileSync("./static/global.css");
import { define } from "../utils.ts";
export default define.page(function ({ Component }) {
return (
<html>
<head>
<link rel="stylesheet" href="/prism-material-dark.css" />
<link rel="stylesheet" href="/styles.css" />
<link
rel="icon"
type="image/png"
href="/favicon.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#141218" />
<link
rel="preload"
href="/fonts/work-sans-v18-latin-regular.woff2"
@@ -31,15 +23,25 @@ export default function App({ Component }: PageProps) {
as="font"
type="font/woff2"
/>
<style dangerouslySetInnerHTML={{ __html: globalCss }} />
<link rel="stylesheet" href="/global.css" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#141218" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style
dangerouslySetInnerHTML={{
__html: Deno.readTextFileSync("./static/global.css"),
}}
>
</style>
<title>Memorium</title>
</head>
<body f-client-nav>
<Partial name="body">
<body>
<Component />
</Partial>
</body>
<script src="/thumbhash.js" type="module" async defer />
</html>
);
}
});

View File

@@ -1,7 +1,18 @@
import { Head } from "$fresh/runtime.ts";
import { Head } from "fresh/runtime";
import { MainLayout } from "@components/layouts/main.tsx";
import { HttpError, PageProps } from "fresh";
export default function ErrorPage(props: PageProps) {
const error = props.error; // Contains the thrown Error or HTTPError
if (error instanceof HttpError) {
const status = error.status; // HTTP status code
// Render a 404 not found page
if (status === 404) {
return <h1>404 - Page not found</h1>;
}
}
export default function Error404() {
return (
<>
<Head>
@@ -9,7 +20,7 @@ export default function Error404() {
</Head>
<MainLayout url="">
<div class="px-8 text-white mt-10">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<div class="max-w-3xl mx-auto flex flex-col items-center justify-center">
<h1 class="text-4xl font-bold">404 - Page not found</h1>
<p class="my-4">
The page you were looking for doesn't exist.

View File

@@ -1,10 +1,10 @@
import { PageProps } from "$fresh/server.ts";
import { resources } from "@lib/resources.ts";
import { Link } from "@islands/Link.tsx";
import { Emoji } from "@components/Emoji.tsx";
import KMenuButton from "@islands/KMenuButton.tsx";
import { PageProps } from "fresh";
import { define } from "../utils.ts";
export default function MyLayout({ Component }: PageProps) {
export default define.layout(function ({ Component }: PageProps) {
return (
<div
class="md:grid mx-auto"
@@ -14,12 +14,12 @@ export default function MyLayout({ Component }: PageProps) {
<nav class="min-h-fit rounded-3xl p-3 grid gap-3 fixed t-0">
{Object.values(resources).map((m) => {
return (
<Link
<a
href={m.link}
class="flex items-center gap-2 text-white data-[current]:bg-white data-[current]:text-black p-3 text-xl w-full rounded-2xl"
class="flex items-center gap-2 text-white data-current:bg-white data-current:text-black p-3 text-xl w-full rounded-2xl"
>
<Emoji class="w-6 h-6" name={m.emoji} /> {m.name}
</Link>
</a>
);
})}
</nav>
@@ -30,4 +30,4 @@ export default function MyLayout({ Component }: PageProps) {
<KMenuButton />
</div>
);
}
});

View File

@@ -1,27 +1,39 @@
//routes/middleware-error-handler/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
import { DomainError } from "@lib/errors.ts";
import { getCookies } from "@std/http/cookie";
import { verify } from "https://deno.land/x/djwt@v2.2/mod.ts";
import { verify } from "@zaubrik/djwt";
import * as perf from "@lib/performance.ts";
import { JWT_SECRET } from "@lib/env.ts";
import { define } from "../utils.ts";
export async function handler(
req: Request,
ctx: FreshContext,
function importKey(secret: string) {
return crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-512" },
false,
["sign", "verify"],
);
}
const authMiddleware = define.middleware(async function (
ctx,
) {
const req = ctx.req;
try {
performance.mark("a");
const allCookies = getCookies(req.headers);
const sessionCookie = allCookies["session_cookie"];
if (!ctx.state.session && sessionCookie && JWT_SECRET) {
try {
const payload = await verify(sessionCookie, JWT_SECRET, "HS512");
const payload = await verify<typeof ctx.state.session>(
sessionCookie,
await importKey(JWT_SECRET),
);
if (payload) {
ctx.state.session = payload;
}
} catch (_err) {
//
console.log({ _err });
}
}
@@ -44,4 +56,6 @@ export async function handler(
status: 500,
});
}
}
});
export default [authMiddleware];

View File

@@ -1,32 +0,0 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getCacheInfo } from "@lib/cache.ts";
export const handler: Handlers<
{ cacheInfo: ReturnType<typeof getCacheInfo> }
> = {
GET(_, ctx) {
return ctx.render({ cacheInfo: getCacheInfo() });
},
};
export default function Greet(
props: PageProps<
{ cacheInfo: ReturnType<typeof getCacheInfo> }
>,
) {
const { cacheInfo } = props.data;
return (
<MainLayout
url={props.url}
title="Recipes"
context={{ type: "recipes" }}
>
<code>
<pre class="text-white">
{JSON.stringify(cacheInfo, null, 2)}
</pre>
</code>
</MainLayout>
);
}

View File

@@ -1,78 +0,0 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { getLogs, Log } from "@lib/log/index.ts";
import { formatDate } from "@lib/string.ts";
import { renderMarkdown } from "@lib/markdown.ts";
const renderLog = (t: unknown) =>
renderMarkdown(`\`\`\`js
${typeof t === "string" ? t : JSON.stringify(t, null, 2).trim()}
\`\`\``);
export const handler: Handlers = {
async GET(_, ctx) {
const logs = await getLogs();
if (!("session" in ctx.state)) {
throw new AccessDeniedError();
}
return ctx.render({
logs: logs.map((l) => {
return {
...l,
html: l.args.map(renderLog).join("<br/>"),
};
}),
});
},
};
function LogLine(
{ log }: {
log: Log & { html?: string };
},
) {
return (
<div
class="mt-4 flex flex-col gap-2 bg-gray-900 px-4 py-2 rounded-2xl max-w-3xl"
style={{ background: "var(--light)" }}
>
<div class="flex gap-2">
<span class="bg-gray-600 py-1 px-2 text-xs rounded-xl text-white">
{log.date.getHours().toString().padStart(2, "0")}:{log.date
.getMinutes().toString().padStart(2, "0")}:{log.date.getSeconds()
.toString().padStart(2, "0")} {formatDate(log.date)}
</span>
<span class="bg-gray-600 py-1 px-2 text-xs rounded-xl text-white">
{log.scope}
</span>
<span class="bg-gray-600 py-1 px-2 text-xs rounded-xl text-white">
{log.level}
</span>
</div>
<div
class="text-white"
// deno-lint-ignore react-no-danger
dangerouslySetInnerHTML={{ __html: log.html ?? "" }}
/>
</div>
);
}
export default function Greet(
{ data: { logs }, url }: PageProps<{ logs: Log[] }>,
) {
return (
<MainLayout url={url}>
<h1 class="text-white text-4xl">Logs</h1>
{logs.map((r) => {
return (
<LogLine
log={r}
/>
);
})}
</MainLayout>
);
}

View File

@@ -1,89 +0,0 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getPerformances, PerformanceRes } from "@lib/performance.ts";
import { AccessDeniedError } from "@lib/errors.ts";
export const handler: Handlers = {
async GET(_, ctx) {
const performances = await getPerformances();
if (!("session" in ctx.state)) {
throw new AccessDeniedError();
}
return ctx.render({ performances });
},
};
function PerformanceLine(
{ maximum, data: [amount, min, average, max], url }: {
maximum: number;
url: string;
data: readonly [number, number, number, number];
},
) {
return (
<div
class="mt-10 flex flex-col gap-2 bg-gray-900 px-4 py-2 rounded-2xl"
style={{ background: "var(--light)" }}
>
<div style={{ color: "var(--foreground)" }}>
{url}
</div>
<div class="flex gap-2">
<span class="bg-gray-600 p-1 text-xs rounded-xl text-white">
{Math.floor(average / 1000)}ms
</span>
<span class="bg-gray-600 p-1 text-xs rounded-xl text-white">
{amount}x
</span>
</div>
<div class="" style={{ maxWidth: "75vw" }}>
<div
class="h-2 bg-red-200 rounded"
style={{
width: `${Math.floor((max / maximum) * 100)}%`,
borderRadius: "1rem 1rem 1rem 0px",
minWidth: "5px",
}}
/>
<div
class="h-2 bg-green-200 rounded"
style={{
width: `${Math.floor((average / maximum) * 100)}%`,
borderStartEndRadius: "0px",
borderRadius: "0px 0px 1rem 0px",
minWidth: "5px",
}}
/>
<div
class="h-2 bg-yellow-200 rounded"
style={{
width: `${Math.floor((min / maximum) * 100)}%`,
borderRadius: "0px 0px 1rem 1rem",
minWidth: "5px",
}}
/>
</div>
</div>
);
}
export default function Greet(
{ data: { performances }, url }: PageProps<{ performances: PerformanceRes }>,
) {
return (
<MainLayout url={url}>
<h1 class="text-white text-4xl ">Performance</h1>
{performances.res.map((r) => {
return (
<PerformanceLine
maximum={performances.max}
url={r.url}
data={r.data}
/>
);
})}
</MainLayout>
);
}

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const article = await fetchResource(`articles/${ctx.params.name}`);
return json(article);
},
};
});

View File

@@ -1,5 +1,3 @@
import { Handlers } from "$fresh/server.ts";
import { Defuddle } from "defuddle/node";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts";
@@ -7,6 +5,7 @@ import * as unsplash from "@lib/unsplash.ts";
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
import {
extractYoutubeId,
fileExtension,
formatDate,
isYoutubeLink,
safeFileName,
@@ -16,7 +15,7 @@ import { createLogger } from "@lib/log/index.ts";
import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { define } from "../../../../utils.ts";
const log = createLogger("api/article");
@@ -85,29 +84,31 @@ async function processCreateArticle(
streamResponse.info("downloading article");
const result = await webScrape(fetchUrl, streamResponse);
const scrapeResult = await webScrape(fetchUrl, streamResponse);
log.debug("downloaded and parse parsed", result);
log.debug("downloaded and parse parsed", scrapeResult);
streamResponse.info("parsed article, creating tags with openai");
const aiMeta = await openai.extractArticleMetadata(result.markdown);
const aiMeta = await openai.extractArticleMetadata(scrapeResult.markdown);
streamResponse.info("postprocessing article");
const title = result?.title || aiMeta?.headline || "";
const title = scrapeResult?.title || aiMeta?.headline || "";
let coverImagePath: string | undefined = undefined;
if (result?.image?.length) {
log.debug("using local image for cover image", { image: result.image });
if (scrapeResult?.image?.length) {
log.debug("using local image for cover image", {
image: scrapeResult.image,
});
coverImagePath = await fetchAndStoreCover(
result.image,
scrapeResult.image,
title,
streamResponse,
);
} else {
const urlPath = await getUnsplashCoverImage(
result.markdown,
scrapeResult.markdown,
streamResponse,
);
coverImagePath = await fetchAndStoreCover(urlPath, title, streamResponse);
@@ -119,15 +120,15 @@ async function processCreateArticle(
const newArticle: ArticleResource["content"] = {
_type: "Article",
headline: title,
articleBody: result.markdown,
articleBody: scrapeResult.markdown,
url: fetchUrl,
datePublished: formatDate(
result?.published || aiMeta?.datePublished || undefined,
scrapeResult?.published || aiMeta?.datePublished || undefined,
),
image: coverImagePath,
author: {
_type: "Person",
name: (result.schemaOrgData?.author?.name || aiMeta?.author || "")
name: (scrapeResult.schemaOrgData?.author?.name || aiMeta?.author || "")
.replace(
"@",
"twitter:",
@@ -201,8 +202,9 @@ async function processCreateYoutubeVideo(
streamResponse.send({ type: "finished", url: filename });
}
export const handler: Handlers = {
GET(req, ctx) {
export const handler = define.handlers({
GET(ctx) {
const req = ctx.req;
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -239,4 +241,4 @@ export const handler: Handlers = {
return streamResponse.response;
},
};
});

View File

@@ -1,6 +1,4 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { formatDate, safeFileName } from "@lib/string.ts";
import { fileExtension, formatDate, safeFileName } from "@lib/string.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import {
AccessDeniedError,
@@ -13,6 +11,7 @@ import { webScrape } from "@lib/webScraper.ts";
import * as openai from "@lib/openai.ts";
import * as unsplash from "@lib/unsplash.ts";
import { createLogger } from "@lib/log/index.ts";
import { define } from "../../../../utils.ts";
function ext(str: string) {
try {
@@ -163,10 +162,8 @@ async function processEnhanceArticle(
streamResponse.send({ type: "finished", url: name.replace(/$\.md/, "") });
}
const POST = (
_req: Request,
ctx: FreshContext,
): Response => {
export const handler = define.handlers({
POST: (ctx) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -184,8 +181,5 @@ const POST = (
});
return streamResponse.response;
};
export const handler: Handlers = {
POST,
};
},
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const articles = await fetchResource("articles");
return json(articles?.content);
},
};
});

View File

@@ -1,5 +1,4 @@
import { Handlers } from "$fresh/server.ts";
import { create, getNumericDate } from "https://deno.land/x/djwt@v2.2/mod.ts";
import { create, getNumericDate } from "@zaubrik/djwt";
import { oauth2Client } from "@lib/auth.ts";
import { getCookies, setCookie } from "@std/http/cookie";
import { codeChallengeMap } from "./login.ts";
@@ -9,15 +8,16 @@ import { BadRequestError } from "@lib/errors.ts";
import { db } from "@lib/db/sqlite.ts";
import { userTable } from "@lib/db/schema.ts";
import { eq } from "drizzle-orm";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(request) {
export const handler = define.handlers({
async GET(ctx) {
if (!JWT_SECRET) {
throw new BadRequestError();
}
// Exchange the authorization code for an access token
const cookies = getCookies(request.headers);
const cookies = getCookies(ctx.req.headers);
const stored = codeChallengeMap.get(cookies["code_challenge"]);
if (!stored) {
@@ -26,7 +26,7 @@ export const handler: Handlers = {
const { codeVerifier, redirect } = stored;
const tokens = await oauth2Client.code.getToken(request.url, {
const tokens = await oauth2Client.code.getToken(ctx.req.url, {
codeVerifier,
});
@@ -53,11 +53,23 @@ export const handler: Handlers = {
user = res[0];
}
if (!JWT_SECRET) {
throw new BadRequestError();
}
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(JWT_SECRET),
{ name: "HMAC", hash: "SHA-512" },
false,
["sign", "verify"],
);
const jwt = await create({ alg: "HS512", type: "JWT" }, {
id: user.id,
name: user.name,
exp: getNumericDate(SESSION_DURATION),
}, JWT_SECRET);
}, key);
const headers = new Headers({
location: redirect || "/",
@@ -78,4 +90,4 @@ export const handler: Handlers = {
status: 302,
});
},
};
});

View File

@@ -1,14 +1,15 @@
import { Handlers } from "$fresh/server.ts";
import { oauth2Client } from "@lib/auth.ts";
import { setCookie } from "@std/http/cookie";
import { define } from "../../../utils.ts";
export const codeChallengeMap = new Map<
string,
{ codeVerifier: string; redirect?: string }
>();
export const handler: Handlers = {
async GET(req) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const url = new URL(req.url);
const { codeVerifier, uri } = await oauth2Client.code.getAuthorizationUri();
@@ -33,4 +34,4 @@ export const handler: Handlers = {
status: 302,
});
},
};
});

View File

@@ -1,8 +1,9 @@
import { deleteCookie } from "@std/http/cookie";
import { Handlers } from "$fresh/server.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
GET(req) {
export const handler = define.handlers({
GET(ctx) {
const req = ctx.req;
const url = new URL(req.url);
const redirect = decodeURIComponent(url.searchParams.get("redirect") || "");
@@ -19,4 +20,4 @@ export const handler: Handlers = {
status: 302,
});
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { documentTable } from "@lib/db/schema.ts";
import { db } from "@lib/db/sqlite.ts";
import { json } from "@lib/helpers.ts";
import { caches } from "@lib/cache.ts";
import { define } from "../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async DELETE() {
for (const cache of caches.values()) {
cache.clear();
@@ -12,4 +12,4 @@ export const handler: Handlers = {
await db.delete(documentTable).run();
return json({ status: "ok" });
},
};
});

View File

@@ -1,6 +1,6 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { getImageContent } from "@lib/image.ts";
import { createLogger } from "@lib/log/index.ts";
import { Context } from "fresh";
const log = createLogger("api/image");
@@ -69,9 +69,9 @@ async function generateETag(content: Uint8Array<ArrayBuffer>): Promise<string> {
}"`;
}
async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
async function GET(ctx: Context<unknown>): Promise<Response> {
try {
const url = new URL(req.url);
const url = new URL(ctx.req.url);
const params = parseParams(url);
if (typeof params === "string") {
@@ -106,6 +106,6 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
}
}
export const handler: Handlers = {
export const handler = {
GET,
};

View File

@@ -1,8 +1,8 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { define } from "../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
GET() {
return json([]);
},
};
});

View File

@@ -1,9 +1,9 @@
import { Handlers } from "$fresh/server.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import { define } from "../../utils.ts";
const activeResponses: ReturnType<typeof createStreamResponse>[] = [];
export const handler: Handlers = {
export const handler = define.handlers({
GET() {
const r = createStreamResponse();
@@ -11,4 +11,4 @@ export const handler: Handlers = {
return r.response;
},
};
});

View File

@@ -1,8 +1,7 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import * as tmdb from "@lib/tmdb.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import {
fileExtension,
formatDate,
isString,
safeFileName,
@@ -11,13 +10,14 @@ import {
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const movie = await fetchResource(`movies/${ctx.params.name}`);
return json(movie?.content);
},
async POST(_, ctx) {
async POST(ctx) {
const session = ctx.state.session;
if (!session) throw new AccessDeniedError();
@@ -70,4 +70,4 @@ export const handler: Handlers = {
return json({ name: fileName });
},
};
});

View File

@@ -1,7 +1,6 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import * as tmdb from "@lib/tmdb.ts";
import {
fileExtension,
formatDate,
isString,
safeFileName,
@@ -16,11 +15,10 @@ import {
import { createRecommendationResource } from "@lib/recommendation.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../../../utils.ts";
const POST = async (
req: Request,
ctx: FreshContext,
): Promise<Response> => {
export const handler = define.handlers({
POST: async function (ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -33,7 +31,7 @@ const POST = async (
throw new NotFoundError();
}
const body = await req.json();
const body = await ctx.req.json();
const name = ctx.params.name;
const { tmdbId } = body;
if (!name || !tmdbId) {
@@ -88,8 +86,5 @@ const POST = async (
createRecommendationResource(movie, movieDetails.overview);
return json(movie);
};
export const handler: Handlers = {
POST,
};
},
});

View File

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

View File

@@ -1,10 +1,11 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(req, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -15,9 +16,8 @@ export const handler: Handlers = {
throw new BadRequestError();
}
console.log(s);
const resources = await searchResource(s);
return json(resources);
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const recipe = await fetchResource(`recipes/${ctx.params.name}`);
return json(recipe);
},
};
});

View File

@@ -1,16 +1,15 @@
import { Handlers } from "$fresh/server.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts";
import { createLogger } from "@lib/log/index.ts";
import recipeSchema from "@lib/recipeSchema.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { safeFileName, toUrlSafeString } from "@lib/string.ts";
import { fileExtension, safeFileName, toUrlSafeString } from "@lib/string.ts";
import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts";
import z from "zod";
import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts";
import { RecipeResource } from "@lib/marka/schema.ts";
import { define } from "../../../../utils.ts";
const log = createLogger("api/article");
@@ -93,8 +92,9 @@ async function processCreateRecipeFromUrl(
streamResponse.send({ type: "finished", url: id });
}
export const handler: Handlers = {
GET(req, ctx) {
export const handler = define.handlers({
GET(ctx) {
const req = ctx.req;
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -120,4 +120,4 @@ export const handler: Handlers = {
return streamResponse.response;
},
};
});

View File

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

View File

@@ -1,4 +1,3 @@
import { Handlers } from "$fresh/server.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import * as tmdb from "@lib/tmdb.ts";
import {
@@ -8,6 +7,7 @@ import {
import { AccessDeniedError } from "@lib/errors.ts";
import { listResources } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../../utils.ts";
async function processUpdateRecommendations(
streamResponse: ReturnType<typeof createStreamResponse>,
@@ -53,8 +53,8 @@ async function processUpdateRecommendations(
streamResponse.info("100% Finished");
}
export const handler: Handlers = {
GET(_, ctx) {
export const handler = define.handlers({
GET(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -65,4 +65,4 @@ export const handler: Handlers = {
return streamResponse.response;
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { getAllRecommendations } from "@lib/recommendation.ts";
import { json } from "@lib/helpers.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -34,4 +34,4 @@ export const handler: Handlers = {
keywords,
});
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { getAllRecommendations } from "@lib/recommendation.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const recommendations = await getAllRecommendations();
return json(recommendations);
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { getSimilarMovies } from "@lib/recommendation.ts";
import { json } from "@lib/helpers.ts";
import { define } from "../../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -14,4 +14,4 @@ export const handler: Handlers = {
return json(recommendations);
},
};
});

View File

@@ -1,12 +1,16 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import * as tmdb from "@lib/tmdb.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { formatDate, isString, safeFileName } from "@lib/string.ts";
import {
fileExtension,
formatDate,
isString,
safeFileName,
} from "@lib/string.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { toUrlSafeString } from "@lib/string.ts";
import { define } from "../../../utils.ts";
function pickDirector(
credits: Awaited<ReturnType<typeof tmdb.getSeriesCredits>>,
@@ -16,12 +20,12 @@ function pickDirector(
return crewDirector?.name ?? createdBy?.[0]?.name;
}
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const series = await fetchResource(`series/${ctx.params.name}`);
return json(series);
},
async POST(_, ctx) {
async POST(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -78,4 +82,4 @@ export const handler: Handlers = {
return json({ name: fileName });
},
};
});

View File

@@ -1,7 +1,5 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import * as tmdb from "@lib/tmdb.ts";
import { safeFileName } from "@lib/string.ts";
import { fileExtension, safeFileName } from "@lib/string.ts";
import { json } from "@lib/helpers.ts";
import {
AccessDeniedError,
@@ -9,21 +7,20 @@ import {
NotFoundError,
} from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../../utils.ts";
const isString = (input: string | undefined): input is string => {
return typeof input === "string";
};
const POST = async (
req: Request,
ctx: FreshContext,
): Promise<Response> => {
export const handler = define.handlers({
POST: async (ctx) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
const body = await req.json();
const body = await ctx.req.json();
const name = ctx.params.name;
const { tmdbId } = body;
if (!name || !tmdbId) {
@@ -41,11 +38,16 @@ const POST = async (
const releaseDate = seriesDetails.first_air_date;
if (releaseDate && series.content?.datePublished) {
series.content.datePublished = new Date(releaseDate).toISOString();
const d = new Date(releaseDate);
if (!isNaN(d.getTime())) {
series.content.datePublished = d.toISOString();
}
}
const posterPath = seriesDetails.poster_path;
const director = seriesCredits &&
seriesCredits.crew?.filter?.((person) => person.job === "Director")[0] ||
seriesCredits.crew?.filter?.((person) =>
person.job === "Director"
)[0] ||
seriesDetails?.created_by?.[0];
if (director && director.name && !series.content?.author) {
series.content.author = series.content.author || {
@@ -75,8 +77,5 @@ const POST = async (
await createResource(`series/${safeFileName(series.name)}.md`, series);
return json(series);
};
export const handler: Handlers = {
POST,
};
},
});

View File

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

View File

@@ -1,7 +1,7 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { getMovie } from "@lib/tmdb.ts";
import { json } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts";
import { define } from "../../../utils.ts";
type CachedMovieCredits = {
lastUpdated: number;
@@ -13,11 +13,9 @@ const cache = createCache<CachedMovieCredits>("movie-credits", {
expires: CACHE_INTERVAL,
});
const GET = async (
_req: Request,
_ctx: FreshContext,
) => {
const id = _ctx.params.id;
export const handler = define.handlers({
GET: async (ctx) => {
const id = ctx.params.id;
if (!id) {
return new Response("Bad Request", {
@@ -29,7 +27,8 @@ const GET = async (
const cachedResponse = cache.get(cacheId);
if (
cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
cachedResponse &&
Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
) {
return json(cachedResponse.data);
}
@@ -45,8 +44,5 @@ const GET = async (
);
return json(res);
};
export const handler: Handlers = {
GET,
};
},
});

View File

@@ -1,8 +1,8 @@
import { FreshContext } from "$fresh/server.ts";
import { getMovieCredits } from "@lib/tmdb.ts";
import { json } from "@lib/helpers.ts";
import { createLogger } from "@lib/log/index.ts";
import { createCache } from "@lib/cache.ts";
import { define } from "../../../../utils.ts";
type CachedMovieCredits = {
lastUpdated: number;
@@ -16,11 +16,9 @@ const cache = createCache<CachedMovieCredits>("movie-credits", {
const log = createLogger("api/tmdb");
export const handler = async (
_req: Request,
_ctx: FreshContext,
) => {
const id = _ctx.params.id;
export const handler = define.handlers({
GET: async (ctx) => {
const id = ctx.params.id;
if (!id) {
return new Response("Bad Request", {
@@ -34,7 +32,8 @@ export const handler = async (
const cachedResponse = cache.get(cacheId);
if (
cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
cachedResponse &&
Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
) {
return json(cachedResponse.data);
}
@@ -49,4 +48,5 @@ export const handler = async (
);
return json(res);
};
},
});

View File

@@ -1,17 +1,15 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { searchMovie, searchTVShow } from "@lib/tmdb.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { define } from "../../../utils.ts";
const GET = async (
req: Request,
ctx: FreshContext,
) => {
export const handler = define.handlers({
GET: async (ctx) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
const u = new URL(req.url);
const u = new URL(ctx.req.url);
const query = u.searchParams.get("q");
@@ -26,8 +24,5 @@ const GET = async (
: await searchTVShow(query);
return new Response(JSON.stringify(res.results));
};
export const handler: Handlers = {
GET,
};
},
});

View File

@@ -1,4 +1,4 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { MainLayout } from "@components/layouts/main.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { YoutubePlayer } from "@components/Youtube.tsx";
@@ -12,21 +12,22 @@ import { MetaTags } from "@components/MetaTags.tsx";
import { fetchResource } from "@lib/marka/index.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
import { parseRating } from "@lib/helpers.ts";
import { HttpError } from "fresh";
import { define } from "../../utils.ts";
export const handler: Handlers<{ article: ArticleResource; session: unknown }> =
{
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const article = await fetchResource<ArticleResource>(
`articles/${ctx.params.name}.md`,
);
if (!article) {
return ctx.renderNotFound();
throw new HttpError(404);
}
return ctx.render({ article, session: ctx.state.session });
return { data: { article, session: ctx.state.session } };
},
};
});
export default function Greet(
export default define.page(function (
props: PageProps<
{ article: ArticleResource; session: Record<string, string> }
>,
@@ -103,4 +104,4 @@ export default function Greet(
</div>
</MainLayout>
);
}
});

View File

@@ -1,28 +1,27 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { MainLayout } from "@components/layouts/main.tsx";
import { type ArticleResource, GenericResource } from "@lib/marka/schema.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { ResourceCard } from "@components/Card.tsx";
import { Link } from "@islands/Link.tsx";
import { listResources } from "@lib/marka/index.ts";
import { define } from "../../utils.ts";
import { TbArrowLeft } from "@preact-icons/tb";
export const handler: Handlers<
{ articles: ArticleResource[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const articles = await listResources<ArticleResource>("articles");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["articles"] });
return ctx.render({ articles, searchResults });
return { data: { articles, searchResults } };
},
};
});
export default function Greet(
export default define.page(function (
props: PageProps<
{ articles: ArticleResource[] | null; searchResults: GenericResource[] }
>,
@@ -36,13 +35,13 @@ export default function Greet(
searchResults={searchResults}
>
<header class="flex gap-4 items-center mb-5 md:hidden">
<Link
<a
class="px-4 ml-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex items-center gap-1"
href="/"
>
<IconArrowLeft class="w-5 h-5" />
<TbArrowLeft class="w-5 h-5" />
Back
</Link>
</a>
<h3 class="text-2xl text-white font-light">📝 Articles</h3>
</header>
@@ -59,4 +58,4 @@ export default function Greet(
</Grid>
</MainLayout>
);
}
});

View File

@@ -1,12 +1,13 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Card } from "@components/Card.tsx";
import { PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { resources } from "@lib/resources.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { define } from "../utils.ts";
// import "@lib/telegram.ts";
export default function Home(props: PageProps) {
export default define.page(function (props: PageProps) {
return (
<>
<RedirectSearchHandler />
@@ -16,7 +17,6 @@ export default function Home(props: PageProps) {
{Object.values(resources).filter((v) => v.link !== "/").map((m) => {
return (
<Card
splotch
key={m.link}
title={`${m.name}`}
backgroundSize={80}
@@ -33,4 +33,4 @@ export default function Home(props: PageProps) {
</MainLayout>
</>
);
}
});

View File

@@ -1,4 +1,3 @@
import { PageProps, RouteContext } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { ReviewResource } from "@lib/marka/schema.ts";
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
@@ -10,18 +9,18 @@ import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { HttpError } from "fresh";
import { define } from "../../utils.ts";
export default async function Greet(
props: PageProps<{ movie: ReviewResource; session: Record<string, string> }>,
ctx: RouteContext,
) {
export default define.page(async function (ctx) {
const props = ctx.req;
const movie = await fetchResource<ReviewResource>(
`movies/${ctx.params.name}.md`,
);
const session = ctx.state.session;
if (!movie) {
return ctx.renderNotFound();
throw new HttpError(404);
}
const { author, datePublished, reviewBody = "", reviewRating } =
@@ -87,4 +86,4 @@ export default async function Greet(
</div>
</MainLayout>
);
}
});

View File

@@ -2,13 +2,13 @@ import { MainLayout } from "@components/layouts/main.tsx";
import { GenericResource, ReviewResource } from "@lib/marka/schema.ts";
import { ResourceCard } from "@components/Card.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { parseRating } from "@lib/helpers.ts";
import { TbArrowLeft } from "@preact-icons/tb";
function sortOptional(a: number | string = 0, b: number | string = 0) {
return (parseRating(a) > parseRating(b)) ? 1 : -1;
@@ -44,7 +44,7 @@ export default async function MovieIndex(
class="px-4 ml-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex items-center gap-1"
href="/"
>
<IconArrowLeft class="w-5 h-5" />
{TbArrowLeft({ class: "w-5 h-5" })}
Back
</a>

View File

@@ -1,4 +1,4 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { IngredientsList } from "@islands/IngredientsList.tsx";
import { MainLayout } from "@components/layouts/main.tsx";
import Counter from "@islands/Counter.tsx";
@@ -14,24 +14,24 @@ import { fetchResource } from "@lib/marka/index.ts";
import { RecipeResource } from "@lib/marka/schema.ts";
import { parseIngredients } from "@lib/parseIngredient.ts";
import { parseRating } from "@lib/helpers.ts";
import { HttpError } from "fresh";
import { define } from "../../utils.ts";
export const handler: Handlers<
{ recipe: RecipeResource; session: unknown } | null
> = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
try {
const recipe = await fetchResource<RecipeResource>(
`recipes/${ctx.params.name}.md`,
);
if (!recipe) {
return ctx.renderNotFound();
throw new HttpError(404);
}
return ctx.render({ recipe, session: ctx.state.session });
return { data: { recipe, session: ctx.state.session } };
} catch (_e) {
return ctx.renderNotFound();
throw new HttpError(404);
}
},
};
});
function ValidRecipe({
recipe,
@@ -48,11 +48,13 @@ function ValidRecipe({
<h3 class="text-3xl my-5">Ingredients</h3>
{portion && <Counter count={amount} />}
</div>
{
<IngredientsList
ingredients={ingredients}
amount={amount}
portion={portion}
/>
}
<h3 class="text-3xl my-5">Preparation</h3>
<div class="pl-2">
<ol class="list-decimal grid gap-4">
@@ -75,7 +77,7 @@ function ValidRecipe({
);
}
export default function Page(
export default define.page(function (
props: PageProps<{ recipe: RecipeResource; session: Record<string, string> }>,
) {
const { recipe, session } = props.data;
@@ -135,10 +137,10 @@ export default function Page(
)
: (
<div class="whitespace-break-spaces markdown-body">
{JSON.stringify(recipe)}
{recipe}
</div>
)}
</div>
</MainLayout>
);
}
});

View File

@@ -1,35 +1,36 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { MainLayout } from "@components/layouts/main.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { TbArrowLeft } from "@preact-icons/tb";
import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { ResourceCard } from "@components/Card.tsx";
import { listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource, RecipeResource } from "@lib/marka/schema.ts";
import { define } from "../../utils.ts";
export const handler: Handlers<
{ recipes: RecipeResource[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const recipes = await listResources<RecipeResource>("recipes");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["recipes"] });
return ctx.render({ recipes, searchResults });
return { data: { recipes, searchResults } };
},
};
});
export default function Greet(
props: PageProps<
{ recipes: RecipeResource[] | null; searchResults: GenericResource[] }
>,
export default define.page(function (
{ data, url }: PageProps<{
recipes: RecipeResource[] | null;
searchResults: GenericResource[];
}>,
) {
const { recipes, searchResults } = props.data;
const { recipes, searchResults } = data;
return (
<MainLayout
url={props.url}
url={url}
title="Recipes"
searchResults={searchResults}
context={{ type: "recipes" }}
@@ -41,7 +42,7 @@ export default function Greet(
class="px-4 lg:ml-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex items-center gap-1"
href="/"
>
<IconArrowLeft class="w-5 h-5" />
<TbArrowLeft class="w-5 h-5" />
Back
</a>
@@ -54,4 +55,4 @@ export default function Greet(
</Grid>
</MainLayout>
);
}
});

View File

@@ -1,4 +1,4 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { MainLayout } from "@components/layouts/main.tsx";
import { HashTags } from "@components/HashTags.tsx";
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
@@ -9,22 +9,24 @@ import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { getNameOfResource, ReviewResource } from "@lib/marka/schema.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { HttpError } from "fresh";
import { define } from "../../utils.ts";
export const handler: Handlers<{ serie: ReviewResource; session: unknown }> = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const serie = await fetchResource<ReviewResource>(
`series/${ctx.params.name}.md`,
);
if (!serie) {
return ctx.renderNotFound();
throw new HttpError(404);
}
return ctx.render({ serie, session: ctx.state.session });
return { data: { serie, session: ctx.state.session } };
},
};
});
export default function Greet(
export default define.page(function (
props: PageProps<{ serie: ReviewResource; session: Record<string, string> }>,
) {
const { serie, session } = props.data;
@@ -94,4 +96,4 @@ export default function Greet(
</div>
</MainLayout>
);
}
});

View File

@@ -1,27 +1,27 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { MainLayout } from "@components/layouts/main.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { ResourceCard } from "@components/Card.tsx";
import { listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource, ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../utils.ts";
import { TbArrowLeft } from "@preact-icons/tb";
export const handler: Handlers<
{ series: ReviewResource[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const series = await listResources<ReviewResource>("series");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["series"] });
return ctx.render({ series, searchResults });
return { data: { series, searchResults } };
},
};
});
export default function Greet(
export default define.page(function (
props: PageProps<
{ series: ReviewResource[] | null; searchResults: GenericResource[] }
>,
@@ -42,7 +42,7 @@ export default function Greet(
class="px-4 ml-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex items-center gap-1"
href="/"
>
<IconArrowLeft class="w-5 h-5" />
<TbArrowLeft class="w-5 h-5" />
Back
</a>
@@ -57,4 +57,4 @@ export default function Greet(
</Grid>
</MainLayout>
);
}
});

12
static/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
data/
data-dev/
_fresh/
node_modules/
mise.toml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Some files were not shown because too many files have changed in this diff Show More