diff --git a/components/Card.tsx b/components/Card.tsx index c99f063..663f9a2 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -11,7 +11,7 @@ export function Card( }} class="text-white rounded-3xl shadow-md p-4 relative overflow-hidden lg:w-56 lg:h-56 - sm:w-40 sm:h-40 + sm:w-48 sm:h-48 w-32 h-32" >
diff --git a/components/Grid.tsx b/components/Grid.tsx new file mode 100644 index 0000000..702d0c5 --- /dev/null +++ b/components/Grid.tsx @@ -0,0 +1,12 @@ +import { ComponentChildren } from "preact"; + +export const Grid = ({ children }: { children: ComponentChildren }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/components/HashTags.tsx b/components/HashTags.tsx new file mode 100644 index 0000000..acf0438 --- /dev/null +++ b/components/HashTags.tsx @@ -0,0 +1,15 @@ +export const HashTags = ({ tags }: { tags: string[] }) => { + return ( +
+ {tags.map((t) => { + return ( + + #{t} + + ); + })} +
+ ); +}; diff --git a/components/RecipeHero.tsx b/components/RecipeHero.tsx index 6ab92e0..1497ebd 100644 --- a/components/RecipeHero.tsx +++ b/components/RecipeHero.tsx @@ -1,9 +1,13 @@ import IconExternalLink from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx"; import { Star } from "@components/Stars.tsx"; +import IconArrowNarrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-narrow-left.tsx"; +import IconEdit from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/edit.tsx"; + export function RecipeHero( - { data, subline, backlink }: { + { data, subline, backlink, editLink }: { backlink: string; subline?: string[]; + editLink?: string; data: { meta?: { image?: string; link?: string }; name: string }; }, ) { @@ -31,15 +35,26 @@ export function RecipeHero(
- Back + Back + {editLink && + ( + + + + )}
{subline.filter((s) => s && s?.length > 1).map((s) => { diff --git a/components/Youtube.tsx b/components/Youtube.tsx new file mode 100644 index 0000000..e70748a --- /dev/null +++ b/components/Youtube.tsx @@ -0,0 +1,37 @@ +export const isYoutubeLink = (link: string) => { + try { + const url = new URL(link); + return ["youtu.be", "youtube.com"].includes(url.hostname); + } catch (err) { + console.log(err); + return false; + } +}; + +function extractYoutubeId(link: string) { + const url = new URL(link); + if (url.searchParams.has("v")) { + const id = url.searchParams.get("v"); + + if (id?.length && id.length > 4) { + return id; + } + } + + return url.pathname.replace(/^\//, ""); +} + +export const YoutubePlayer = ({ link }: { link: string }) => { + const id = extractYoutubeId(link); + return ( + + ); +}; diff --git a/components/layouts/main.tsx b/components/layouts/main.tsx index 87925c0..70171e6 100644 --- a/components/layouts/main.tsx +++ b/components/layouts/main.tsx @@ -1,4 +1,5 @@ import { ComponentChildren } from "preact"; +import { menu } from "@lib/menus.ts"; export type Props = { children: ComponentChildren; @@ -9,25 +10,6 @@ export type Props = { }; export const MainLayout = ({ children, url }: Props) => { - const menu = [ - { - name: "🏡 Home", - link: "/", - }, - { - name: "🍽️ Recipes", - link: "/recipes", - }, - { - name: "🍿 Movies", - link: "/movies", - }, - { - name: "📝 Articles", - link: "/articles", - }, - ]; - return (
{ const activeMenuType = useSignal(type); const activeMenu = menus[activeMenuType.value || "main"]; - const activeState = useSignal<"normal" | "loading" | "error">("normal"); + const activeState = useSignal<"normal" | "loading" | "error" | "input">( + "normal", + ); const activeIndex = useSignal(-1); const input = useRef(null); @@ -127,14 +129,14 @@ export const KMenu = ( style={{ background: "#1f1f1f88" }} >
- {activeState.value === "normal" && + {(activeState.value === "normal" || activeState.value === "input") && ( <>
@@ -161,6 +163,11 @@ export const KMenu = ( {activeState.value === "normal" && (
+ {entries?.length === 0 && ( +
+ No Entries +
+ )} {entries.map( (k, index) => { return ( diff --git a/islands/KMenu/commands.ts b/islands/KMenu/commands.ts index defea24..a8e45e9 100644 --- a/islands/KMenu/commands.ts +++ b/islands/KMenu/commands.ts @@ -23,7 +23,9 @@ export const menus: Record = { title: "Link:", entries: [], }; + state.activeMenu.value = "input_link"; + state.activeState.value = "input"; const unsub = state.commandInput.subscribe(async (value) => { if (isValidUrl(value)) { diff --git a/islands/KMenu/types.ts b/islands/KMenu/types.ts index 348fff7..3a75408 100644 --- a/islands/KMenu/types.ts +++ b/islands/KMenu/types.ts @@ -2,7 +2,7 @@ import { Signal } from "@preact/signals"; export type MenuState = { activeMenu: Signal; - activeState: Signal<"error" | "normal" | "loading">; + activeState: Signal<"input" | "error" | "normal" | "loading">; commandInput: Signal; visible: Signal; menus: Record; diff --git a/lib/cache/cache.ts b/lib/cache/cache.ts index c7454ee..74b2399 100644 --- a/lib/cache/cache.ts +++ b/lib/cache/cache.ts @@ -54,7 +54,19 @@ export function expire(id: string, seconds: number) { } } -export async function set(id: string, content: T) { +type RedisOptions = { + expires?: number; +}; + +export async function set( + id: string, + content: T, + options?: RedisOptions, +) { console.log("[cache] storing ", { id }); - return await cache.set(id, content); + const res = await cache.set(id, content); + if (options?.expires) { + await expire(id, options.expires); + } + return res; } diff --git a/lib/cache/documents.ts b/lib/cache/documents.ts index 8c4754c..efa9e27 100644 --- a/lib/cache/documents.ts +++ b/lib/cache/documents.ts @@ -1,57 +1,31 @@ import { Document } from "@lib/documents.ts"; import * as cache from "@lib/cache/cache.ts"; -type DocumentsCache = { - lastUpdated: number; - documents: Document[]; -}; - -const CACHE_INTERVAL = 5000; // 5 seconds; +const CACHE_INTERVAL = 20; // 5 seconds; const CACHE_KEY = "documents"; export async function getDocuments() { - const docs = await cache.get(CACHE_KEY); - if (!docs) return; - - if (Date.now() > docs.lastUpdated + CACHE_INTERVAL) { - return; - } - - return docs.documents; + const res = await cache.get(CACHE_KEY); + if (res) return JSON.parse(res); + return; } export function setDocuments(documents: Document[]) { return cache.set( CACHE_KEY, - JSON.stringify({ - lastUpdated: Date.now(), - documents, - }), + JSON.stringify(documents), + { expires: CACHE_INTERVAL }, ); } -type DocumentCache = { - lastUpdated: number; - content: string; -}; - -export async function getDocument(id: string) { - const doc = await cache.get(CACHE_KEY + "/" + id); - if (!doc) return; - - if (Date.now() > doc.lastUpdated + CACHE_INTERVAL) { - return; - } - - return doc.content; +export function getDocument(id: string) { + return cache.get(CACHE_KEY + "/" + id); } export async function setDocument(id: string, content: string) { await cache.set( CACHE_KEY + "/" + id, - JSON.stringify({ - lastUpdated: Date.now(), - content, - }), + content, + { expires: CACHE_INTERVAL }, ); } diff --git a/lib/cache/image.ts b/lib/cache/image.ts index a695bf1..3925f8b 100644 --- a/lib/cache/image.ts +++ b/lib/cache/image.ts @@ -12,7 +12,6 @@ const CACHE_KEY = "images"; function getCacheKey({ url: _url, width, height }: ImageCacheOptions) { const url = new URL(_url); - return `${CACHE_KEY}/${url.hostname}/${url.pathname}/${width}/${height}` .replace( "//", diff --git a/lib/documents.ts b/lib/documents.ts index ef39d4c..66f2860 100644 --- a/lib/documents.ts +++ b/lib/documents.ts @@ -25,7 +25,7 @@ export async function getDocuments(): Promise { const headers = new Headers(); headers.append("Accept", "application/json"); - + console.log("[documents] fetching all documents"); const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, { headers: headers, }); @@ -47,6 +47,8 @@ export function createDocument( headers.append("Content-Type", mediaType); } + console.log("[documents] creating document", { name }); + return fetch(SILVERBULLET_SERVER + "/" + name, { body: content, method: "PUT", @@ -58,6 +60,7 @@ export async function getDocument(name: string): Promise { const cachedDocument = await cache.getDocument(name); if (cachedDocument) return cachedDocument; + console.log("[documents] fetching document", { name }); const response = await fetch(SILVERBULLET_SERVER + "/" + name); const text = await response.text(); diff --git a/lib/menus.ts b/lib/menus.ts new file mode 100644 index 0000000..83db52b --- /dev/null +++ b/lib/menus.ts @@ -0,0 +1,18 @@ +export const menu = [ + { + name: "🏡 Home", + link: "/", + }, + { + name: "🍽️ Recipes", + link: "/recipes", + }, + { + name: "🍿 Movies", + link: "/movies", + }, + { + name: "📝 Articles", + link: "/articles", + }, +]; diff --git a/lib/resource/articles.ts b/lib/resource/articles.ts index 2dcf7c0..b092964 100644 --- a/lib/resource/articles.ts +++ b/lib/resource/articles.ts @@ -2,7 +2,7 @@ import { parseDocument, renderMarkdown } from "@lib/documents.ts"; import { parse } from "yaml"; import { createCrud } from "@lib/crud.ts"; import { stringify } from "https://deno.land/std@0.194.0/yaml/stringify.ts"; -import { formatDate } from "@lib/string.ts"; +import { extractHashTags, formatDate } from "@lib/string.ts"; import { fixRenderedMarkdown } from "@lib/helpers.ts"; export type Article = { @@ -80,10 +80,9 @@ function parseArticle(original: string, id: string): Article { } let content = original.slice(range[0], range[1]); - const tags = []; - for (const [hashtag] of original.matchAll(/\B(\#[a-zA-Z\-]+\b)(?!;)/g)) { - tags.push(hashtag.replace(/\#/g, "")); - content = content.replace(hashtag, ""); + const tags = extractHashTags(content); + for (const tag of tags) { + content = content.replace("#" + tag, ""); } return { diff --git a/lib/resource/movies.ts b/lib/resource/movies.ts index e4006b7..21f9abd 100644 --- a/lib/resource/movies.ts +++ b/lib/resource/movies.ts @@ -1,12 +1,13 @@ import { parseDocument, renderMarkdown } from "@lib/documents.ts"; import { parse } from "yaml"; import { createCrud } from "@lib/crud.ts"; +import { extractHashTags } from "@lib/string.ts"; export type Movie = { id: string; name: string; description: string; - hashtags: string[]; + tags: string[]; meta: { date: Date; image: string; @@ -52,16 +53,15 @@ export function parseMovie(original: string, id: string): Movie { } let description = original.slice(range[0], range[1]); - const hashtags = []; - for (const [hashtag] of original.matchAll(/\B(\#[a-zA-Z]+\b)(?!;)/g)) { - hashtags.push(hashtag.replace(/\#/g, "")); - description = description.replace(hashtag, ""); + const tags = extractHashTags(description); + for (const tag of tags) { + description = description.replace("#" + tag, ""); } return { id, name, - hashtags, + tags, description: renderMarkdown(description), meta, }; diff --git a/lib/string.ts b/lib/string.ts index 78a69cc..3b2a1e2 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -15,3 +15,15 @@ export function safeFileName(inputString: string): string { return fileName; } + +export function extractHashTags(inputString: string) { + const hashtags = []; + + for ( + const [hashtag] of inputString.matchAll(/(? = { async GET(_, ctx) { @@ -23,27 +25,26 @@ export default function Greet(props: PageProps
) { - {article.tags.length && - ( -
- {article.tags.map((t) => { - return ( - - #{t} - - ); - })} -
- )} + {article.tags.length > 0 && ( + <> +
+ + + )} +
+ {isYoutubeLink(article.meta.link) && ( + + )}
-          {article.content}
+          {article.content||""}
         
diff --git a/routes/articles/index.tsx b/routes/articles/index.tsx index d151384..b26cab8 100644 --- a/routes/articles/index.tsx +++ b/routes/articles/index.tsx @@ -4,6 +4,7 @@ import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow- import { Article, getAllArticles } from "@lib/resource/articles.ts"; import { Card } from "@components/Card.tsx"; import { KMenu } from "@islands/KMenu.tsx"; +import { Grid } from "@components/Grid.tsx"; export const handler: Handlers = { async GET(_, ctx) { @@ -28,11 +29,11 @@ export default function Greet(props: PageProps) { -
+ {props.data?.map((doc) => { return ; })} -
+ ); } diff --git a/routes/index.tsx b/routes/index.tsx index 905556c..e56ab56 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -2,6 +2,7 @@ import { Head } from "$fresh/runtime.ts"; import { MainLayout } from "@components/layouts/main.tsx"; import { Card } from "@components/Card.tsx"; import { PageProps } from "$fresh/server.ts"; +import { menu } from "@lib/menus.ts"; export default function Home(props: PageProps) { return ( @@ -10,17 +11,16 @@ export default function Home(props: PageProps) { app -
- - +
+ {menu.map((m) => { + return ( + + ); + })}
diff --git a/routes/movies/[name].tsx b/routes/movies/[name].tsx index 0e144f6..d4c94cf 100644 --- a/routes/movies/[name].tsx +++ b/routes/movies/[name].tsx @@ -3,6 +3,7 @@ import { MainLayout } from "@components/layouts/main.tsx"; import { getMovie, Movie } from "@lib/resource/movies.ts"; import { RecipeHero } from "@components/RecipeHero.tsx"; import { KMenu } from "@islands/KMenu.tsx"; +import { HashTags } from "@components/HashTags.tsx"; export const handler: Handlers = { async GET(_, ctx) { @@ -24,6 +25,12 @@ export default function Greet(props: PageProps) { backlink="/movies" /> + {movie.tags.length > 0 && ( + <> +
+ + + )}
 = {
   async GET(_, ctx) {
@@ -25,11 +26,11 @@ export default function Greet(props: PageProps) {
 
         

🍿 Movies

-
+ {props.data?.map((doc) => { return ; })} -
+ ); } diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index f5651e4..be1b8f3 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -3,6 +3,7 @@ import { RecipeCard } from "@components/RecipeCard.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx"; +import { Grid } from "@components/Grid.tsx"; export const handler: Handlers = { async GET(_, ctx) { @@ -25,11 +26,11 @@ export default function Greet(props: PageProps) {

🍽️ Recipes

-
+ {props.data?.map((doc) => { return ; })} -
+ ); }