feat: add initial recommendation data
This commit is contained in:
parent
517b1ba23d
commit
cc112b7554
@ -20,48 +20,18 @@ export type Props = {
|
|||||||
export const MainLayout = (
|
export const MainLayout = (
|
||||||
{ children, url, title, context, searchResults }: Props,
|
{ children, url, title, context, searchResults }: Props,
|
||||||
) => {
|
) => {
|
||||||
const hasSearch = url.search.includes("q=");
|
const _url = typeof url === "string" ? new URL(url) : url;
|
||||||
|
const hasSearch = _url.search.includes("q=");
|
||||||
|
|
||||||
return (
|
if (hasSearch) {
|
||||||
<div
|
return (
|
||||||
class="md:grid mx-auto"
|
<Search
|
||||||
style={{ gridTemplateColumns: "200px 1fr", maxWidth: "1024px" }}
|
q={_url.searchParams.get("q")}
|
||||||
>
|
{...context}
|
||||||
<Head>
|
results={searchResults}
|
||||||
<style>{CSS}</style>
|
/>
|
||||||
<style>{KATEX_CSS}</style>
|
);
|
||||||
{title &&
|
}
|
||||||
<title>{title}</title>}
|
|
||||||
</Head>
|
return <>{children}</>;
|
||||||
<aside class="p-4 hidden md:block">
|
|
||||||
<nav class="min-h-fit rounded-3xl p-3 grid gap-3 fixed t-0">
|
|
||||||
{Object.values(resources).map((m) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={m.link}
|
|
||||||
class={`flex items-center gap-2 ${
|
|
||||||
m.link === url.pathname ? "bg-white text-black" : "text-white"
|
|
||||||
} p-3 text-xl w-full rounded-2xl`}
|
|
||||||
>
|
|
||||||
{<Emoji class="w-6 h-6" name={m.emoji} />} {m.name}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
<main
|
|
||||||
class="py-5"
|
|
||||||
style={{ fontFamily: "Work Sans" }}
|
|
||||||
>
|
|
||||||
{hasSearch && (
|
|
||||||
<Search
|
|
||||||
q={url.searchParams.get("q")}
|
|
||||||
{...context}
|
|
||||||
results={searchResults}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!hasSearch && children}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"lock": false,
|
"lock": false,
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
||||||
"update": "deno run -A -r https://fresh.deno.dev/update .",
|
|
||||||
"build": "deno run -A dev.ts build",
|
"build": "deno run -A dev.ts build",
|
||||||
"preview": "deno run -A main.ts"
|
"preview": "deno run -A main.ts",
|
||||||
|
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"rules": {
|
"rules": {
|
||||||
@ -27,7 +28,7 @@
|
|||||||
"@islands": "./islands",
|
"@islands": "./islands",
|
||||||
"@islands/": "./islands/",
|
"@islands/": "./islands/",
|
||||||
"zod": "https://deno.land/x/zod@v3.21.4/mod.ts",
|
"zod": "https://deno.land/x/zod@v3.21.4/mod.ts",
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.4.2/",
|
"$fresh/": "https://deno.land/x/fresh@1.4.3/",
|
||||||
"preact": "https://esm.sh/preact@10.15.1",
|
"preact": "https://esm.sh/preact@10.15.1",
|
||||||
"preact/": "https://esm.sh/preact@10.15.1/",
|
"preact/": "https://esm.sh/preact@10.15.1/",
|
||||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1",
|
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1",
|
||||||
|
3
dev.ts
3
dev.ts
@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env -S deno run -A --watch=static/,routes/
|
#!/usr/bin/env -S deno run -A --watch=static/,routes/
|
||||||
|
|
||||||
import dev from "$fresh/dev.ts";
|
import dev from "$fresh/dev.ts";
|
||||||
|
import config from "./fresh.config.ts";
|
||||||
|
|
||||||
await dev(import.meta.url, "./main.ts");
|
await dev(import.meta.url, "./main.ts", config);
|
||||||
|
6
fresh.config.ts
Normal file
6
fresh.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from "$fresh/server.ts";
|
||||||
|
import twindPlugin from "$fresh/plugins/twind.ts";
|
||||||
|
import twindConfig from "./twind.config.ts";
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [twindPlugin(twindConfig)],
|
||||||
|
});
|
146
fresh.gen.ts
146
fresh.gen.ts
@ -4,42 +4,43 @@
|
|||||||
|
|
||||||
import * as $0 from "./routes/_404.tsx";
|
import * as $0 from "./routes/_404.tsx";
|
||||||
import * as $1 from "./routes/_app.tsx";
|
import * as $1 from "./routes/_app.tsx";
|
||||||
import * as $2 from "./routes/_middleware.ts";
|
import * as $2 from "./routes/_layout.tsx";
|
||||||
import * as $3 from "./routes/admin/log/index.tsx";
|
import * as $3 from "./routes/_middleware.ts";
|
||||||
import * as $4 from "./routes/admin/performance/index.tsx";
|
import * as $4 from "./routes/admin/log/index.tsx";
|
||||||
import * as $5 from "./routes/api/articles/[name].ts";
|
import * as $5 from "./routes/admin/performance/index.tsx";
|
||||||
import * as $6 from "./routes/api/articles/create/index.ts";
|
import * as $6 from "./routes/api/articles/[name].ts";
|
||||||
import * as $7 from "./routes/api/articles/index.ts";
|
import * as $7 from "./routes/api/articles/create/index.ts";
|
||||||
import * as $8 from "./routes/api/auth/callback.ts";
|
import * as $8 from "./routes/api/articles/index.ts";
|
||||||
import * as $9 from "./routes/api/auth/login.ts";
|
import * as $9 from "./routes/api/auth/callback.ts";
|
||||||
import * as $10 from "./routes/api/auth/logout.ts";
|
import * as $10 from "./routes/api/auth/login.ts";
|
||||||
import * as $11 from "./routes/api/cache/index.ts";
|
import * as $11 from "./routes/api/auth/logout.ts";
|
||||||
import * as $12 from "./routes/api/images/index.ts";
|
import * as $12 from "./routes/api/cache/index.ts";
|
||||||
import * as $13 from "./routes/api/index.ts";
|
import * as $13 from "./routes/api/images/index.ts";
|
||||||
import * as $14 from "./routes/api/logs.ts";
|
import * as $14 from "./routes/api/index.ts";
|
||||||
import * as $15 from "./routes/api/movies/[name].ts";
|
import * as $15 from "./routes/api/logs.ts";
|
||||||
import * as $16 from "./routes/api/movies/enhance/[name].ts";
|
import * as $16 from "./routes/api/movies/[name].ts";
|
||||||
import * as $17 from "./routes/api/movies/index.ts";
|
import * as $17 from "./routes/api/movies/enhance/[name].ts";
|
||||||
import * as $18 from "./routes/api/query/index.ts";
|
import * as $18 from "./routes/api/movies/index.ts";
|
||||||
import * as $19 from "./routes/api/query/sync.ts";
|
import * as $19 from "./routes/api/query/index.ts";
|
||||||
import * as $20 from "./routes/api/recipes/[name].ts";
|
import * as $20 from "./routes/api/query/sync.ts";
|
||||||
import * as $21 from "./routes/api/recipes/index.ts";
|
import * as $21 from "./routes/api/recipes/[name].ts";
|
||||||
import * as $22 from "./routes/api/resources.ts";
|
import * as $22 from "./routes/api/recipes/index.ts";
|
||||||
import * as $23 from "./routes/api/series/[name].ts";
|
import * as $23 from "./routes/api/resources.ts";
|
||||||
import * as $24 from "./routes/api/series/enhance/[name].ts";
|
import * as $24 from "./routes/api/series/[name].ts";
|
||||||
import * as $25 from "./routes/api/series/index.ts";
|
import * as $25 from "./routes/api/series/enhance/[name].ts";
|
||||||
import * as $26 from "./routes/api/tmdb/[id].ts";
|
import * as $26 from "./routes/api/series/index.ts";
|
||||||
import * as $27 from "./routes/api/tmdb/credits/[id].ts";
|
import * as $27 from "./routes/api/tmdb/[id].ts";
|
||||||
import * as $28 from "./routes/api/tmdb/query.ts";
|
import * as $28 from "./routes/api/tmdb/credits/[id].ts";
|
||||||
import * as $29 from "./routes/articles/[name].tsx";
|
import * as $29 from "./routes/api/tmdb/query.ts";
|
||||||
import * as $30 from "./routes/articles/index.tsx";
|
import * as $30 from "./routes/articles/[name].tsx";
|
||||||
import * as $31 from "./routes/index.tsx";
|
import * as $31 from "./routes/articles/index.tsx";
|
||||||
import * as $32 from "./routes/movies/[name].tsx";
|
import * as $32 from "./routes/index.tsx";
|
||||||
import * as $33 from "./routes/movies/index.tsx";
|
import * as $33 from "./routes/movies/[name].tsx";
|
||||||
import * as $34 from "./routes/recipes/[name].tsx";
|
import * as $34 from "./routes/movies/index.tsx";
|
||||||
import * as $35 from "./routes/recipes/index.tsx";
|
import * as $35 from "./routes/recipes/[name].tsx";
|
||||||
import * as $36 from "./routes/series/[name].tsx";
|
import * as $36 from "./routes/recipes/index.tsx";
|
||||||
import * as $37 from "./routes/series/index.tsx";
|
import * as $37 from "./routes/series/[name].tsx";
|
||||||
|
import * as $38 from "./routes/series/index.tsx";
|
||||||
import * as $$0 from "./islands/Counter.tsx";
|
import * as $$0 from "./islands/Counter.tsx";
|
||||||
import * as $$1 from "./islands/IngredientsList.tsx";
|
import * as $$1 from "./islands/IngredientsList.tsx";
|
||||||
import * as $$2 from "./islands/KMenu.tsx";
|
import * as $$2 from "./islands/KMenu.tsx";
|
||||||
@ -56,42 +57,43 @@ const manifest = {
|
|||||||
routes: {
|
routes: {
|
||||||
"./routes/_404.tsx": $0,
|
"./routes/_404.tsx": $0,
|
||||||
"./routes/_app.tsx": $1,
|
"./routes/_app.tsx": $1,
|
||||||
"./routes/_middleware.ts": $2,
|
"./routes/_layout.tsx": $2,
|
||||||
"./routes/admin/log/index.tsx": $3,
|
"./routes/_middleware.ts": $3,
|
||||||
"./routes/admin/performance/index.tsx": $4,
|
"./routes/admin/log/index.tsx": $4,
|
||||||
"./routes/api/articles/[name].ts": $5,
|
"./routes/admin/performance/index.tsx": $5,
|
||||||
"./routes/api/articles/create/index.ts": $6,
|
"./routes/api/articles/[name].ts": $6,
|
||||||
"./routes/api/articles/index.ts": $7,
|
"./routes/api/articles/create/index.ts": $7,
|
||||||
"./routes/api/auth/callback.ts": $8,
|
"./routes/api/articles/index.ts": $8,
|
||||||
"./routes/api/auth/login.ts": $9,
|
"./routes/api/auth/callback.ts": $9,
|
||||||
"./routes/api/auth/logout.ts": $10,
|
"./routes/api/auth/login.ts": $10,
|
||||||
"./routes/api/cache/index.ts": $11,
|
"./routes/api/auth/logout.ts": $11,
|
||||||
"./routes/api/images/index.ts": $12,
|
"./routes/api/cache/index.ts": $12,
|
||||||
"./routes/api/index.ts": $13,
|
"./routes/api/images/index.ts": $13,
|
||||||
"./routes/api/logs.ts": $14,
|
"./routes/api/index.ts": $14,
|
||||||
"./routes/api/movies/[name].ts": $15,
|
"./routes/api/logs.ts": $15,
|
||||||
"./routes/api/movies/enhance/[name].ts": $16,
|
"./routes/api/movies/[name].ts": $16,
|
||||||
"./routes/api/movies/index.ts": $17,
|
"./routes/api/movies/enhance/[name].ts": $17,
|
||||||
"./routes/api/query/index.ts": $18,
|
"./routes/api/movies/index.ts": $18,
|
||||||
"./routes/api/query/sync.ts": $19,
|
"./routes/api/query/index.ts": $19,
|
||||||
"./routes/api/recipes/[name].ts": $20,
|
"./routes/api/query/sync.ts": $20,
|
||||||
"./routes/api/recipes/index.ts": $21,
|
"./routes/api/recipes/[name].ts": $21,
|
||||||
"./routes/api/resources.ts": $22,
|
"./routes/api/recipes/index.ts": $22,
|
||||||
"./routes/api/series/[name].ts": $23,
|
"./routes/api/resources.ts": $23,
|
||||||
"./routes/api/series/enhance/[name].ts": $24,
|
"./routes/api/series/[name].ts": $24,
|
||||||
"./routes/api/series/index.ts": $25,
|
"./routes/api/series/enhance/[name].ts": $25,
|
||||||
"./routes/api/tmdb/[id].ts": $26,
|
"./routes/api/series/index.ts": $26,
|
||||||
"./routes/api/tmdb/credits/[id].ts": $27,
|
"./routes/api/tmdb/[id].ts": $27,
|
||||||
"./routes/api/tmdb/query.ts": $28,
|
"./routes/api/tmdb/credits/[id].ts": $28,
|
||||||
"./routes/articles/[name].tsx": $29,
|
"./routes/api/tmdb/query.ts": $29,
|
||||||
"./routes/articles/index.tsx": $30,
|
"./routes/articles/[name].tsx": $30,
|
||||||
"./routes/index.tsx": $31,
|
"./routes/articles/index.tsx": $31,
|
||||||
"./routes/movies/[name].tsx": $32,
|
"./routes/index.tsx": $32,
|
||||||
"./routes/movies/index.tsx": $33,
|
"./routes/movies/[name].tsx": $33,
|
||||||
"./routes/recipes/[name].tsx": $34,
|
"./routes/movies/index.tsx": $34,
|
||||||
"./routes/recipes/index.tsx": $35,
|
"./routes/recipes/[name].tsx": $35,
|
||||||
"./routes/series/[name].tsx": $36,
|
"./routes/recipes/index.tsx": $36,
|
||||||
"./routes/series/index.tsx": $37,
|
"./routes/series/[name].tsx": $37,
|
||||||
|
"./routes/series/index.tsx": $38,
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
"./islands/Counter.tsx": $$0,
|
"./islands/Counter.tsx": $$0,
|
||||||
|
@ -71,7 +71,7 @@ export const SearchResultItem = (
|
|||||||
) => {
|
) => {
|
||||||
const doc = item.document;
|
const doc = item.document;
|
||||||
const resourceType = resources[doc.type];
|
const resourceType = resources[doc.type];
|
||||||
const href = (resourceType) ? `${resourceType.link}/${doc.id}` : "";
|
const href = resourceType ? `${resourceType.link}/${doc.id}` : "";
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { OpenAI } from "https://deno.land/x/openai/mod.ts";
|
import { OpenAI } from "https://deno.land/x/openai@1.4.2/mod.ts";
|
||||||
import { OPENAI_API_KEY } from "@lib/env.ts";
|
import { OPENAI_API_KEY } from "@lib/env.ts";
|
||||||
|
|
||||||
const openAI = OPENAI_API_KEY && new OpenAI(OPENAI_API_KEY);
|
const openAI = OPENAI_API_KEY && new OpenAI(OPENAI_API_KEY);
|
||||||
@ -63,6 +63,28 @@ export async function extractAuthorName(content: string) {
|
|||||||
return author;
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createKeywords(type: string, description: string) {
|
||||||
|
if (!openAI) return;
|
||||||
|
const chatCompletion = await openAI.createChatCompletion({
|
||||||
|
model: "gpt-3.5-turbo",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content":
|
||||||
|
`you create some general vibey keywords to use in a recommendation system based on a ${type} description`,
|
||||||
|
},
|
||||||
|
{ "role": "user", "content": description.slice(0, 2000) },
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "return a list of keywords seperated by commas",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return chatCompletion.choices[0].message.content?.toLowerCase().split(", ")
|
||||||
|
.map((v) => v.replaceAll(" ", "-"));
|
||||||
|
}
|
||||||
|
|
||||||
export async function createTags(content: string) {
|
export async function createTags(content: string) {
|
||||||
if (!openAI) return;
|
if (!openAI) return;
|
||||||
const chatCompletion = await openAI.createChatCompletion({
|
const chatCompletion = await openAI.createChatCompletion({
|
||||||
|
53
lib/recommendation.ts
Normal file
53
lib/recommendation.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import * as cache from "@lib/cache/cache.ts";
|
||||||
|
import * as openai from "@lib/openai.ts";
|
||||||
|
import { GenericResource } from "@lib/types.ts";
|
||||||
|
import { parseRating } from "@lib/helpers.ts";
|
||||||
|
|
||||||
|
type RecommendationResource = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
rating: number;
|
||||||
|
tags?: string[];
|
||||||
|
keywords?: string[];
|
||||||
|
author?: string;
|
||||||
|
year?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createRecommendationResource(
|
||||||
|
res: GenericResource,
|
||||||
|
description?: string,
|
||||||
|
) {
|
||||||
|
const cacheId = `recommendations:${res.type}:${res.id}`;
|
||||||
|
const resource: RecommendationResource = await cache.get(cacheId) || {
|
||||||
|
id: res.id,
|
||||||
|
type: res.type,
|
||||||
|
rating: -1,
|
||||||
|
};
|
||||||
|
if (description && !resource.keywords) {
|
||||||
|
const keywords = await openai.createKeywords(res.type, description);
|
||||||
|
if (keywords?.length) {
|
||||||
|
resource.keywords = keywords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { author, date, rating } = res.meta || {};
|
||||||
|
|
||||||
|
if (res?.tags) {
|
||||||
|
resource.tags = res.tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rating !== "undefined") {
|
||||||
|
resource.rating = parseRating(rating);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author) {
|
||||||
|
resource.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
const d = typeof date === "string" ? new Date(date) : date;
|
||||||
|
resource.year = d.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.set(cacheId, JSON.stringify(resource));
|
||||||
|
}
|
@ -12,6 +12,8 @@ export type Movie = {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
meta: {
|
meta: {
|
||||||
date: Date;
|
date: Date;
|
||||||
|
tmdbId?: number;
|
||||||
|
keywords?: string[];
|
||||||
image: string;
|
image: string;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
average?: string;
|
average?: string;
|
||||||
@ -26,6 +28,11 @@ export function renderMovie(movie: Movie) {
|
|||||||
meta.date = formatDate(meta.date) as unknown as Date;
|
meta.date = formatDate(meta.date) as unknown as Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete meta.thumbnail;
|
||||||
|
delete meta.average;
|
||||||
|
|
||||||
|
const movieImage = ``;
|
||||||
|
|
||||||
return fixRenderedMarkdown(`${
|
return fixRenderedMarkdown(`${
|
||||||
meta
|
meta
|
||||||
? `---
|
? `---
|
||||||
@ -35,7 +42,11 @@ ${stringify(meta)}
|
|||||||
---`
|
---`
|
||||||
}
|
}
|
||||||
# ${movie.name}
|
# ${movie.name}
|
||||||
${movie.meta.image ? `` : ""}
|
${
|
||||||
|
// So we do not add a new image to the description everytime we render
|
||||||
|
(movie.meta.image && !movie.description.includes(movieImage))
|
||||||
|
? movieImage
|
||||||
|
: ""}
|
||||||
${movie.tags.map((t) => `#${t}`).join(" ")}
|
${movie.tags.map((t) => `#${t}`).join(" ")}
|
||||||
${movie.description}
|
${movie.description}
|
||||||
`);
|
`);
|
||||||
@ -103,6 +114,10 @@ const crud = createCrud<Movie>({
|
|||||||
hasThumbnails: true,
|
hasThumbnails: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getMovie = crud.read;
|
export const getMovie = async (id: string) => {
|
||||||
|
const movie = await crud.read(id);
|
||||||
|
return movie;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAllMovies = crud.readAll;
|
export const getAllMovies = crud.readAll;
|
||||||
export const createMovie = crud.create;
|
export const createMovie = crud.create;
|
||||||
|
@ -15,6 +15,7 @@ export type Series = {
|
|||||||
date: Date;
|
date: Date;
|
||||||
image: string;
|
image: string;
|
||||||
author: string;
|
author: string;
|
||||||
|
tmdbId?: number;
|
||||||
rating: number;
|
rating: number;
|
||||||
average?: string;
|
average?: string;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
@ -22,12 +23,17 @@ export type Series = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderSeries(movie: Series) {
|
function renderSeries(series: Series) {
|
||||||
const meta = movie.meta;
|
const meta = series.meta;
|
||||||
if ("date" in meta) {
|
if ("date" in meta) {
|
||||||
meta.date = formatDate(meta.date);
|
meta.date = formatDate(meta.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delete meta.thumbnail;
|
||||||
|
delete meta.average;
|
||||||
|
|
||||||
|
const movieImage = ``;
|
||||||
|
|
||||||
return fixRenderedMarkdown(`${
|
return fixRenderedMarkdown(`${
|
||||||
meta
|
meta
|
||||||
? `---
|
? `---
|
||||||
@ -36,10 +42,14 @@ ${stringify(meta)}
|
|||||||
: `---
|
: `---
|
||||||
---`
|
---`
|
||||||
}
|
}
|
||||||
# ${movie.name}
|
# ${series.name}
|
||||||
${movie.meta.image ? `` : ""}
|
${
|
||||||
${movie.tags.map((t) => `#${t}`).join(" ")}
|
// So we do not add a new image to the description everytime we render
|
||||||
${movie.description}
|
(series.meta.image && !series.description.includes(movieImage))
|
||||||
|
? movieImage
|
||||||
|
: ""}
|
||||||
|
${series.tags.map((t) => `#${t}`).join(" ")}
|
||||||
|
${series.description}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { BadRequestError } from "@lib/errors.ts";
|
|
||||||
import { resources } from "@lib/resources.ts";
|
import { resources } from "@lib/resources.ts";
|
||||||
import { SearchResult } from "@lib/types.ts";
|
import { SearchResult } from "@lib/types.ts";
|
||||||
import { getTypeSenseClient } from "@lib/typesense.ts";
|
import { getTypeSenseClient } from "@lib/typesense.ts";
|
||||||
@ -15,9 +14,9 @@ type SearchParams = {
|
|||||||
query_by?: string;
|
query_by?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseResourceUrl(_url: string): SearchParams | undefined {
|
export function parseResourceUrl(_url: string | URL): SearchParams | undefined {
|
||||||
try {
|
try {
|
||||||
const url = new URL(_url);
|
const url = typeof _url === "string" ? new URL(_url) : _url;
|
||||||
let query = url.searchParams.get("q") || "*";
|
let query = url.searchParams.get("q") || "*";
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -37,6 +37,7 @@ export interface TMDBSeries {
|
|||||||
export type GenericResource = {
|
export type GenericResource = {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
tags?: string[];
|
||||||
type: keyof typeof resources;
|
type: keyof typeof resources;
|
||||||
meta?: {
|
meta?: {
|
||||||
image?: string;
|
image?: string;
|
||||||
|
5
main.ts
5
main.ts
@ -8,8 +8,7 @@ import "$std/dotenv/load.ts";
|
|||||||
|
|
||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "./fresh.gen.ts";
|
||||||
|
import config from "./fresh.config.ts";
|
||||||
|
|
||||||
import twindPlugin from "$fresh/plugins/twind.ts";
|
await start(manifest, config);
|
||||||
import twindConfig from "./twind.config.ts";
|
|
||||||
|
|
||||||
await start(manifest, { plugins: [twindPlugin(twindConfig)] });
|
|
||||||
|
41
routes/_layout.tsx
Normal file
41
routes/_layout.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { LayoutProps } from "$fresh/server.ts";
|
||||||
|
import { resources } from "@lib/resources.ts";
|
||||||
|
import { CSS, KATEX_CSS } from "https://deno.land/x/gfm@0.2.5/mod.ts";
|
||||||
|
import { Head } from "$fresh/runtime.ts";
|
||||||
|
import { Emoji } from "@components/Emoji.tsx";
|
||||||
|
|
||||||
|
export default function MyLayout({ Component, url }: LayoutProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="md:grid mx-auto"
|
||||||
|
style={{ gridTemplateColumns: "200px 1fr", maxWidth: "1024px" }}
|
||||||
|
>
|
||||||
|
<Head>
|
||||||
|
<style>{CSS}</style>
|
||||||
|
<style>{KATEX_CSS}</style>
|
||||||
|
</Head>
|
||||||
|
<aside class="p-4 hidden md:block">
|
||||||
|
<nav class="min-h-fit rounded-3xl p-3 grid gap-3 fixed t-0">
|
||||||
|
{Object.values(resources).map((m) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={m.link}
|
||||||
|
class={`flex items-center gap-2 ${
|
||||||
|
m.link === url.pathname ? "bg-white text-black" : "text-white"
|
||||||
|
} p-3 text-xl w-full rounded-2xl`}
|
||||||
|
>
|
||||||
|
{<Emoji class="w-6 h-6" name={m.emoji} />} {m.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main
|
||||||
|
class="py-5"
|
||||||
|
style={{ fontFamily: "Work Sans" }}
|
||||||
|
>
|
||||||
|
<Component />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -25,8 +25,11 @@ export const handler: Handlers = {
|
|||||||
|
|
||||||
const releaseDate = movieDetails.release_date;
|
const releaseDate = movieDetails.release_date;
|
||||||
const posterPath = movieDetails.poster_path;
|
const posterPath = movieDetails.poster_path;
|
||||||
const director =
|
const director = movieCredits?.crew?.filter?.((person) =>
|
||||||
movieCredits?.crew?.filter?.((person) => person.job === "Director")[0];
|
person.job === "Director"
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
movieDetails.overview;
|
||||||
|
|
||||||
let finalPath = "";
|
let finalPath = "";
|
||||||
const name = movieDetails.title || movieDetails.original_title ||
|
const name = movieDetails.title || movieDetails.original_title ||
|
||||||
@ -41,7 +44,9 @@ export const handler: Handlers = {
|
|||||||
await createDocument(finalPath, poster);
|
await createDocument(finalPath, poster);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = {} as Movie["meta"];
|
const metadata = {
|
||||||
|
tmdbId,
|
||||||
|
} as Movie["meta"];
|
||||||
if (releaseDate) {
|
if (releaseDate) {
|
||||||
metadata.date = new Date(releaseDate);
|
metadata.date = new Date(releaseDate);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
NotFoundError,
|
NotFoundError,
|
||||||
} from "@lib/errors.ts";
|
} from "@lib/errors.ts";
|
||||||
import * as cache from "@lib/cache/cache.ts";
|
import * as cache from "@lib/cache/cache.ts";
|
||||||
|
import { createRecommendationResource } from "@lib/recommendation.ts";
|
||||||
|
|
||||||
const POST = async (
|
const POST = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
@ -42,8 +43,9 @@ const POST = async (
|
|||||||
movie.meta.date = new Date(releaseDate);
|
movie.meta.date = new Date(releaseDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const director =
|
const director = movieCredits?.crew?.filter?.((person) =>
|
||||||
movieCredits?.crew?.filter?.((person) => person.job === "Director")[0];
|
person.job === "Director"
|
||||||
|
)[0];
|
||||||
if (director && !movie.meta.author) {
|
if (director && !movie.meta.author) {
|
||||||
movie.meta.author = director.name;
|
movie.meta.author = director.name;
|
||||||
}
|
}
|
||||||
@ -57,6 +59,10 @@ const POST = async (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!movie.meta.tmdbId) {
|
||||||
|
movie.meta.tmdbId = tmdbId;
|
||||||
|
}
|
||||||
|
|
||||||
let finalPath = "";
|
let finalPath = "";
|
||||||
const posterPath = movieDetails.poster_path;
|
const posterPath = movieDetails.poster_path;
|
||||||
if (posterPath && !movie.meta.image) {
|
if (posterPath && !movie.meta.image) {
|
||||||
@ -72,6 +78,8 @@ const POST = async (
|
|||||||
|
|
||||||
cache.del(`documents:Media:movies:${name}.md`);
|
cache.del(`documents:Media:movies:${name}.md`);
|
||||||
|
|
||||||
|
createRecommendationResource(movie, movieDetails.overview);
|
||||||
|
|
||||||
return json(movie);
|
return json(movie);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export const handler: Handlers = {
|
|||||||
await createDocument(finalPath, poster);
|
await createDocument(finalPath, poster);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = {} as Series["meta"];
|
const metadata = { tmdbId } as Series["meta"];
|
||||||
if (releaseDate) {
|
if (releaseDate) {
|
||||||
metadata.date = new Date(releaseDate);
|
metadata.date = new Date(releaseDate);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps, RouteContext } from "$fresh/server.ts";
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
import { getMovie, Movie } from "@lib/resource/movies.ts";
|
||||||
import { RecipeHero } from "@components/RecipeHero.tsx";
|
import { RecipeHero } from "@components/RecipeHero.tsx";
|
||||||
@ -7,17 +7,12 @@ import { renderMarkdown } from "@lib/documents.ts";
|
|||||||
import { KMenu } from "@islands/KMenu.tsx";
|
import { KMenu } from "@islands/KMenu.tsx";
|
||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
|
|
||||||
export const handler: Handlers<Movie | null> = {
|
export default async function Greet(
|
||||||
async GET(_, ctx) {
|
|
||||||
const movie = await getMovie(ctx.params.name);
|
|
||||||
return ctx.render({ movie, session: ctx.state.session });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Greet(
|
|
||||||
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
|
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
|
||||||
|
ctx: RouteContext,
|
||||||
) {
|
) {
|
||||||
const { movie, session } = props.data;
|
const movie = await getMovie(ctx.params.name);
|
||||||
|
const session = ctx.state.session;
|
||||||
|
|
||||||
const { author = "", date = "" } = movie.meta;
|
const { author = "", date = "" } = movie.meta;
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { MainLayout } from "@components/layouts/main.tsx";
|
import { MainLayout } from "@components/layouts/main.tsx";
|
||||||
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
|
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
|
||||||
import { ResourceCard } from "@components/Card.tsx";
|
import { ResourceCard } from "@components/Card.tsx";
|
||||||
@ -8,26 +7,18 @@ import { KMenu } from "@islands/KMenu.tsx";
|
|||||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||||
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
import { parseResourceUrl, searchResource } from "@lib/search.ts";
|
||||||
import { SearchResult } from "@lib/types.ts";
|
import { SearchResult } from "@lib/types.ts";
|
||||||
|
import { PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
export const handler: Handlers<
|
export default async function Greet(
|
||||||
{ movies: Movie[] | null; searchResults?: SearchResult }
|
|
||||||
> = {
|
|
||||||
async GET(req, ctx) {
|
|
||||||
const movies = await getAllMovies();
|
|
||||||
const searchParams = parseResourceUrl(req.url);
|
|
||||||
const searchResults = searchParams &&
|
|
||||||
await searchResource({ ...searchParams, type: "movie" });
|
|
||||||
return ctx.render({
|
|
||||||
movies: movies.sort((a, b) => a?.meta?.rating > b?.meta?.rating ? -1 : 1),
|
|
||||||
searchResults,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Greet(
|
|
||||||
props: PageProps<{ movies: Movie[] | null; searchResults: SearchResult }>,
|
props: PageProps<{ movies: Movie[] | null; searchResults: SearchResult }>,
|
||||||
) {
|
) {
|
||||||
const { movies, searchResults } = props.data;
|
const allMovies = await getAllMovies();
|
||||||
|
const searchParams = parseResourceUrl(props.url);
|
||||||
|
const searchResults = searchParams &&
|
||||||
|
await searchResource({ ...searchParams, type: "movie" });
|
||||||
|
const movies = allMovies.sort((a, b) =>
|
||||||
|
a?.meta?.rating > b?.meta?.rating ? -1 : 1
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout
|
<MainLayout
|
||||||
|
Loading…
x
Reference in New Issue
Block a user