refactor: commands from menu

This commit is contained in:
max_richter 2023-08-04 13:48:12 +02:00
parent b95cfcc5b4
commit f9638c35fc
12 changed files with 317 additions and 114 deletions

View File

@ -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",

View File

@ -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,
};

View File

@ -37,6 +37,7 @@ export const KMenu = (
const loadingText = useSignal("");
const activeIndex = useSignal(-1);
const containerRef = useRef<HTMLDivElement>(null);
const input = useRef<HTMLInputElement>(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 = (
(
<>
<div class="grid place-items-center border-r border-gray-500">
<span class="text-white">
<span class="text-white mx-4">
{activeMenu.title}
</span>
</div>
@ -172,7 +190,11 @@ export const KMenu = (
</div>
{activeState.value === "normal" &&
(
<div class="" style={{ maxHeight: "12rem", overflowY: "auto" }}>
<div
class=""
style={{ maxHeight: "12rem", overflowY: "auto" }}
ref={containerRef}
>
{entries?.length === 0 && (
<div class="text-gray-400 px-4 py-2">
No Entries

View File

@ -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<string, Menu> = {
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<string, Menu> = {
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,
],
},
};

View File

@ -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");
},
};

View File

@ -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,
};

View File

@ -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,
};

24
lib/cache/cache.ts vendored
View File

@ -1,4 +1,5 @@
import {
Bulk,
connect,
Redis,
RedisConnectOptions,
@ -14,16 +15,31 @@ async function createCache<T>(): Promise<Map<string, T> | 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<string, T>();
const mockRedis = new Map<string, RedisValue>();
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();

View File

@ -76,3 +76,15 @@ export const createStreamResponse = () => {
enqueue,
};
};
export function debounce<T extends (...args: Parameters<T>) => void>(
this: ThisParameterType<T>,
fn: T,
delay = 300,
) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}

View File

@ -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<Movie>({
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);
};

View File

@ -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);
},
};

View File

@ -17,7 +17,7 @@ export default function Greet(props: PageProps<Movie>) {
const { author = "", date = "" } = movie.meta;
console.log(movie.description)
console.log(movie.description);
return (
<MainLayout url={props.url}>
@ -35,7 +35,9 @@ export default function Greet(props: PageProps<Movie>) {
</>
)}
<div class="px-8 text-white mt-10">
{movie?.description?.length > 80 ? <h2 class="text-4xl font-bold mb-4">Review</h2>:<></>}
{movie?.description?.length > 80
? <h2 class="text-4xl font-bold mb-4">Review</h2>
: <></>}
<pre
class="whitespace-break-spaces"
dangerouslySetInnerHTML={{ __html: movie.description || "" }}