feat: update some shit

This commit is contained in:
max_richter 2023-10-16 01:40:10 +02:00
parent 799a736f36
commit 03d17569da
10 changed files with 336 additions and 191 deletions

154
components/PageHero.tsx Normal file
View File

@ -0,0 +1,154 @@
import { withSubComponents } from "@components/helpers/withSubComponents.ts";
import Image from "@components/Image.tsx";
import { IconExternalLink } from "@components/icons.tsx";
import { type ComponentChildren, createContext } from "preact";
import { IconArrowNarrowLeft } from "@components/icons.tsx";
import { IconEdit } from "@components/icons.tsx";
import { useContext } from "preact/hooks";
import { GenericResource } from "@lib/types.ts";
const HeroContext = createContext<{ image?: string; thumbnail?: string }>({
image: undefined,
thumbnail: undefined,
});
function Wrapper(
{ children, image, thumbnail }: {
children: ComponentChildren;
image?: string;
thumbnail?: string;
},
) {
return (
<div
class={`flex justify-between flex-col relative w-full min-h-[${
image ? 400 : 200
}px] rounded-3xl overflow-hidden`}
>
<HeroContext.Provider value={{ image }}>
{image &&
(
<Image
fill
src={image}
thumbnail={thumbnail}
alt="Recipe Banner"
// style={{ objectPosition: "0% 25%" }}
class="absolute object-cover w-full h-full -z-10"
/>
)}
{children}
</HeroContext.Provider>
</div>
);
}
function Title(
{ children, link }: { children: ComponentChildren; link?: string },
) {
const ctx = useContext(HeroContext);
return (
<div
class={`${
ctx.image ? "noisy-gradient" : ""
} after:opacity-90 flex gap-4 items-center ${ctx.image ? "pt-12" : ""}`}
>
<h2
class="relative text-4xl font-bold z-10"
style={{ color: ctx.image ? "#1F1F1F" : "white" }}
>
{children}
{link &&
(
<a
href={link}
target="__blank"
class="p-2 inline-flex"
>
<IconExternalLink />
</a>
)}
</h2>
</div>
);
}
function BackLink({ href }: { href: string }) {
return (
<a
class="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex gap-1 items-center"
href={href}
>
<IconArrowNarrowLeft class="w-5 h-5" /> Back
</a>
);
}
function EditLink({ href }: { href: string }) {
const ctx = useContext(HeroContext);
return (
<a
class={`px-4 py-2 ${
ctx.image ? "bg-gray-300 text-gray-800" : "text-gray-200"
} rounded-lg flex gap-1 items-center`}
href={href}
>
<IconEdit class="w-5 h-5" />
</a>
);
}
function Header({ children }: { children: ComponentChildren }) {
return (
<div class="flex justify-between mx-8 mt-8">
{children}
</div>
);
}
function Subline(
{ entries, children }: {
children?: ComponentChildren;
entries: (string | { href: string; title: string })[];
},
) {
const ctx = useContext(HeroContext);
return (
<div
class={`relative flex items-center z-50 flex gap-5 font-sm text-light mt-3`}
style={{ color: ctx.image ? "#1F1F1F" : "white" }}
>
{children}
{entries.filter((s) =>
s && (typeof s === "string" ? s?.length > 1 : true)
).map((s) => {
if (typeof s === "string") {
return <span>{s}</span>;
} else {
return <a href={s.href}>{s.title}</a>;
}
})}
</div>
);
}
function Footer({ children }: { children: ComponentChildren }) {
const ctx = useContext(HeroContext);
return (
<div
class={`relative inset-x-0 py-4 px-8 ${ctx.image ? "py-8" : ""} `}
>
{children}
</div>
);
}
export default withSubComponents(Wrapper, {
EditLink,
BackLink,
Footer,
Subline,
Header,
Title,
});

View File

@ -1,106 +0,0 @@
import { Star } from "@components/Stars.tsx";
import {
IconArrowNarrowLeft,
IconEdit,
IconExternalLink,
} from "@components/icons.tsx";
import Image from "@components/Image.tsx";
import { GenericResource } from "@lib/types.ts";
export function RecipeHero(
{ data, subline, backlink, editLink }: {
backlink: string;
subline?: (string | { title: string; href: string })[];
editLink?: string;
data: GenericResource;
},
) {
const { meta: { image } = {} } = data;
return (
<div
class={`flex justify-between flex-col relative w-full min-h-[${
image ? 400 : 200
}px] rounded-3xl overflow-hidden`}
>
{image &&
(
<Image
fill
src={image}
thumbnail={data.meta?.thumbnail}
alt="Recipe Banner"
style={{ objectPosition: "0% 25%" }}
class="absolute object-cover w-full h-full -z-10"
/>
)}
<div class="flex justify-between mx-8 mt-8">
<a
class="px-4 py-2 bg-gray-300 text-gray-800 rounded-lg flex gap-1 items-center"
href={backlink}
>
<IconArrowNarrowLeft class="w-5 h-5" /> Back
</a>
{editLink &&
(
<a
class={`px-4 py-2 ${
image ? "bg-gray-300 text-gray-800" : "text-gray-200"
} rounded-lg flex gap-1 items-center`}
href={editLink}
>
<IconEdit class="w-5 h-5" />
</a>
)}
</div>
<div
class={`relative inset-x-0 py-4 px-8 ${image ? "py-8" : ""} `}
>
<div
class={`${
image ? "noisy-gradient" : ""
} after:opacity-90 flex gap-4 items-center ${image ? "pt-12" : ""}`}
>
<h2
class="relative text-4xl font-bold z-10"
style={{ color: image ? "#1F1F1F" : "white" }}
>
{data.name}
{data.meta?.link &&
(
<a
href={data.meta.link}
target="__blank"
class="p-2 inline-flex"
name="Link to Original recipe"
>
<IconExternalLink />
</a>
)}
</h2>
{data.meta?.rating && <Star rating={data.meta.rating} />}
</div>
{subline?.length &&
(
<div
class={`relative z-50 flex gap-5 font-sm text-light mt-3`}
style={{ color: image ? "#1F1F1F" : "white" }}
>
{subline.filter((s) =>
s && (typeof s === "string" ? s?.length > 1 : true)
).map((s) => {
if (typeof s === "string") {
return <span>{s}</span>;
} else {
return <a href={s.href}>{s.title}</a>;
}
})}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,11 @@
// deno-lint-ignore no-explicit-any
export function withSubComponents<A, B extends Record<string, any>>(
component: A,
subcomponents: B,
): A & B {
Object.keys(subcomponents).forEach((key) => {
// deno-lint-ignore no-explicit-any
(component as any)[key] = (subcomponents as any)[key];
});
return component as A & B;
}

View File

@ -16,3 +16,4 @@ export { default as IconLogout } from "https://deno.land/x/tabler_icons_tsx@0.0.
export { default as IconSearch } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/search.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 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 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";

View File

@ -1,9 +1,16 @@
import { useCallback, useState } from "preact/hooks"; import { useCallback, useState } from "preact/hooks";
import { IconWand } from "@components/icons.tsx";
export function Recommendations({ id, type }: { id: string; type: string }) { type RecommendationState = "disabled" | "loading";
const [state, setState] = useState<"disabled" | "loading">(
"disabled", export function Recommendations(
); { id, type, load = false }: {
id: string;
type: string;
load?: boolean;
},
) {
const [state, setState] = useState<RecommendationState>("disabled");
const [results, setResults] = useState(); const [results, setResults] = useState();
const startFetch = useCallback( const startFetch = useCallback(
@ -17,27 +24,54 @@ export function Recommendations({ id, type }: { id: string; type: string }) {
[id, type], [id, type],
); );
if (results) { if (load) {
return ( startFetch();
<ul class="gap-5">
{results.map((res) => {
return (
<div class="flex gap-5 items-center mb-4">
<img
class="w-12 h-12 rounded-full object-cover"
src={`https://image.tmdb.org/t/p/original${res.poster_path}`}
/>
<p>{res.title}</p>
</div>
);
})}
</ul>
);
} }
if (state === "loading") { return (
return <p>Loading...</p>; <span>
} {results && (
<div
style={{ boxShadow: "0px 49px 33px #1412183b inset" }}
class="relative w-full bg-white -mt-10 pt-10 -z-50 rounded-2xl p-5"
>
<h3 class="text-2xl my-5 flex items-center gap-2">
Similar Movies
</h3>
<ul class="gap-5">
{results.map((res) => {
return (
<div class="flex gap-5 items-center mb-4">
<img
class="w-12 h-12 rounded-full object-cover"
src={`https://image.tmdb.org/t/p/original${res.poster_path}`}
/>
<p>{res.title}</p>
</div>
);
})}
</ul>
</div>
)}
return <button onClick={startFetch}>load recommendations</button>; {state === "loading" && !results && (
<div
style={{ boxShadow: "0px 49px 33px #1412183b inset" }}
class="relative w-full bg-white -mt-10 pt-10 -z-50 rounded-2xl p-5"
>
<p class="mt-5">Loading...</p>
</div>
)}
{!results && state === "disabled" &&
(
<button
onClick={startFetch}
class="ml-8 mt-2 font-thin flex items-center gap-2 text-white my-2"
>
<IconWand class="w-4" /> Similar
</button>
)}
</span>
);
} }

View File

@ -4,7 +4,7 @@ import { CSS, KATEX_CSS } from "https://deno.land/x/gfm@0.2.5/mod.ts";
import { Head, Partial } from "$fresh/runtime.ts"; import { Head, Partial } from "$fresh/runtime.ts";
import { Emoji } from "@components/Emoji.tsx"; import { Emoji } from "@components/Emoji.tsx";
export default function MyLayout({ Component, url }: LayoutProps) { export default function MyLayout({ Component }: LayoutProps) {
return ( return (
<div <div
class="md:grid mx-auto" class="md:grid mx-auto"
@ -20,7 +20,7 @@ export default function MyLayout({ Component, url }: LayoutProps) {
return ( return (
<a <a
href={m.link} href={m.link}
class={`flex items-center gap-2 text-white [data-ancestor]:bg-white [data-ancestor]: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} {<Emoji class="w-6 h-6" name={m.emoji} />} {m.name}
</a> </a>

View File

@ -1,15 +1,16 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx"; import { MainLayout } from "@components/layouts/main.tsx";
import { Article, getArticle } from "@lib/resource/articles.ts"; import { Article, getArticle } from "@lib/resource/articles.ts";
import { RecipeHero } from "@components/RecipeHero.tsx";
import { KMenu } from "@islands/KMenu.tsx"; import { KMenu } from "@islands/KMenu.tsx";
import { YoutubePlayer } from "@components/Youtube.tsx"; import { YoutubePlayer } from "@components/Youtube.tsx";
import { HashTags } from "@components/HashTags.tsx"; import { HashTags } from "@components/HashTags.tsx";
import { isYoutubeLink } from "@lib/string.ts"; import { isYoutubeLink } from "@lib/string.ts";
import { renderMarkdown } from "@lib/documents.ts"; import { renderMarkdown } from "@lib/documents.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
export const handler: Handlers<Article | null> = { export const handler: Handlers<{ article: Article; session: unknown }> = {
async GET(_, ctx) { async GET(_, ctx) {
const article = await getArticle(ctx.params.name); const article = await getArticle(ctx.params.name);
return ctx.render({ article, session: ctx.state.session }); return ctx.render({ article, session: ctx.state.session });
@ -33,20 +34,33 @@ export default function Greet(
> >
<RedirectSearchHandler /> <RedirectSearchHandler />
<KMenu type="main" context={{ type: "article" }} /> <KMenu type="main" context={{ type: "article" }} />
<RecipeHero
data={article} <PageHero image={article.meta.image} thumbnail={article.meta.thumbnail}>
subline={[ <PageHero.Header>
author && { <PageHero.BackLink href="/articles" />
title: author, {session && (
href: `/?q=${encodeURIComponent(author)}`, <PageHero.EditLink
}, href={`https://notes.max-richter.dev/Media/articles/${article.id}`}
date.toString(), />
]} )}
editLink={session </PageHero.Header>
? `https://notes.max-richter.dev/Media/articles/${article.id}` <PageHero.Footer>
: ""} <PageHero.Title link={article.meta.link}>
backlink="/articles" {article.name}
/> </PageHero.Title>
<PageHero.Subline
entries={[
author && {
title: author,
href: `/?q=${encodeURIComponent(author)}`,
},
date.toString(),
]}
>
{article.meta.rating && <Star rating={article.meta.rating} />}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
{article.tags.length > 0 && ( {article.tags.length > 0 && (
<> <>
<br /> <br />

View File

@ -1,12 +1,13 @@
import { Handlers, PageProps, RouteContext } from "$fresh/server.ts"; import { 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 { HashTags } from "@components/HashTags.tsx"; import { HashTags } from "@components/HashTags.tsx";
import { renderMarkdown } from "@lib/documents.ts"; 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";
import { Recommendations } from "@islands/Recommendations.tsx"; import { Recommendations } from "@islands/Recommendations.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
export default async function Greet( export default async function Greet(
props: PageProps<{ movie: Movie; session: Record<string, string> }>, props: PageProps<{ movie: Movie; session: Record<string, string> }>,
@ -23,19 +24,33 @@ export default async function Greet(
<MainLayout url={props.url} title={`Movie > ${movie.name}`} context={movie}> <MainLayout url={props.url} title={`Movie > ${movie.name}`} context={movie}>
<RedirectSearchHandler /> <RedirectSearchHandler />
<KMenu type="main" context={movie} /> <KMenu type="main" context={movie} />
<RecipeHero <PageHero image={movie.meta.image} thumbnail={movie.meta.thumbnail}>
data={movie} <PageHero.Header>
subline={[ <PageHero.BackLink href="/movies" />
author && { {session && (
title: author, <PageHero.EditLink
href: `/?q=${encodeURIComponent(author)}`, href={`https://notes.max-richter.dev/Media/movies/${movie.id}`}
}, />
date.toString(), )}
]} </PageHero.Header>
editLink={session <PageHero.Footer>
? `https://notes.max-richter.dev/Media/movies/${movie.id}` <PageHero.Title>{movie.name}</PageHero.Title>
: ""} <PageHero.Subline
backlink="/movies" entries={[
author && {
title: author,
href: `/?q=${encodeURIComponent(author)}`,
},
date.toString(),
]}
>
{movie.meta.rating && <Star rating={movie.meta.rating} />}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
<Recommendations
id={movie.id}
type="movie"
/> />
{movie.tags.length > 0 && ( {movie.tags.length > 0 && (
<> <>
@ -53,8 +68,6 @@ export default async function Greet(
> >
{content} {content}
</pre> </pre>
<Recommendations id={movie.id} type="movie"></Recommendations>
</div> </div>
</MainLayout> </MainLayout>
); );

View File

@ -1,14 +1,15 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { IngredientsList } from "@islands/IngredientsList.tsx"; import { IngredientsList } from "@islands/IngredientsList.tsx";
import { RecipeHero } from "@components/RecipeHero.tsx";
import { MainLayout } from "@components/layouts/main.tsx"; import { MainLayout } from "@components/layouts/main.tsx";
import Counter from "@islands/Counter.tsx"; import Counter from "@islands/Counter.tsx";
import { useSignal } from "@preact/signals"; import { useSignal } from "@preact/signals";
import { getRecipe, Recipe } from "@lib/resource/recipes.ts"; import { getRecipe, Recipe } from "@lib/resource/recipes.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx"; import { KMenu } from "@islands/KMenu.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
export const handler: Handlers<Recipe | null> = { export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
async GET(_, ctx) { async GET(_, ctx) {
const recipe = await getRecipe(ctx.params.name); const recipe = await getRecipe(ctx.params.name);
return ctx.render({ recipe, session: ctx.state.session }); return ctx.render({ recipe, session: ctx.state.session });
@ -25,7 +26,7 @@ export default function Greet(
const subline = [ const subline = [
recipe?.meta?.time && `Duration ${recipe.meta.time}`, recipe?.meta?.time && `Duration ${recipe.meta.time}`,
].filter(Boolean); ].filter(Boolean) as string[];
return ( return (
<MainLayout <MainLayout
@ -35,14 +36,25 @@ export default function Greet(
> >
<RedirectSearchHandler /> <RedirectSearchHandler />
<KMenu type="main" context={recipe} /> <KMenu type="main" context={recipe} />
<RecipeHero
data={recipe} <PageHero image={recipe.meta?.image} thumbnail={recipe.meta?.thumbnail}>
backlink="/recipes" <PageHero.Header>
editLink={session <PageHero.BackLink href="/articles" />
? `https://notes.max-richter.dev/Recipes/${recipe.id}` {session && (
: ""} <PageHero.EditLink
subline={subline} href={`https://notes.max-richter.dev/Recipes/${recipe.id}`}
/> />
)}
</PageHero.Header>
<PageHero.Footer>
<PageHero.Title>{recipe.name}</PageHero.Title>
<PageHero.Subline
entries={subline}
>
{recipe.meta?.rating && <Star rating={recipe.meta?.rating} />}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
<div class="px-8 text-white mt-10"> <div class="px-8 text-white mt-10">
<div class="flex items-center gap-8"> <div class="flex items-center gap-8">
<h3 class="text-3xl my-5">Ingredients</h3> <h3 class="text-3xl my-5">Ingredients</h3>

View File

@ -1,11 +1,12 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx"; import { MainLayout } from "@components/layouts/main.tsx";
import { RecipeHero } from "@components/RecipeHero.tsx";
import { HashTags } from "@components/HashTags.tsx"; import { HashTags } from "@components/HashTags.tsx";
import { renderMarkdown } from "@lib/documents.ts"; import { renderMarkdown } from "@lib/documents.ts";
import { getSeries, Series } from "@lib/resource/series.ts"; import { getSeries, Series } from "@lib/resource/series.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx"; import { KMenu } from "@islands/KMenu.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
export const handler: Handlers<{ serie: Series; session: unknown }> = { export const handler: Handlers<{ serie: Series; session: unknown }> = {
async GET(_, ctx) { async GET(_, ctx) {
@ -27,20 +28,31 @@ export default function Greet(
<MainLayout url={props.url} title={`Serie > ${serie.name}`} context={serie}> <MainLayout url={props.url} title={`Serie > ${serie.name}`} context={serie}>
<RedirectSearchHandler /> <RedirectSearchHandler />
<KMenu type="main" context={serie} /> <KMenu type="main" context={serie} />
<RecipeHero
data={serie} <PageHero image={serie.meta.image} thumbnail={serie.meta.thumbnail}>
subline={[ <PageHero.Header>
author && { <PageHero.BackLink href="/series" />
title: author, {session && (
href: `/?q=${encodeURIComponent(author)}`, <PageHero.EditLink
}, href={`https://notes.max-richter.dev/Media/series/${serie.id}`}
date.toString(), />
]} )}
editLink={session </PageHero.Header>
? `https://notes.max-richter.dev/Media/series/${serie.id}` <PageHero.Footer>
: ""} <PageHero.Title>{serie.name}</PageHero.Title>
backlink="/series" <PageHero.Subline
/> entries={[
author && {
title: author,
href: `/?q=${encodeURIComponent(author)}`,
},
date.toString(),
]}
>
{serie.meta.rating && <Star rating={serie.meta.rating} />}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
{serie.tags.length > 0 && ( {serie.tags.length > 0 && (
<> <>
<br /> <br />