From d47ffb94bf0f12a96e86a8652c01d17d52d411a7 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Sun, 30 Jul 2023 23:55:51 +0200 Subject: [PATCH] feat: add movies --- components/Card.tsx | 2 +- components/MovieCard.tsx | 18 +++++ components/RecipeHero.tsx | 26 ++++--- fresh.gen.ts | 32 +++++---- lib/movies.ts | 70 +++++++++++++++++++ routes/api/movies/[name].ts | 23 ++++++ routes/api/movies/index.ts | 34 +++++++++ routes/index.tsx | 11 +-- routes/movies/[name].tsx | 38 +++------- routes/movies/index.tsx | 16 +++-- routes/recipes/[name].tsx | 2 +- routes/recipes/index.tsx | 3 +- static/g10.svg:Zone.Identifier | 3 - static/g8.svg:Zone.Identifier | 3 - ...n-vector-illustration_.svg:Zone.Identifier | 4 -- 15 files changed, 211 insertions(+), 74 deletions(-) create mode 100644 components/MovieCard.tsx create mode 100644 lib/movies.ts create mode 100644 routes/api/movies/[name].ts create mode 100644 routes/api/movies/index.ts delete mode 100644 static/g10.svg:Zone.Identifier delete mode 100644 static/g8.svg:Zone.Identifier delete mode 100644 static/vecteezy_picture-gallery-image-line-icon-vector-illustration_.svg:Zone.Identifier diff --git a/components/Card.tsx b/components/Card.tsx index 2b19fa9..dcad4af 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -10,7 +10,7 @@ export function Card( backgroundImage: `url(${image})`, backgroundSize: "cover", }} - class="bg-gray-900 text-white rounded-3xl shadow-md p-4 relative overflow-hidden + class="text-white rounded-3xl shadow-md p-4 relative overflow-hidden lg:w-56 lg:h-56 sm:w-40 sm:h-40 w-32 h-32" diff --git a/components/MovieCard.tsx b/components/MovieCard.tsx new file mode 100644 index 0000000..555d53a --- /dev/null +++ b/components/MovieCard.tsx @@ -0,0 +1,18 @@ +import { Card } from "@components/Card.tsx"; +import { Movie } from "@lib/movies.ts"; + +export function MovieCard({ movie }: { movie: Movie }) { + const { meta: { image = "/placeholder.svg" } = {} } = movie; + + const imageUrl = image?.startsWith("Media/movies/") + ? `/api/images?image=${image}&width=200&height=200` + : image; + + return ( + + ); +} diff --git a/components/RecipeHero.tsx b/components/RecipeHero.tsx index 7ff53c9..6860989 100644 --- a/components/RecipeHero.tsx +++ b/components/RecipeHero.tsx @@ -2,12 +2,18 @@ import { Recipe } from "@lib/recipes.ts"; import IconExternalLink from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx"; import { Star } from "@components/Stars.tsx"; -export function RecipeHero({ recipe }: { recipe: Recipe }) { - const { meta: { image } = {} } = recipe; +export function RecipeHero( + { data, backlink }: { + backlink: string; + data: { meta?: { image?: string; link?: string }; name: string }; + }, +) { + const { meta: { image } = {} } = data; - const imageUrl = image?.startsWith("Recipes/images/") - ? `/api/images?image=${image}&width=800` - : image; + const imageUrl = + (image?.startsWith("Recipes/images/") || image?.startsWith("Media/movies/")) + ? `/api/images?image=${image}&width=800` + : image; return ( - {recipe.meta?.link && + {data.meta?.link && ( ); diff --git a/fresh.gen.ts b/fresh.gen.ts index 18a0472..d27509d 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -6,13 +6,15 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; import * as $2 from "./routes/api/images/index.ts"; import * as $3 from "./routes/api/index.ts"; -import * as $4 from "./routes/api/recipes/[name].ts"; -import * as $5 from "./routes/api/recipes/index.ts"; -import * as $6 from "./routes/index.tsx"; -import * as $7 from "./routes/movies/[name].tsx"; -import * as $8 from "./routes/movies/index.tsx"; -import * as $9 from "./routes/recipes/[name].tsx"; -import * as $10 from "./routes/recipes/index.tsx"; +import * as $4 from "./routes/api/movies/[name].ts"; +import * as $5 from "./routes/api/movies/index.ts"; +import * as $6 from "./routes/api/recipes/[name].ts"; +import * as $7 from "./routes/api/recipes/index.ts"; +import * as $8 from "./routes/index.tsx"; +import * as $9 from "./routes/movies/[name].tsx"; +import * as $10 from "./routes/movies/index.tsx"; +import * as $11 from "./routes/recipes/[name].tsx"; +import * as $12 from "./routes/recipes/index.tsx"; import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/IngredientsList.tsx"; @@ -22,13 +24,15 @@ const manifest = { "./routes/_app.tsx": $1, "./routes/api/images/index.ts": $2, "./routes/api/index.ts": $3, - "./routes/api/recipes/[name].ts": $4, - "./routes/api/recipes/index.ts": $5, - "./routes/index.tsx": $6, - "./routes/movies/[name].tsx": $7, - "./routes/movies/index.tsx": $8, - "./routes/recipes/[name].tsx": $9, - "./routes/recipes/index.tsx": $10, + "./routes/api/movies/[name].ts": $4, + "./routes/api/movies/index.ts": $5, + "./routes/api/recipes/[name].ts": $6, + "./routes/api/recipes/index.ts": $7, + "./routes/index.tsx": $8, + "./routes/movies/[name].tsx": $9, + "./routes/movies/index.tsx": $10, + "./routes/recipes/[name].tsx": $11, + "./routes/recipes/index.tsx": $12, }, islands: { "./islands/Counter.tsx": $$0, diff --git a/lib/movies.ts b/lib/movies.ts new file mode 100644 index 0000000..9170f5c --- /dev/null +++ b/lib/movies.ts @@ -0,0 +1,70 @@ +import { + parseDocument, + parseFrontmatter, + renderMarkdown, +} from "@lib/documents.ts"; + +export type Movie = { + id: string; + name: string; + description: string; + hashtags: string[]; + meta: { + published: Date; + image: string; + author: string; + rating: number; + status: "not-seen" | "watch-again" | "finished"; + }; +}; + +export function parseMovie(original: string, id: string): Movie { + const doc = parseDocument(original); + + let meta = {} as Movie["meta"]; + let name = ""; + + const range = [Infinity, -Infinity]; + + for (const child of doc.children) { + if (child.type === "yaml") { + meta = parseFrontmatter(child.value) as Movie["meta"]; + + if (meta["rating"] && typeof meta["rating"] === "string") { + meta.rating = [...meta.rating?.matchAll("⭐")].length; + } + + continue; + } + + if ( + child.type === "heading" && child.depth === 1 && !name && + child.children.length === 1 && child.children[0].type === "text" + ) { + name = child.children[0].value; + continue; + } + + if (name) { + const start = child.position?.start.offset || Infinity; + const end = child.position?.end.offset || -Infinity; + if (start < range[0]) range[0] = start; + if (end > range[1]) range[1] = end; + } + } + + 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, ""); + } + + return { + id, + name, + hashtags, + description: renderMarkdown(description), + meta, + }; +} diff --git a/routes/api/movies/[name].ts b/routes/api/movies/[name].ts new file mode 100644 index 0000000..88e0f26 --- /dev/null +++ b/routes/api/movies/[name].ts @@ -0,0 +1,23 @@ +import { HandlerContext } from "$fresh/server.ts"; +import { getDocument } from "@lib/documents.ts"; +import { parseMovie } from "@lib/movies.ts"; + +export async function getMovie(name: string) { + const document = await getDocument(`Media/movies/${name}.md`); + + const movie = parseMovie(document, name); + + return movie; +} + +export const handler = async ( + _req: Request, + _ctx: HandlerContext, +): Promise => { + const movie = await getMovie(_ctx.params.name); + + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + + return new Response(JSON.stringify(movie)); +}; diff --git a/routes/api/movies/index.ts b/routes/api/movies/index.ts new file mode 100644 index 0000000..d9adb65 --- /dev/null +++ b/routes/api/movies/index.ts @@ -0,0 +1,34 @@ +import { HandlerContext } from "$fresh/server.ts"; +import { getDocument, getDocuments } from "@lib/documents.ts"; +import { parseMovie } from "@lib/movies.ts"; + +export async function getMovies() { + const documents = await getDocuments(); + + return Promise.all( + documents.filter((d) => { + return d.name.startsWith("Media/movies/") && + d.contentType === "text/markdown" && + !d.name.endsWith("index.md"); + }).map(async (doc) => { + const document = await getDocument(doc.name); + const movie = parseMovie(document, doc.name); + return { + ...movie, + id: movie.id.replace(/\.md$/, "").replace(/^Media\/movies\//, ""), + }; + }), + ); +} + +export const handler = async ( + _req: Request, + _ctx: HandlerContext, +): Promise => { + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + + const movies = await getMovies(); + + return new Response(JSON.stringify(movies), { headers }); +}; diff --git a/routes/index.tsx b/routes/index.tsx index 80640bf..905556c 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -1,6 +1,4 @@ import { Head } from "$fresh/runtime.ts"; -import { useSignal } from "@preact/signals"; -import Counter from "@islands/Counter.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; import { Card } from "@components/Card.tsx"; import { PageProps } from "$fresh/server.ts"; @@ -14,10 +12,15 @@ export default function Home(props: PageProps) {
+
diff --git a/routes/movies/[name].tsx b/routes/movies/[name].tsx index 43c8920..c8653c3 100644 --- a/routes/movies/[name].tsx +++ b/routes/movies/[name].tsx @@ -1,44 +1,28 @@ import { Handlers, PageProps } from "$fresh/server.ts"; -import { IngredientsList } from "@islands/IngredientsList.tsx"; -import { RecipeHero } from "@components/RecipeHero.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Recipe } from "@lib/recipes.ts"; -import { getRecipe } from "../api/recipes/[name].ts"; -import Counter from "@islands/Counter.tsx"; -import { useSignal } from "@preact/signals"; +import { Movie } from "@lib/movies.ts"; +import { getMovie } from "../api/movies/[name].ts"; +import { RecipeHero } from "@components/RecipeHero.tsx"; -export const handler: Handlers = { +export const handler: Handlers = { async GET(_, ctx) { - const recipe = await getRecipe(ctx.params.name); - return ctx.render(recipe); + const movie = await getMovie(ctx.params.name); + return ctx.render(movie); }, }; -export default function Greet(props: PageProps) { - const recipe = props.data; - - const portion = recipe.meta?.portion; - const amount = useSignal(portion || 1); +export default function Greet(props: PageProps) { + const movie = props.data; return ( - +
-
-

Ingredients

- {portion && } -
- -

Preparation

-          {recipe.preparation}
+          {movie.description}
         
diff --git a/routes/movies/index.tsx b/routes/movies/index.tsx index 2cef35b..1ff466f 100644 --- a/routes/movies/index.tsx +++ b/routes/movies/index.tsx @@ -4,14 +4,18 @@ import { MainLayout } from "@components/layouts/main.tsx"; import { Recipe } from "@lib/recipes.ts"; import { getRecipes } from "../api/recipes/index.ts"; import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx"; -export const handler: Handlers = { +import { getMovies } from "../api/movies/index.ts"; +import { Movie } from "@lib/movies.ts"; +import { MovieCard } from "@components/MovieCard.tsx"; + +export const handler: Handlers = { async GET(_, ctx) { - const recipes = await getRecipes(); - return ctx.render(recipes); + const movies = await getMovies(); + return ctx.render(movies); }, }; -export default function Greet(props: PageProps) { +export default function Greet(props: PageProps) { return (
@@ -23,11 +27,11 @@ export default function Greet(props: PageProps) { Back -

Recipes

+

🍿 Movies

{props.data?.map((doc) => { - return ; + return ; })}
diff --git a/routes/recipes/[name].tsx b/routes/recipes/[name].tsx index 43c8920..265a581 100644 --- a/routes/recipes/[name].tsx +++ b/routes/recipes/[name].tsx @@ -22,7 +22,7 @@ export default function Greet(props: PageProps) { return ( - +

Ingredients

diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index 2cef35b..c648dcd 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -4,6 +4,7 @@ import { MainLayout } from "@components/layouts/main.tsx"; import { Recipe } from "@lib/recipes.ts"; import { getRecipes } from "../api/recipes/index.ts"; import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx"; + export const handler: Handlers = { async GET(_, ctx) { const recipes = await getRecipes(); @@ -23,7 +24,7 @@ export default function Greet(props: PageProps) { Back -

Recipes

+

🍽️ Recipes

{props.data?.map((doc) => { diff --git a/static/g10.svg:Zone.Identifier b/static/g10.svg:Zone.Identifier deleted file mode 100644 index 053d112..0000000 --- a/static/g10.svg:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -HostUrl=about:internet diff --git a/static/g8.svg:Zone.Identifier b/static/g8.svg:Zone.Identifier deleted file mode 100644 index 053d112..0000000 --- a/static/g8.svg:Zone.Identifier +++ /dev/null @@ -1,3 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -HostUrl=about:internet diff --git a/static/vecteezy_picture-gallery-image-line-icon-vector-illustration_.svg:Zone.Identifier b/static/vecteezy_picture-gallery-image-line-icon-vector-illustration_.svg:Zone.Identifier deleted file mode 100644 index 6e88659..0000000 --- a/static/vecteezy_picture-gallery-image-line-icon-vector-illustration_.svg:Zone.Identifier +++ /dev/null @@ -1,4 +0,0 @@ -[ZoneTransfer] -ZoneId=3 -ReferrerUrl=https://cloudconvert.com/ -HostUrl=https://storage.cloudconvert.com/tasks/748af8fd-a74c-4374-b73b-fa782814df9d/vecteezy_picture-gallery-image-line-icon-vector-illustration_.svg?AWSAccessKeyId=cloudconvert-production&Expires=1690830191&Signature=fBYB2Q0uKneAnH7%2BrXXzsWOKUXw%3D&response-content-disposition=attachment%3B%20filename%3D%22vecteezy_picture-gallery-image-line-icon-vector-illustration_.svg%22&response-content-type=image%2Fsvg%2Bxml