feat: initial refactor to use marka as backend

This commit is contained in:
Max Richter
2025-10-28 20:15:23 +01:00
parent 0beb3b1071
commit f680b5f832
39 changed files with 245 additions and 1012 deletions

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { getArticle } from "@lib/resource/articles.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET(_, ctx) {
const article = await getArticle(ctx.params.name);
const article = await fetchResource(`articles/${ctx.params.name}`);
return json(article);
},
};

View File

@@ -6,7 +6,7 @@ import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts";
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
import { Article, createArticle } from "@lib/resource/articles.ts";
import { Article } from "@lib/resource/articles.ts";
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
import {
extractYoutubeId,
@@ -170,7 +170,7 @@ async function processCreateArticle(
streamResponse.enqueue("writing to disk");
await createArticle(newArticle.id, newArticle);
// await createArticle(newArticle.id, newArticle);
streamResponse.enqueue("id: " + newArticle.id);
}
@@ -210,7 +210,7 @@ async function processCreateYoutubeVideo(
streamResponse.enqueue("creating article");
await createArticle(newArticle.id, newArticle);
// await createArticle(newArticle.id, newArticle);
streamResponse.enqueue("finished");

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { getAllArticles } from "@lib/resource/articles.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET() {
const movies = await getAllArticles();
return json(movies);
const articles = await fetchResource("articles");
return json(articles?.content);
},
};

View File

@@ -1,8 +1,6 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { getImageContent } from "@lib/image.ts";
import { SILVERBULLET_SERVER } from "@lib/env.ts";
import { createLogger } from "@lib/log/index.ts";
import { isLocalImage } from "@lib/string.ts";
const log = createLogger("api/image");
@@ -64,11 +62,10 @@ function parseParams(reqUrl: URL): ImageParams | string {
// Helper function to generate ETag
async function generateETag(content: ArrayBuffer): Promise<string> {
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
return `"${
Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}"`;
return `"${Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}"`;
}
async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
@@ -83,8 +80,8 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
});
}
const imageUrl = isLocalImage(params.image)
? `${SILVERBULLET_SERVER}/${params.image.replace(/^\//, "")}`
const imageUrl = params.image.startsWith("resources")
? `https://marka.max-richter.dev/${params.image.replace(/^\//, "")}`
: params.image;
log.debug("Processing image request:", { imageUrl, params });

View File

@@ -1,10 +1,8 @@
import { Handlers } from "$fresh/server.ts";
import { getDocuments } from "@lib/documents.ts";
import { json } from "@lib/helpers.ts";
export const handler: Handlers = {
async GET() {
const documents = await getDocuments();
return json(documents);
return json([]);
},
};

View File

@@ -1,16 +1,16 @@
import { Handlers } from "$fresh/server.ts";
import { createMovie, getMovie, Movie } from "@lib/resource/movies.ts";
import { 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 { isString, safeFileName } from "@lib/string.ts";
import { createDocument } from "@lib/documents.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET(_, ctx) {
const movie = await getMovie(ctx.params.name);
return json(movie);
const movie = await fetchResource(`movies/${ctx.params.name}`);
return json(movie?.content);
},
async POST(_, ctx) {
const session = ctx.state.session;

View File

@@ -1,7 +1,5 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { createDocument } from "@lib/documents.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { createMovie, getMovie } from "@lib/resource/movies.ts";
import * as tmdb from "@lib/tmdb.ts";
import { isString, safeFileName } from "@lib/string.ts";
import { json } from "@lib/helpers.ts";
@@ -11,6 +9,7 @@ import {
NotFoundError,
} from "@lib/errors.ts";
import { createRecommendationResource } from "@lib/recommendation.ts";
import { fetchResource } from "@lib/resources.ts";
const POST = async (
req: Request,
@@ -21,7 +20,7 @@ const POST = async (
throw new AccessDeniedError();
}
const movie = await getMovie(ctx.params.name);
const movie = await fetchResource(`movies/${ctx.params.name}`);
if (!movie) {
throw new NotFoundError();
}
@@ -72,12 +71,12 @@ const POST = async (
const poster = await tmdb.getMoviePoster(posterPath);
const extension = fileExtension(posterPath);
finalPath = `Media/movies/images/${safeFileName(name)}_cover.${extension}`;
await createDocument(finalPath, poster);
// await createDocument(finalPath, poster);
movie.meta = movie.meta || {};
movie.meta.image = finalPath;
}
await createMovie(movie.id, movie);
// await createMovie(movie.id, movie);
createRecommendationResource(movie, movieDetails.overview);

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { getAllMovies } from "@lib/resource/movies.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET() {
const movies = await getAllMovies();
const movies = await fetchResource("movies");
return json(movies);
},
};

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { getRecipe } from "@lib/resource/recipes.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET(_, ctx) {
const recipe = await getRecipe(ctx.params.name);
const recipe = await fetchResource(`recipes/${ctx.params.name}`);
return json(recipe);
},
};

View File

@@ -6,8 +6,8 @@ import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts";
import tds from "https://cdn.skypack.dev/turndown@7.2.0";
import { createLogger } from "@lib/log/index.ts";
import { createRecipe, Recipe } from "@lib/resource/recipes.ts";
import recipeSchema, { isValidRecipe } from "@lib/recipeSchema.ts";
import { Recipe } from "@lib/resource/recipes.ts";
import recipeSchema from "@lib/recipeSchema.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";
@@ -205,10 +205,10 @@ async function processCreateRecipeFromUrl(
streamResponse.enqueue("downloading image");
try {
streamResponse.enqueue("downloading image");
const res = await fetch(src);
// const res = await fetch(src);
streamResponse.enqueue("saving image");
const buffer = await res.arrayBuffer();
await createDocument(finalPath, buffer);
// const buffer = await res.arrayBuffer();
// await createDocument(finalPath, buffer);
newRecipe.meta.image = finalPath;
} catch (err) {
console.log("Failed to save image", err);
@@ -218,7 +218,7 @@ async function processCreateRecipeFromUrl(
streamResponse.enqueue("finished processing, creating file");
await createRecipe(newRecipe.id, newRecipe);
// await createRecipe(newRecipe.id, newRecipe);
streamResponse.enqueue("id: " + newRecipe.id);
}

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { getAllRecipes } from "@lib/resource/recipes.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET() {
const recipes = await getAllRecipes();
const recipes = await fetchResource("recipes");
return json(recipes);
},
};

View File

@@ -1,22 +1,23 @@
import { Handlers } from "$fresh/server.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
import { Movie } from "@lib/resource/movies.ts";
import * as tmdb from "@lib/tmdb.ts";
import {
createRecommendationResource,
getRecommendation,
} from "@lib/recommendation.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { fetchResource } from "@lib/resources.ts";
async function processUpdateRecommendations(
streamResponse: ReturnType<typeof createStreamResponse>,
) {
const allMovies = await getAllMovies();
const allMovies = await fetchResource("movies");
const movies = allMovies.filter((m) => {
if (!m?.meta) return false;
if (!m.meta.rating) return false;
if (!m.meta.tmdbId) return false;
const movies = allMovies?.content.filter((m) => {
if (!m?.content) return false;
if (!m.content.reviewRating) return false;
if (!m.content.tmdbId) return false;
return true;
}) as Movie[];

View File

@@ -5,11 +5,12 @@ import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"
import { isString, safeFileName } from "@lib/string.ts";
import { createDocument } from "@lib/documents.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { createSeries, getSeries, Series } from "@lib/resource/series.ts";
import { Series } from "@lib/resource/series.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers = {
async GET(_, ctx) {
const series = await getSeries(ctx.params.name);
const series = await fetchResource(`series/${ctx.params.name}`);
return json(series);
},
async POST(_, ctx) {

View File

@@ -9,7 +9,6 @@ import {
BadRequestError,
NotFoundError,
} from "@lib/errors.ts";
import { createSeries, getSeries } from "@lib/resource/series.ts";
const isString = (input: string | undefined): input is string => {
return typeof input === "string";
@@ -64,15 +63,15 @@ const POST = async (
let finalPath = "";
if (posterPath && !series.meta?.image) {
const poster = await tmdb.getMoviePoster(posterPath);
// const poster = await tmdb.getMoviePoster(posterPath);
const extension = fileExtension(posterPath);
finalPath = `Media/series/images/${safeFileName(name)}_cover.${extension}`;
await createDocument(finalPath, poster);
// await createDocument(finalPath, poster);
series.meta = series.meta || {};
series.meta.image = finalPath;
}
await createSeries(series.id, series);
// await createSeries(series.id, series);
return json(series);
};

View File

@@ -1,6 +1,6 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Article, getArticle } from "@lib/resource/articles.ts";
import { Article } from "@lib/resource/articles.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { YoutubePlayer } from "@components/Youtube.tsx";
import { HashTags } from "@components/HashTags.tsx";
@@ -10,10 +10,11 @@ import { RedirectSearchHandler } from "@islands/Search.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers<{ article: Article; session: unknown }> = {
async GET(_, ctx) {
const article = await getArticle(ctx.params.name);
const article = await fetchResource(`articles/${ctx.params.name}.md`);
if (!article) {
return ctx.renderNotFound();
}
@@ -26,34 +27,39 @@ export default function Greet(
) {
const { article, session } = props.data;
const { author = "", date = "" } = article.meta;
const { author = "", date = "", articleBody = "" } = article?.content || {};
const content = renderMarkdown(
removeImage(article.content, article.meta.image),
removeImage(articleBody, article.content.image),
);
console.log({ article });
return (
<MainLayout
url={props.url}
title={`Article > ${article.name}`}
title={`Article > ${article.content.headline}`}
context={article}
>
<RedirectSearchHandler />
<KMenu type="main" context={{ type: "article" }} />
<MetaTags resource={article} />
<PageHero image={article.meta.image} thumbnail={article.meta.thumbnail}>
<PageHero
image={article.content.image}
thumbnail={article.content.thumbnail}
>
<PageHero.Header>
<PageHero.BackLink href="/articles" />
{session && (
<PageHero.EditLink
href={`https://notes.max-richter.dev/Media/articles/${article.id}`}
href={`https://notes.max-richter.dev/resources/articles/${article.name}`}
/>
)}
</PageHero.Header>
<PageHero.Footer>
<PageHero.Title link={article.meta.link}>
{article.name}
<PageHero.Title link={article.content.url}>
{article.content.headline}
</PageHero.Title>
<PageHero.Subline
entries={[
@@ -64,20 +70,20 @@ export default function Greet(
date.toString(),
]}
>
{article.meta.rating && <Star rating={article.meta.rating} />}
{article.content.rating && <Star rating={article.content.rating} />}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
{article.tags.length > 0 && (
{article.content?.tags?.length > 0 && (
<>
<br />
<HashTags tags={article.tags} />
<HashTags tags={article.content.tags} />
</>
)}
<div class="px-8 text-white mt-10">
{isYoutubeLink(article.meta.link) && (
<YoutubePlayer link={article.meta.link} />
{isYoutubeLink(article.content.url) && (
<YoutubePlayer link={article.content.url} />
)}
<pre
class="whitespace-break-spaces markdown-body"
@@ -85,7 +91,7 @@ export default function Greet(
data-dark-theme="dark"
dangerouslySetInnerHTML={{ __html: content || "" }}
>
{content||""}
{content || ""}
</pre>
</div>
</MainLayout>

View File

@@ -1,6 +1,6 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Article, getAllArticles } from "@lib/resource/articles.ts";
import { Article } from "@lib/resource/articles.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
@@ -9,12 +9,14 @@ import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource } from "@lib/types.ts";
import { ResourceCard } from "@components/Card.tsx";
import { Link } from "@islands/Link.tsx";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers<
{ articles: Article[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
const articles = await getAllArticles();
const { content: articles } = await fetchResource("articles");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["article"] });

View File

@@ -4,7 +4,7 @@ import { PageProps } from "$fresh/server.ts";
import { resources } from "@lib/resources.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import "@lib/telegram.ts";
// import "@lib/telegram.ts";
export default function Home(props: PageProps) {
return (
@@ -22,11 +22,10 @@ export default function Home(props: PageProps) {
<Card
title={`${m.name}`}
backgroundSize={80}
image={`${
m.emoji.endsWith(".png")
image={`${m.emoji.endsWith(".png")
? `/emojis/${encodeURIComponent(m.emoji)}`
: "/placeholder.svg"
}`}
}`}
link={m.link}
/>
);

View File

@@ -1,7 +1,6 @@
import { PageProps, RouteContext } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { getMovie, Movie } from "@lib/resource/movies.ts";
import { HashTags } from "@components/HashTags.tsx";
import { Movie } from "@lib/resource/movies.ts";
import { removeImage, renderMarkdown } from "@lib/documents.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
@@ -9,22 +8,24 @@ import { Recommendations } from "@islands/Recommendations.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export default async function Greet(
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
ctx: RouteContext,
) {
const movie = await getMovie(ctx.params.name);
const movie = await fetchResource(`movies/${ctx.params.name}.md`);
const session = ctx.state.session;
if (!movie) {
return ctx.renderNotFound();
}
const { author = "", date = "" } = movie.meta;
const { author = "", date = "" } = movie.content;
const content = renderMarkdown(
removeImage(movie.description || "", movie.meta.image),
removeImage(movie.content.reviewBody || "", movie.content.image),
);
return (
@@ -33,14 +34,14 @@ export default async function Greet(
<KMenu type="main" context={movie} />
<MetaTags resource={movie} />
<PageHero
image={movie.meta.image}
thumbnail={movie.meta.thumbnail}
image={movie.content.image}
thumbnail={movie.content.thumbnail}
>
<PageHero.Header>
<PageHero.BackLink href="/movies" />
{session && (
<PageHero.EditLink
href={`https://notes.max-richter.dev/Media/movies/${movie.id}`}
href={`https://notes.max-richter.dev/resources/movies/${movie.name}`}
/>
)}
</PageHero.Header>
@@ -49,13 +50,17 @@ export default async function Greet(
<PageHero.Subline
entries={[
author && {
title: author,
href: `/?q=${encodeURIComponent(author)}`,
title: author?.name,
href: `/?q=${encodeURIComponent(author?.name)}`,
},
date.toString(),
]}
>
{movie.meta.rating && <Star rating={movie.meta.rating} />}
{movie.content.reviewRating && (
<Star
rating={parseRating(movie.content.reviewRating?.ratingValue)}
/>
)}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
@@ -65,14 +70,8 @@ export default async function Greet(
type="movie"
/>
)}
{movie.tags.length > 0 && (
<>
<br />
<HashTags tags={movie.tags} />
</>
)}
<div class="px-8 text-white mt-10">
{movie?.description?.length > 80
{movie?.content?.reviewBody?.length > 80
? <h2 class="text-4xl font-bold mb-4">Review</h2>
: <></>}
<pre

View File

@@ -1,25 +1,26 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { getAllMovies, Movie } from "@lib/resource/movies.ts";
import { Movie } from "@lib/resource/movies.ts";
import { ResourceCard } from "@components/Card.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource } from "@lib/types.ts";
import { PageProps } from "$fresh/server.ts";
import { fetchResource } from "@lib/resources.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
export default async function Greet(
props: PageProps<
{ movies: Movie[] | null; searchResults: GenericResource[] }
>,
) {
const allMovies = await getAllMovies();
const { content: allMovies } = await fetchResource("movies");
const searchParams = parseResourceUrl(props.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["movie"] });
const movies = allMovies.sort((a, b) =>
a?.meta?.rating > b?.meta?.rating ? -1 : 1
a?.content?.reviewRating?.ratingValue > b?.content?.reviewRating?.ratingValue ? -1 : 1
);
return (

View File

@@ -3,7 +3,7 @@ import { IngredientsList } from "@islands/IngredientsList.tsx";
import { MainLayout } from "@components/layouts/main.tsx";
import Counter from "@islands/Counter.tsx";
import { Signal, useSignal } from "@preact/signals";
import { getRecipe, Recipe } from "@lib/resource/recipes.ts";
import { Recipe } from "@lib/recipeSchema.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import PageHero from "@components/PageHero.tsx";
@@ -11,11 +11,12 @@ import { Star } from "@components/Stars.tsx";
import { renderMarkdown } from "@lib/documents.ts";
import { isValidRecipe } from "@lib/recipeSchema.ts";
import { MetaTags } from "@components/MetaTags.tsx";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
async GET(_, ctx) {
try {
const recipe = await getRecipe(ctx.params.name);
const recipe = await fetchResource(`recipes/${ctx.params.name}.md`);
if (!recipe) {
return ctx.renderNotFound();
}
@@ -38,22 +39,24 @@ function ValidRecipe({
{portion && <Counter count={amount} />}
</div>
<IngredientsList
ingredients={recipe.ingredients}
ingredients={recipe.content.recipeIngredient}
amount={amount}
portion={portion}
/>
<h3 class="text-3xl my-5">Preparation</h3>
<div class="pl-2">
<ol class="list-decimal grid gap-4">
{recipe.instructions && (recipe.instructions.map((instruction) => {
return (
<li
dangerouslySetInnerHTML={{
__html: renderMarkdown(instruction),
}}
/>
);
}))}
{recipe.content.recipeInstructions &&
(recipe.content.recipeInstructions.filter((inst) => !!inst?.length)
.map((instruction) => {
return (
<li
dangerouslySetInnerHTML={{
__html: renderMarkdown(instruction),
}}
/>
);
}))}
</ol>
</div>
</>
@@ -65,35 +68,38 @@ export default function Page(
) {
const { recipe, session } = props.data;
const portion = recipe.meta?.portion;
const portion = recipe.recipeYield;
const amount = useSignal(portion || 1);
const subline = [
recipe?.meta?.time && `Duration ${recipe.meta.time}`,
recipe?.content?.prepTime && `Duration ${recipe?.content?.prepTime}`,
].filter(Boolean) as string[];
return (
<MainLayout
url={props.url}
title={`Recipes > ${recipe.name}`}
title={`Recipes > ${recipe.content?.name}`}
context={recipe}
>
<RedirectSearchHandler />
<KMenu type="main" context={recipe} />
<MetaTags resource={recipe} />
<PageHero image={recipe.meta?.image} thumbnail={recipe.meta?.thumbnail}>
<PageHero
image={recipe.content?.image}
thumbnail={recipe.content?.thumbnail}
>
<PageHero.Header>
<PageHero.BackLink href="/recipes" />
{session && (
<PageHero.EditLink
href={`https://notes.max-richter.dev/Recipes/${recipe.id}`}
href={`https://notes.max-richter.dev/resources/recipes/${recipe.name}`}
/>
)}
</PageHero.Header>
<PageHero.Footer>
<PageHero.Title link={recipe.meta?.link}>
{recipe.name}
<PageHero.Title link={recipe.content?.link}>
{recipe.content.name}
</PageHero.Title>
<PageHero.Subline
entries={subline}
@@ -113,12 +119,9 @@ export default function Page(
/>
)
: (
<div
class="whitespace-break-spaces markdown-body"
dangerouslySetInnerHTML={{
__html: renderMarkdown(recipe?.markdown || ""),
}}
/>
<div class="whitespace-break-spaces markdown-body">
{JSON.stringify(recipe)}
</div>
)}
</div>
</MainLayout>

View File

@@ -1,19 +1,20 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts";
import { Recipe } from "@lib/recipeSchema.ts";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource } from "@lib/types.ts";
import { ResourceCard } from "@components/Card.tsx";
import { fetchResource } from "@lib/resources.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
export const handler: Handlers<
{ recipes: Recipe[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
const recipes = await getAllRecipes();
const { content: recipes } = await fetchResource("recipes");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["recipe"] });
@@ -48,8 +49,8 @@ export default function Greet(
<h3 class="text-2xl text-white font-light">🍽 Recipes</h3>
</header>
<Grid>
{recipes?.map((doc) => {
return <ResourceCard sublink="recipes" res={doc} />;
{recipes?.filter((s) => !!s?.content?.name).map((doc) => {
return <ResourceCard sublink="recipes" key={doc.name} res={doc} />;
})}
</Grid>
</MainLayout>

View File

@@ -2,16 +2,18 @@ import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { HashTags } from "@components/HashTags.tsx";
import { removeImage, renderMarkdown } from "@lib/documents.ts";
import { getSeries, Series } from "@lib/resource/series.ts";
import { Series } from "@lib/resource/series.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/resources.ts";
export const handler: Handlers<{ serie: Series; session: unknown }> = {
async GET(_, ctx) {
const serie = await getSeries(ctx.params.name);
const serie = await fetchResource(`series/${ctx.params.name}`);
if (!serie) {
return ctx.renderNotFound();
@@ -25,24 +27,28 @@ export default function Greet(
) {
const { serie, session } = props.data;
const { author = "", date = "" } = serie.meta;
const { author = "", date = "" } = serie.content;
const content = renderMarkdown(
removeImage(serie.description || "", serie.meta.image),
removeImage(serie.description || "", serie.content.image),
);
return (
<MainLayout url={props.url} title={`Serie > ${serie.name}`} context={serie}>
<MainLayout
url={props.url}
title={`Serie > ${serie.content.name}`}
context={serie}
>
<RedirectSearchHandler />
<KMenu type="main" context={serie} />
<MetaTags resource={serie} />
<PageHero image={serie.meta.image} thumbnail={serie.meta.thumbnail}>
<PageHero image={serie.content.image} thumbnail={serie.content.thumbnail}>
<PageHero.Header>
<PageHero.BackLink href="/series" />
{session && (
<PageHero.EditLink
href={`https://notes.max-richter.dev/Media/series/${serie.id}`}
href={`https://notes.max-richter.dev/resources/series/${serie.name}`}
/>
)}
</PageHero.Header>
@@ -57,18 +63,22 @@ export default function Greet(
date.toString(),
]}
>
{serie.meta.rating && <Star rating={serie.meta.rating} />}
{serie.content.reviewRating && (
<Star
rating={parseRating(serie.content.reviewRating.ratingValue)}
/>
)}
</PageHero.Subline>
</PageHero.Footer>
</PageHero>
{serie.tags.length > 0 && (
{serie.content?.tags?.length > 0 && (
<>
<br />
<HashTags tags={serie.tags} />
<HashTags tags={serie.content.tags} />
</>
)}
<div class="px-8 text-white mt-10">
{serie?.description?.length > 80
{serie?.content?.reviewBody?.length > 80
? <h2 class="text-4xl font-bold mb-4">Review</h2>
: <></>}
<pre

View File

@@ -2,18 +2,19 @@ import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { getAllSeries, Series } from "@lib/resource/series.ts";
import { Series } from "@lib/resource/series.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { ResourceCard } from "@components/Card.tsx";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource } from "@lib/types.ts";
import { fetchResource } from "@lib/resources.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
export const handler: Handlers<
{ series: Series[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
const series = await getAllSeries();
const { content: series } = await fetchResource("series");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["series"] });