diff --git a/deno.json b/deno.json index 3d3c7e9..776fe65 100755 --- a/deno.json +++ b/deno.json @@ -15,7 +15,7 @@ }, "imports": { "$fresh/": "https://deno.land/x/fresh@1.3.1/", - "yaml": "https://deno.land/std@0.196.0/yaml/parse.ts", + "yaml": "https://deno.land/std@0.194.0/yaml/mod.ts", "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.0", diff --git a/fresh.gen.ts b/fresh.gen.ts index 3791f39..2188ec2 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -31,7 +31,10 @@ import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/IngredientsList.tsx"; import * as $$2 from "./islands/KMenu.tsx"; import * as $$3 from "./islands/KMenu/commands.ts"; -import * as $$4 from "./islands/KMenu/types.ts"; +import * as $$4 from "./islands/KMenu/commands/add_movie_infos.ts"; +import * as $$5 from "./islands/KMenu/commands/create_article.ts"; +import * as $$6 from "./islands/KMenu/commands/create_movie.ts"; +import * as $$7 from "./islands/KMenu/types.ts"; const manifest = { routes: { @@ -66,7 +69,10 @@ const manifest = { "./islands/IngredientsList.tsx": $$1, "./islands/KMenu.tsx": $$2, "./islands/KMenu/commands.ts": $$3, - "./islands/KMenu/types.ts": $$4, + "./islands/KMenu/commands/add_movie_infos.ts": $$4, + "./islands/KMenu/commands/create_article.ts": $$5, + "./islands/KMenu/commands/create_movie.ts": $$6, + "./islands/KMenu/types.ts": $$7, }, baseUrl: import.meta.url, }; diff --git a/islands/KMenu.tsx b/islands/KMenu.tsx index 8d81439..c131a54 100644 --- a/islands/KMenu.tsx +++ b/islands/KMenu.tsx @@ -37,6 +37,7 @@ export const KMenu = ( const loadingText = useSignal(""); const activeIndex = useSignal(-1); + const containerRef = useRef(null); const input = useRef(null); const commandInput = useSignal(""); @@ -83,6 +84,23 @@ export const KMenu = ( }, context); } + const container = containerRef.current; + if (container) { + const selectedItem = container.children[activeIndex.value]; + if (selectedItem) { + const itemPosition = selectedItem.getBoundingClientRect(); + const containerPosition = container.getBoundingClientRect(); + + // Check if the selected item is above the visible area + if (itemPosition.top < containerPosition.top) { + container.scrollTop -= containerPosition.top - itemPosition.top; + } // Check if the selected item is below the visible area + else if (itemPosition.bottom > containerPosition.bottom) { + container.scrollTop += itemPosition.bottom - containerPosition.bottom; + } + } + } + useEventListener("keydown", (ev: KeyboardEvent) => { if (ev.key === "/" && ev.ctrlKey) { visible.value = !visible.value; @@ -140,7 +158,7 @@ export const KMenu = ( } border-gray-500 `} style={{ gridTemplateColumns: activeState.value !== "loading" - ? "4em 1fr" + ? "auto 1fr" : "1fr", }} > @@ -148,7 +166,7 @@ export const KMenu = ( ( <>
- + {activeMenu.title}
@@ -172,7 +190,11 @@ export const KMenu = ( {activeState.value === "normal" && ( -
+
{entries?.length === 0 && (
No Entries diff --git a/islands/KMenu/commands.ts b/islands/KMenu/commands.ts index dea6634..a0c7dfa 100644 --- a/islands/KMenu/commands.ts +++ b/islands/KMenu/commands.ts @@ -1,56 +1,12 @@ import { Menu } from "@islands/KMenu/types.ts"; -import { Movie } from "@lib/resource/movies.ts"; -import { TMDBMovie } from "@lib/types.ts"; -import { fetchStream, isValidUrl } from "@lib/helpers.ts"; +import { addMovieInfos } from "@islands/KMenu/commands/add_movie_infos.ts"; +import { createNewMovie } from "@islands/KMenu/commands/create_movie.ts"; +import { createNewArticle } from "@islands/KMenu/commands/create_article.ts"; export const menus: Record = { main: { title: "Run", entries: [ - { - title: "Close menu", - meta: "", - cb: (state) => { - state.visible.value = false; - }, - visible: () => false, - }, - { - title: "Create new article", - meta: "", - icon: "IconSquareRoundedPlus", - cb: (state) => { - state.menus["input_link"] = { - title: "Link:", - entries: [], - }; - - state.activeMenu.value = "input_link"; - state.activeState.value = "input"; - - const unsub = state.commandInput.subscribe((value) => { - if (isValidUrl(value)) { - unsub(); - - state.activeState.value = "loading"; - - fetchStream("/api/articles/create?url=" + value, (chunk) => { - console.log({ chunk: chunk.split("\n") }); - if (chunk.startsWith("id:")) { - state.loadingText.value = "Finished"; - setTimeout(() => { - window.location.href = "/articles/" + - chunk.replace("id:", "").trim(); - }, 500); - } else { - state.loadingText.value = chunk; - } - }); - } - }); - }, - visible: () => true, - }, { title: "Clear Cache", icon: "IconRefresh", @@ -63,58 +19,9 @@ export const menus: Record = { state.visible.value = false; }, }, - { - title: "Add Movie infos", - meta: "", - icon: "IconReportSearch", - cb: async (state, context) => { - state.activeState.value = "loading"; - const movie = context as Movie; - - const query = movie.name; - - const response = await fetch( - `/api/tmdb/query?q=${encodeURIComponent(query)}`, - ); - - const json = await response.json() as TMDBMovie[]; - - const menuID = `result/${movie.name}`; - - state.menus[menuID] = { - title: "Select", - entries: json.map((m) => ({ - title: `${m.title} released ${m.release_date}`, - cb: async () => { - state.activeState.value = "loading"; - await fetch(`/api/movies/enhance/${movie.name}/`, { - method: "POST", - body: JSON.stringify({ tmdbId: m.id }), - }); - state.visible.value = false; - state.activeState.value = "normal"; - window.location.reload(); - }, - })), - }; - - state.activeMenu.value = menuID; - - state.activeState.value = "normal"; - }, - visible: () => { - const loc = globalThis["location"]; - return loc?.pathname?.includes("movie"); - }, - }, - { - title: "Reload Page", - meta: "", - cb: () => { - window.location.reload(); - }, - visible: () => false, - }, + createNewArticle, + createNewMovie, + addMovieInfos, ], }, }; diff --git a/islands/KMenu/commands/add_movie_infos.ts b/islands/KMenu/commands/add_movie_infos.ts new file mode 100644 index 0000000..ea56675 --- /dev/null +++ b/islands/KMenu/commands/add_movie_infos.ts @@ -0,0 +1,48 @@ +import { MenuEntry } from "@islands/KMenu/types.ts"; +import { Movie } from "@lib/resource/movies.ts"; +import { TMDBMovie } from "@lib/types.ts"; + +export const addMovieInfos: MenuEntry = { + title: "Add Movie infos", + meta: "", + icon: "IconReportSearch", + cb: async (state, context) => { + state.activeState.value = "loading"; + const movie = context as Movie; + + const query = movie.name; + + const response = await fetch( + `/api/tmdb/query?q=${encodeURIComponent(query)}`, + ); + + const json = await response.json() as TMDBMovie[]; + + const menuID = `result/${movie.name}`; + + state.menus[menuID] = { + title: "Select", + entries: json.map((m) => ({ + title: `${m.title} released ${m.release_date}`, + cb: async () => { + state.activeState.value = "loading"; + await fetch(`/api/movies/enhance/${movie.name}/`, { + method: "POST", + body: JSON.stringify({ tmdbId: m.id }), + }); + state.visible.value = false; + state.activeState.value = "normal"; + window.location.reload(); + }, + })), + }; + + state.activeMenu.value = menuID; + + state.activeState.value = "normal"; + }, + visible: () => { + const loc = globalThis["location"]; + return loc?.pathname?.includes("movie"); + }, +}; diff --git a/islands/KMenu/commands/create_article.ts b/islands/KMenu/commands/create_article.ts new file mode 100644 index 0000000..5ddfba1 --- /dev/null +++ b/islands/KMenu/commands/create_article.ts @@ -0,0 +1,39 @@ +import { MenuEntry } from "@islands/KMenu/types.ts"; +import { fetchStream, isValidUrl } from "@lib/helpers.ts"; + +export const createNewArticle: MenuEntry = { + title: "Create new article", + meta: "", + icon: "IconSquareRoundedPlus", + cb: (state) => { + state.menus["input_link"] = { + title: "Link:", + entries: [], + }; + + state.activeMenu.value = "input_link"; + state.activeState.value = "input"; + + const unsub = state.commandInput.subscribe((value) => { + if (isValidUrl(value)) { + unsub(); + + state.activeState.value = "loading"; + + fetchStream("/api/articles/create?url=" + value, (chunk) => { + console.log({ chunk: chunk.split("\n") }); + if (chunk.startsWith("id:")) { + state.loadingText.value = "Finished"; + setTimeout(() => { + window.location.href = "/articles/" + + chunk.replace("id:", "").trim(); + }, 500); + } else { + state.loadingText.value = chunk; + } + }); + } + }); + }, + visible: () => true, +}; diff --git a/islands/KMenu/commands/create_movie.ts b/islands/KMenu/commands/create_movie.ts new file mode 100644 index 0000000..2462803 --- /dev/null +++ b/islands/KMenu/commands/create_movie.ts @@ -0,0 +1,72 @@ +import { MenuEntry } from "@islands/KMenu/types.ts"; +import { TMDBMovie } from "@lib/types.ts"; +import { debounce } from "@lib/helpers.ts"; +import { Movie } from "@lib/resource/movies.ts"; + +export const createNewMovie: MenuEntry = { + title: "Create new movie", + meta: "", + icon: "IconSquareRoundedPlus", + cb: (state) => { + state.menus["input_link"] = { + title: "Search", + entries: [], + }; + + state.menus["loading"] = { + title: "Search", + entries: [ + { + title: "Loading", + icon: "IconLoader2", + cb() { + }, + }, + ], + }; + + state.activeMenu.value = "input_link"; + state.activeState.value = "normal"; + + let currentQuery: string; + const search = debounce(async function search(query: string) { + currentQuery = query; + console.log({ query }); + if (query.length < 2) { + return; + } + + const response = await fetch("/api/tmdb/query?q=" + query); + + const movies = await response.json() as TMDBMovie[]; + + console.log({ query, currentQuery, movies }); + + if (query !== currentQuery) return; + + state.menus["input_link"] = { + title: "Search", + entries: movies.map((r) => { + return { + title: `${r.title} - ${r.release_date}`, + cb: async () => { + state.activeState.value = "loading"; + const response = await fetch("/api/movies/" + r.id, { + method: "POST", + }); + const movie = await response.json() as Movie; + window.location.href = "/movies/" + movie.name; + }, + }; + }), + }; + state.activeMenu.value = "input_link"; + }, 500); + + const unsub = state.commandInput.subscribe((value) => { + state.activeMenu.value = "loading"; + search(value); + }); + }, + visible: () => true, +}; diff --git a/lib/cache/cache.ts b/lib/cache/cache.ts index 74b2399..fb6ec21 100644 --- a/lib/cache/cache.ts +++ b/lib/cache/cache.ts @@ -1,4 +1,5 @@ import { + Bulk, connect, Redis, RedisConnectOptions, @@ -14,16 +15,31 @@ async function createCache(): Promise | Redis> { const conf: RedisConnectOptions = { hostname: REDIS_HOST, port: REDIS_PORT || 6379, + maxRetryCount: 2, }; if (REDIS_PASS) { conf.password = REDIS_PASS; } - const client = await connect(conf); - console.log("[redis] connected"); - return client; + try { + const client = await connect(conf); + console.log("[redis] connected"); + return client; + } catch (_err) { + console.log("[cache] cant connect to redis, falling back to mock"); + } } - return new Map(); + const mockRedis = new Map(); + + return { + async set(key: string, value: RedisValue) { + mockRedis.set(key, value); + return value.toString(); + }, + async get(key: string) { + return mockRedis.get(key) as Bulk; + }, + }; } const cache = await createCache(); diff --git a/lib/helpers.ts b/lib/helpers.ts index 65ea7fa..35f8cad 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -76,3 +76,15 @@ export const createStreamResponse = () => { enqueue, }; }; + +export function debounce) => void>( + this: ThisParameterType, + fn: T, + delay = 300, +) { + let timer: ReturnType | undefined; + return (...args: Parameters) => { + clearTimeout(timer); + timer = setTimeout(() => fn.apply(this, args), delay); + }; +} diff --git a/lib/resource/movies.ts b/lib/resource/movies.ts index 09e5e52..a85e580 100644 --- a/lib/resource/movies.ts +++ b/lib/resource/movies.ts @@ -1,7 +1,8 @@ import { parseDocument, renderMarkdown } from "@lib/documents.ts"; -import { parse } from "yaml"; +import { parse, stringify } from "yaml"; import { createCrud } from "@lib/crud.ts"; -import { extractHashTags } from "@lib/string.ts"; +import { extractHashTags, formatDate } from "@lib/string.ts"; +import { fixRenderedMarkdown } from "@lib/helpers.ts"; export type Movie = { id: string; @@ -18,6 +19,27 @@ export type Movie = { }; }; +function renderMovie(movie: Movie) { + const meta = movie.meta; + if ("date" in meta) { + meta.date = formatDate(meta.date); + } + + return fixRenderedMarkdown(`${ + meta + ? `--- +${stringify(meta)} +---` + : `--- +---` + } +# ${movie.name} +${movie.meta.image ? `![](${movie.meta.image})` : ""} +${movie.tags.map((t) => `#${t}`).join(" ")} +${movie.description} +`); +} + export function parseMovie(original: string, id: string): Movie { const doc = parseDocument(original); @@ -80,3 +102,8 @@ const crud = createCrud({ export const getMovie = crud.read; export const getAllMovies = crud.readAll; +export const createMovie = (movie: Movie) => { + console.log("creating movie", { movie }); + const content = renderMovie(movie); + return crud.create(movie.id, content); +}; diff --git a/routes/api/movies/[name].ts b/routes/api/movies/[name].ts index 00456ad..7e65c90 100644 --- a/routes/api/movies/[name].ts +++ b/routes/api/movies/[name].ts @@ -1,10 +1,62 @@ import { Handlers } from "$fresh/server.ts"; -import { getMovie } from "@lib/resource/movies.ts"; +import { createMovie, getMovie, Movie } from "@lib/resource/movies.ts"; import { json } from "@lib/helpers.ts"; +import * as tmdb from "@lib/tmdb.ts"; +import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; +import { safeFileName } from "@lib/string.ts"; +import { createDocument } from "@lib/documents.ts"; export const handler: Handlers = { async GET(_, ctx) { const movie = await getMovie(ctx.params.name); return json(movie); }, + async POST(_, ctx) { + const tmdbId = parseInt(ctx.params.name); + + const movieDetails = await tmdb.getMovie(tmdbId); + const movieCredits = await tmdb.getMovieCredits(tmdbId); + + const releaseDate = movieDetails.release_date; + const posterPath = movieDetails.poster_path; + const director = + movieCredits?.crew?.filter?.((person) => person.job === "Director")[0]; + + let finalPath = ""; + const name = movieDetails.title || movieDetails.original_title || + ctx.params.name; + if (posterPath) { + const poster = await tmdb.getMoviePoster(posterPath); + const extension = fileExtension(posterPath); + + finalPath = `Media/movies/images/${ + safeFileName(name) + }_cover.${extension}`; + await createDocument(finalPath, poster); + } + + const metadata = {} as Movie["meta"]; + if (releaseDate) { + metadata.date = new Date(releaseDate); + } + if (finalPath) { + metadata.image = finalPath; + } + if (director) { + metadata.author = director.name; + } + + const movie: Movie = { + id: name, + name: name, + type: "movie", + description: "", + tags: [], + meta: metadata, + }; + + await createMovie(movie); + + return json(movie); + }, }; diff --git a/routes/movies/[name].tsx b/routes/movies/[name].tsx index 5492857..7a055a3 100644 --- a/routes/movies/[name].tsx +++ b/routes/movies/[name].tsx @@ -17,7 +17,7 @@ export default function Greet(props: PageProps) { const { author = "", date = "" } = movie.meta; - console.log(movie.description) + console.log(movie.description); return ( @@ -35,7 +35,9 @@ export default function Greet(props: PageProps) { )}
- {movie?.description?.length > 80 ?

Review

:<>} + {movie?.description?.length > 80 + ?

Review

+ : <>}