Compare commits
22 Commits
feat/memor
...
ae266dbdc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae266dbdc5
|
||
|
|
3a120e32fc
|
||
|
|
d9a2f63865
|
||
|
|
24a66940e9
|
||
|
|
2446629515
|
||
|
|
7048db9d76
|
||
|
|
0db489269b
|
||
|
|
1c3f136f57
|
||
|
|
038e78f27d
|
||
|
|
93c00e1c7e
|
||
|
|
c914ee6719
|
||
|
|
9ca190550d
|
||
|
|
edecf0bf75
|
||
|
|
a27e9046c0
|
||
|
|
5ba54fee6e
|
||
|
|
06e5126fe0
|
||
|
|
61251e2c85
|
||
|
|
a1b8eb22e5
|
||
|
|
246fc3ae44
|
||
|
|
48f451ceb0
|
||
|
|
fba2337b9c
|
||
| 546b36f44f |
@@ -1,12 +1,12 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { i18n, filterSitemapByDefaultLocale } from "astro-i18n-aut/integration";
|
||||
import { defineConfig } from "astro/config";
|
||||
import { filterSitemapByDefaultLocale, i18n } from "astro-i18n-aut/integration";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import mdx from '@astrojs/mdx';
|
||||
import glsl from 'vite-plugin-glsl';
|
||||
import Icons from "unplugin-icons/vite";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import glsl from "vite-plugin-glsl";
|
||||
|
||||
import svelte from "@astrojs/svelte";
|
||||
import UnoCSS from 'unocss/astro'
|
||||
import UnoCSS from "unocss/astro";
|
||||
|
||||
const defaultLocale = "de";
|
||||
const locales = {
|
||||
@@ -14,7 +14,7 @@ const locales = {
|
||||
de: "de",
|
||||
};
|
||||
|
||||
const DEFAULT_LAYOUT = '@layouts/Post.astro';
|
||||
const DEFAULT_LAYOUT = "@layouts/Post.astro";
|
||||
|
||||
function setDefaultLayout() {
|
||||
return function(_, file) {
|
||||
@@ -27,6 +27,9 @@ export default defineConfig({
|
||||
site: "https://max-richter.dev",
|
||||
trailingSlash: "never",
|
||||
prefetch: true,
|
||||
image: {
|
||||
remotePatterns: [{ protocol: "https" }],
|
||||
},
|
||||
build: {
|
||||
format: "file",
|
||||
},
|
||||
@@ -34,30 +37,30 @@ export default defineConfig({
|
||||
plugins: [
|
||||
glsl(),
|
||||
Icons({
|
||||
compiler: 'svelte',
|
||||
compiler: "svelte",
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
// Customize watch behavior to reduce file watchers
|
||||
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
|
||||
usePolling: process.env.NODE_ENV === 'production',
|
||||
ignored: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
||||
usePolling: process.env.NODE_ENV === "production",
|
||||
},
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
remarkPlugins: [setDefaultLayout]
|
||||
remarkPlugins: [setDefaultLayout],
|
||||
},
|
||||
integrations: [
|
||||
i18n({
|
||||
exclude: ["pages/**/*.json.ts", "pages/api/**/*",],
|
||||
exclude: ["pages/**/*.json.ts", "pages/api/**/*"],
|
||||
locales,
|
||||
defaultLocale,
|
||||
}),
|
||||
mdx(),
|
||||
svelte(),
|
||||
UnoCSS({
|
||||
injectReset: true
|
||||
injectReset: true,
|
||||
}),
|
||||
sitemap({
|
||||
i18n: {
|
||||
@@ -66,5 +69,5 @@ export default defineConfig({
|
||||
},
|
||||
filter: filterSitemapByDefaultLocale({ defaultLocale }),
|
||||
}),
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
34
package.json
@@ -10,33 +10,33 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.3.1",
|
||||
"@astrojs/svelte": "^7.1.0",
|
||||
"@astrojs/check": "^0.9.5",
|
||||
"@astrojs/mdx": "^4.3.7",
|
||||
"@astrojs/svelte": "^7.2.0",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"astro": "^5.12.0",
|
||||
"astro": "^5.14.8",
|
||||
"astro-i18n-aut": "^0.7.3",
|
||||
"exifreader": "^4.31.1",
|
||||
"svelte": "^5.36.10",
|
||||
"svelte-gestures": "^5.1.4",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"exifreader": "^4.32.0",
|
||||
"svelte": "^5.39.8",
|
||||
"svelte-gestures": "^5.2.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"thumbhash": "^0.1.1",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.4.1",
|
||||
"@iconify-json/tabler": "^1.2.19",
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@iconify-json/tabler": "^1.2.23",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@unocss/preset-icons": "^66.3.3",
|
||||
"@unocss/reset": "^66.3.3",
|
||||
"@unocss/preset-icons": "^66.5.2",
|
||||
"@unocss/reset": "^66.5.2",
|
||||
"astro-font": "^1.1.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"ogl": "^1.0.11",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"sharp": "^0.34.3",
|
||||
"unocss": "^66.3.3",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite-plugin-glsl": "^1.5.1"
|
||||
"sharp": "^0.34.4",
|
||||
"unocss": "^66.5.2",
|
||||
"unplugin-icons": "^22.4.2",
|
||||
"vite-plugin-glsl": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
2556
pnpm-lock.yaml
generated
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import markdownToText from "@helpers/markdownToText";
|
||||
import { markdownToText,readDuration } from "@helpers/markdown";
|
||||
import { Card } from "./card";
|
||||
import { useTranslatedPath, useTranslations } from "@i18n/utils";
|
||||
import Image from "@components/Image.astro";
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const {
|
||||
data: { title, cover, icon },
|
||||
data: { title, cover, icon, date },
|
||||
collection,
|
||||
body,
|
||||
id,
|
||||
@@ -30,14 +30,15 @@ const link = translatePath(`/${collection}/${id.split("/")[0]}`);
|
||||
<Card
|
||||
classes={`grid gradient border-1 border-neutral overflow-hidden ${cover ? "grid-rows-[200px_1fr] xs:grid-rows-none xs:grid-cols-[1fr_200px]" : ""}`}>
|
||||
<Card.Content classes="px-8 py-7 order-last xs:order-first">
|
||||
{(date || body)&& <Card.Metadata date={date} readDuration={readDuration(body)} />}
|
||||
<Card.Title classes="text-4xl flex items-center gap-2">
|
||||
{
|
||||
icon &&
|
||||
(
|
||||
icon?.length > 5
|
||||
? <img class="h-6 w-6" src={icon} />
|
||||
: <span class="p-r-4 text-md">{icon}</span>
|
||||
)
|
||||
icon &&
|
||||
(icon?.length > 5 ? (
|
||||
<img class="h-6 w-6" src={icon} />
|
||||
) : (
|
||||
<span class="p-r-4 text-md">{icon}</span>
|
||||
))
|
||||
}
|
||||
{title}
|
||||
</Card.Title>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { Picture as AstroImage } from "astro:assets";
|
||||
import { inferRemoteSize } from 'astro/assets/utils';
|
||||
import { generateThumbHash, getExifData } from "@helpers/image";
|
||||
import sharp from "sharp";
|
||||
interface Props {
|
||||
src: ImageMetadata & { fsPath?: string };
|
||||
alt: string;
|
||||
@@ -13,6 +15,25 @@ interface Props {
|
||||
maxWidth?: number;
|
||||
}
|
||||
|
||||
async function checkImage(image: ImageMetadata) {
|
||||
console.log("Checking image: ", image);
|
||||
const src = image.src;
|
||||
try {
|
||||
if (src.startsWith("/@fs") || src.startsWith("/_astro")) return true;
|
||||
const res = await inferRemoteSize(src);
|
||||
if (res.format) {
|
||||
image.format = res.format;
|
||||
return true;
|
||||
}else {
|
||||
console.log("Failed to load: ", src);
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.log("Failed to fetch: ", src);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
src: image,
|
||||
loader = true,
|
||||
@@ -22,9 +43,10 @@ const {
|
||||
maxWidth,
|
||||
} = Astro.props;
|
||||
|
||||
let thumbhash = hash && image.fsPath ? await generateThumbHash(image) : "";
|
||||
let thumbhash = hash && await generateThumbHash(image);
|
||||
const imageOk = await checkImage(image);
|
||||
|
||||
let exif = await getExifData(image);
|
||||
let exif = imageOk && (await getExifData(image));
|
||||
|
||||
const sizes = [
|
||||
{
|
||||
@@ -45,18 +67,23 @@ const sizes = [
|
||||
].filter((size) => !maxWidth || size.width <= maxWidth);
|
||||
---
|
||||
|
||||
<AstroImage
|
||||
src={image}
|
||||
alt={alt}
|
||||
data-thumbhash={thumbhash}
|
||||
data-exif={JSON.stringify(exif)}
|
||||
pictureAttributes={{
|
||||
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
|
||||
}}
|
||||
class={Astro.props.class}
|
||||
widths={sizes.map((size) => size.width)}
|
||||
sizes={sizes
|
||||
.map((size) => `${size.media || "100vw"} ${size.width}px`)
|
||||
.join(", ")}>
|
||||
<slot />
|
||||
</AstroImage>
|
||||
{
|
||||
imageOk ? (
|
||||
<AstroImage
|
||||
src={image}
|
||||
alt={alt}
|
||||
data-thumbhash={thumbhash}
|
||||
data-exif={JSON.stringify(exif)}
|
||||
inferSize={true}
|
||||
pictureAttributes={{
|
||||
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
|
||||
}}
|
||||
class={Astro.props.class}
|
||||
widths={sizes.map((size) => size.width)}
|
||||
sizes={sizes
|
||||
.map((size) => `${size.media || "100vw"} ${size.width}px`)
|
||||
.join(", ")}>
|
||||
<slot />
|
||||
</AstroImage>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
let progress: number[] = [];
|
||||
let currentIndex = -1;
|
||||
const maxZoom = 5;
|
||||
import { swipe } from "svelte-gestures";
|
||||
import { useSwipe } from "svelte-gestures";
|
||||
|
||||
const mod = (a: number, b: number) => ((a % b) + b) % b;
|
||||
|
||||
@@ -232,10 +232,9 @@
|
||||
|
||||
{#if currentIndex > -1}
|
||||
<div
|
||||
{...useSwipe(handleSwipe)}
|
||||
class="image"
|
||||
use:swipe
|
||||
role="dialog"
|
||||
on:swipe={handleSwipe}
|
||||
on:wheel|passive={handleScroll}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:pointermove={handlePointerMove}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import markdownToText from "@helpers/markdownToText";
|
||||
import { markdownToText } from "@helpers/markdown";
|
||||
import { useTranslatedPath } from "@i18n/utils";
|
||||
import type { InferEntrySchema } from "astro:content";
|
||||
|
||||
|
||||
30
src/components/card/Metadata.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
export let date: string | Date;
|
||||
export let readDuration: string | undefined;
|
||||
|
||||
const toDate = (d: string | Date) =>
|
||||
typeof d === "string" ? new Date(d) : d;
|
||||
|
||||
const iso = (d: string | Date) => {
|
||||
const v = toDate(d);
|
||||
return isNaN(v.getTime()) ? "" : v.toISOString();
|
||||
};
|
||||
|
||||
const formatDate = (d: string | Date) =>
|
||||
new Intl.DateTimeFormat("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(toDate(d));
|
||||
</script>
|
||||
|
||||
<div class="flex gap-5">
|
||||
{#if date}
|
||||
<time class="text-sm text-neutral" datetime={iso(date)}
|
||||
>{formatDate(date)}</time>
|
||||
{/if}
|
||||
|
||||
{#if readDuration}
|
||||
<div class="text-sm text-neutral">{readDuration} mins read</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,19 +1,22 @@
|
||||
import Wrapper from './Wrapper.svelte';
|
||||
import Image from './Image.svelte';
|
||||
import Content from './Content.svelte';
|
||||
import Title from './Title.svelte';
|
||||
import Description from './Description.svelte';
|
||||
import ReadMoreButton from './ReadMoreButton.svelte';
|
||||
import Wrapper from "./Wrapper.svelte";
|
||||
import Image from "./Image.svelte";
|
||||
import Content from "./Content.svelte";
|
||||
import Title from "./Title.svelte";
|
||||
import Description from "./Description.svelte";
|
||||
import ReadMoreButton from "./ReadMoreButton.svelte";
|
||||
import Metadata from "./Metadata.svelte";
|
||||
|
||||
const Card = Wrapper as typeof Wrapper & {
|
||||
Image: typeof Image;
|
||||
Metadata: typeof Metadata;
|
||||
Content: typeof Content;
|
||||
Title: typeof Title;
|
||||
Description: typeof Description;
|
||||
ReadMoreButton: typeof ReadMoreButton;
|
||||
}
|
||||
};
|
||||
|
||||
Card.Image = Image;
|
||||
Card.Metadata = Metadata;
|
||||
Card.Content = Content;
|
||||
Card.Title = Title;
|
||||
Card.Description = Description;
|
||||
|
||||
25
src/components/resources/Article.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { markdownToHtml, markdownToText } from "@helpers/markdown";
|
||||
import Image from "@components/Image.astro";
|
||||
|
||||
const { resource } = Astro.props;
|
||||
const ingredients = resource?.content?.recipeIngredient || [];
|
||||
const instructions = resource?.content?.recipeInstructions || [];
|
||||
---
|
||||
|
||||
<h1 class="text-4xl">{resource?.content?.headline}</h1>
|
||||
<div>
|
||||
{
|
||||
resource?.content?.image && (
|
||||
<Image
|
||||
hash
|
||||
src={{ src: memorium.getImageUrl(resource.content.image) }}
|
||||
alt="Cover for {resource?.content?.name}"
|
||||
class="rounded-2xl overflow-hidden"
|
||||
pictureClass="rounded-2xl"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div set:html={markdownToHtml(resource?.content?.articleBody)} />
|
||||
13
src/components/resources/Display.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import Recipe from "./Recipe.astro";
|
||||
import Article from "./Article.astro";
|
||||
const { resource } = Astro.props;
|
||||
const type = resource?.content?._type ?? "unknown";
|
||||
---
|
||||
|
||||
{type === "Recipe" && <Recipe resource={resource} />}
|
||||
{type === "Article" && <Article resource={resource} />}
|
||||
|
||||
{type === "unknown" && <div>
|
||||
<h3>Unknown resource type</h3>
|
||||
</div>}
|
||||
32
src/components/resources/Recipe.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { markdownToHtml, markdownToText } from "@helpers/markdown";
|
||||
import Image from "@components/Image.astro";
|
||||
|
||||
const { resource } = Astro.props
|
||||
const ingredients = resource?.content?.recipeIngredient || [];
|
||||
const instructions = resource?.content?.recipeInstructions || [];
|
||||
---
|
||||
|
||||
<h1 class="text-4xl">{resource?.content?.name}</h1>
|
||||
<div>
|
||||
{resource?.content?.image && <Image hash src={{src: memorium.getImageUrl(resource.content.image)}} alt="Cover for {resource?.content?.name}" class="rounded-2xl overflow-hidden" pictureClass="rounded-2xl" />}
|
||||
</div>
|
||||
<p>{resource?.content?.description}</p>
|
||||
<h2 class="text-2xl">Ingredients</h2>
|
||||
<ul>
|
||||
{
|
||||
ingredients.map((ingredient) => (
|
||||
<li set:html={markdownToHtml(ingredient)}/>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<h2 class="text-2xl">Steps</h2>
|
||||
<ol>
|
||||
{
|
||||
instructions.map((ingredient) => (
|
||||
<li set:html={markdownToHtml(ingredient)}/>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
@@ -1,4 +0,0 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-af9e72fcc6.gif
|
||||
HostUrl=https://ezgif.com/save/ezgif-1-78ce3365b7.webm
|
||||
@@ -1,4 +0,0 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-74cf771d87.gif
|
||||
HostUrl=https://ezgif.com/save/ezgif-1-93f790072e.webm
|
||||
@@ -1,4 +0,0 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-70f1c50104.gif
|
||||
HostUrl=https://ezgif.com/save/ezgif-1-28f4d917d4.webm
|
||||
@@ -1,3 +0,0 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
HostUrl=about:internet
|
||||
BIN
src/content/projects/silvester/images/cover.png
Normal file
|
After Width: | Height: | Size: 1007 KiB |
BIN
src/content/projects/silvester/images/eif-anim-1.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
src/content/projects/silvester/images/eif-anim-2.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
src/content/projects/silvester/images/eif-anim-3.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
src/content/projects/silvester/images/eif-navigation.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
src/content/projects/silvester/images/eif-teaser.mp4
Normal file
BIN
src/content/projects/silvester/images/hp-1-start.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
src/content/projects/silvester/images/hp-2-start.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/projects/silvester/images/hp-3-need-name.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/projects/silvester/images/hp-4-which-house.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/projects/silvester/images/hp-5-select-house.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/projects/silvester/images/hp-7-invitation-card.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/content/projects/silvester/images/occult-1.png
Normal file
|
After Width: | Height: | Size: 664 KiB |
BIN
src/content/projects/silvester/images/occult-2.png
Normal file
|
After Width: | Height: | Size: 701 KiB |
BIN
src/content/projects/silvester/images/occult-3.png
Normal file
|
After Width: | Height: | Size: 743 KiB |
BIN
src/content/projects/silvester/images/occult-4.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
src/content/projects/silvester/images/party-placeholder.jpg
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
src/content/projects/silvester/images/venice-1-start.png
Normal file
|
After Width: | Height: | Size: 584 KiB |
BIN
src/content/projects/silvester/images/venice-2-mask.png
Normal file
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 287 KiB |
BIN
src/content/projects/silvester/images/venice-5-portrait.png
Normal file
|
After Width: | Height: | Size: 844 KiB |
BIN
src/content/projects/silvester/images/venice-6-gallery.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
91
src/content/projects/silvester/index.mdx
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
date: 2025-10-21
|
||||
title: "Silvester-Partys: Eine Retrospektive"
|
||||
draft: true
|
||||
cover: ./images/cover.png
|
||||
description: "Eine Übersicht über unsere jährlichen Silvester-Partys, von Gatsby bis Okkult."
|
||||
tags: ["event", "webdev", "party", "design"]
|
||||
icon: 🎉
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro"
|
||||
import ImageSlider from "@components/ImageSlider.svelte"
|
||||
import GatsbyPartyScreenshot from "./images/party-placeholder.jpg"
|
||||
import GatsbyPartyPhoto from "./images/party-placeholder.jpg"
|
||||
import HarryPotterPartyScreenshot from "./images/party-placeholder.jpg"
|
||||
import HarryPotterPartyPhoto from "./images/party-placeholder.jpg"
|
||||
import VenicePartyScreenshot from "./images/party-placeholder.jpg"
|
||||
import VenicePartyPhoto from "./images/party-placeholder.jpg"
|
||||
import FashionPartyScreenshot from "./images/party-placeholder.jpg"
|
||||
import FashionPartyPhoto from "./images/party-placeholder.jpg"
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
import videoUrl from "./images/eif-teaser.mp4?url"
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
Seit 2019 veranstaltet meine WG eine Silvester Party. Dafür haben wir jedes Jahr ein besonderes Motto ausgewählt welches dann die Kostüme und die Dekoration bestimmt.
|
||||
Ich hab die Parties immer zum Anlass genommen um mich einmal kreativ auszuleben und habe für jede Party mal einfachere mal komplexere Digital Einladungskarten gestaltet.
|
||||
|
||||
### 2019-2020: The Great Gatsby
|
||||
|
||||
Unsere erste Party stand unter dem Motto "Great Gatsby".
|
||||
|
||||
<ImageSlider title="The Great Gatsby" client:load>
|
||||
<Image src={GatsbyPartyScreenshot} alt="Screenshot der Gatsby Party Visualisierung" />
|
||||
<Image src={GatsbyPartyPhoto} alt="Foto der Gatsby Party" />
|
||||
</ImageSlider>
|
||||
|
||||
### 2022-2023: Harry Potter und der sprechende Hut
|
||||
|
||||
Für den Jahreswechsel 2022-2023 verwandelte sich unsere Wohnung in die Große Halle von Hogwarts. Das Thema war Harry Potter, und wir hatten einen WebGL-basierten sprechenden Hut, der die Gäste interaktiv den Häusern zuordnete. Zusätzlich gab es ein interaktives Harry-Potter-Quiz auf unserer Webseite. Ein visuelles Display zeigte live den Punktestand der einzelnen Häuser an, wobei ein Admin Punkte in Echtzeit hinzufügen oder abziehen konnte.
|
||||
|
||||
Seit 2022 nutzen wir ein Google Sheet als Backend für die Partyorganisation. Jeder Gast gibt bei der Anmeldung eine Wahrscheinlichkeit (in Prozent von 0-100) an, wie wahrscheinlich er oder sie teilnehmen wird. Dies ermöglicht uns eine genauere Vorhersage der Gästezahl.
|
||||
|
||||
<ImageSlider title="Harry Potter und der sprechende Hut" client:load>
|
||||
<Image src={HarryPotterPartyScreenshot} alt="Screenshot der Harry Potter Party Webseite" />
|
||||
<Image src={HarryPotterPartyPhoto} alt="Foto der Harry Potter Party" />
|
||||
<Image src={import("./images/hp-1-start.png")} alt="Harry Potter Party Start" />
|
||||
<Image src={import("images/hp-2-start.png")} alt="Harry Potter Party Start 2" />
|
||||
<Image src={import("images/hp-3-need-name.png")} alt="Harry Potter Party Need Name" />
|
||||
<Image src={import("images/hp-4-which-house.png")} alt="Harry Potter Party Which House" />
|
||||
<Image src={import("images/hp-5-select-house.png")} alt="Harry Potter Party Select House" />
|
||||
<Image src={import("images/hp-6-attendance-probability.png")} alt="Harry Potter Party Attendance Probability" />
|
||||
<Image src={import("images/hp-7-invitation-card.png")} alt="Harry Potter Party Invitation Card" />
|
||||
</ImageSlider>
|
||||
|
||||
### 2023-2024: Venezianischer Maskenball
|
||||
|
||||
Unsere Silvesterparty 2023-2024 entführte unsere Gäste nach Venedig zu einem opulenten Maskenball. Bei der Registrierung auf unserer Webseite erhielt jeder Gast einen einzigartigen Adelstitel und ein passendes Porträt, die dann in einer digitalen Galerie auf der Webseite ausgestellt wurden.
|
||||
|
||||
**Organisation:** Auch hier kam unser Google Sheet Backend zum Einsatz, um die Gästeanmeldungen und Wahrscheinlichkeiten zu verwalten.
|
||||
|
||||
<ImageSlider title="Venezianischer Maskenball" client:load>
|
||||
<Image src={VenicePartyScreenshot} alt="Screenshot der Venezianischer Maskenball Galerie" />
|
||||
<Image src={VenicePartyPhoto} alt="Foto der Venezianischer Maskenball Party" />
|
||||
<Image src={import("images/venice-1-start.png")} alt="Venice Party Start" />
|
||||
<Image src={import("images/venice-2-mask.png")} alt="Venice Party Mask" />
|
||||
<Image src={import("images/venice-3-invitation-test.png")} alt="Venice Party Invitation Test" />
|
||||
<Image src={import("images/venice-4-generate-portrait.png")} alt="Venice Party Generate Portrait" />
|
||||
<Image src={import("images/venice-5-portrait.png")} alt="Venice Party Portrait" />
|
||||
<Image src={import("images/venice-6-gallery.png")} alt="Venice Party Gallery" />
|
||||
</ImageSlider>
|
||||
|
||||
### 2024-2025: Everything is Fashion
|
||||
|
||||
Das Motto für 2024-2025 war "Everything is Fashion". Die Party begann mit einem animierten Intro, das die Gäste auf das modische Thema einstimmte.
|
||||
|
||||
<video src={videoUrl} controls alt="Fashion Party Teaser Video" />
|
||||
|
||||
<ImageSlider title="Everything is Fashion" client:load>
|
||||
<Image src={FashionPartyScreenshot} alt="Screenshot des Fashion Party Intros" />
|
||||
<Image src={FashionPartyPhoto} alt="Foto der Fashion Party" />
|
||||
<Image src={import("images/eif-anim-1.png")} alt="Fashion Party Animation 1" />
|
||||
<Image src={import("images/eif-anim-2.png")} alt="Fashion Party Animation 2" />
|
||||
<Image src={import("images/eif-anim-3.png")} alt="Fashion Party Animation 3" />
|
||||
<Image src={import("images/eif-navigation.png")} alt="Fashion Party Navigation" />
|
||||
</ImageSlider>
|
||||
|
||||
### 2025-2026: Okkult
|
||||
|
||||
Für die kommende Silvesterparty 2025-2026 planen wir ein "Okkult"-Thema, das eine mystische und geheimnisvolle Atmosphäre schaffen wird.
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { rgbaToThumbHash } from "thumbhash";
|
||||
import ExifReader from 'exifreader';
|
||||
import ExifReader from "exifreader";
|
||||
import type { ImageMetadata } from "astro";
|
||||
import sharp from "sharp";
|
||||
|
||||
let s: typeof import("sharp") | undefined;
|
||||
async function getSharp(): Promise<typeof import("sharp") | undefined> {
|
||||
if (s) return s;
|
||||
s = (await import("sharp")).default;
|
||||
return s;
|
||||
}
|
||||
|
||||
export async function generateThumbHash(image: ImageMetadata & { fsPath?: string }) {
|
||||
|
||||
const sharp = await getSharp();
|
||||
if (!sharp) return;
|
||||
|
||||
export async function generateThumbHash(
|
||||
image: ImageMetadata & { fsPath?: string },
|
||||
) {
|
||||
const scaleFactor = 100 / Math.max(image.width, image.height);
|
||||
|
||||
const smallWidth = Math.floor(image.width * scaleFactor);
|
||||
const smallHeight = Math.floor(image.height * scaleFactor);
|
||||
let smallWidth = Math.floor(image.width * scaleFactor);
|
||||
let smallHeight = Math.floor(image.height * scaleFactor);
|
||||
|
||||
try {
|
||||
const smallImg = await sharp(image.fsPath)
|
||||
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath ??
|
||||
image.src;
|
||||
|
||||
if (!imagePath) return;
|
||||
|
||||
let sp;
|
||||
if (imagePath.startsWith("https://") || imagePath.startsWith("http://")) {
|
||||
const res = await fetch(imagePath);
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
sp = sharp(await res.arrayBuffer());
|
||||
} else {
|
||||
sp = sharp(imagePath);
|
||||
}
|
||||
|
||||
if (!smallWidth || !smallHeight) {
|
||||
const meta = await sp.metadata();
|
||||
const scaleFactor = 100 / Math.max(meta.width, meta.height);
|
||||
smallWidth = Math.floor(meta.width * scaleFactor);
|
||||
smallHeight = Math.floor(meta.height * scaleFactor);
|
||||
}
|
||||
|
||||
const smallImg = await sp
|
||||
.resize(smallWidth, smallHeight)
|
||||
.withMetadata()
|
||||
.raw()
|
||||
@@ -30,10 +45,12 @@ export async function generateThumbHash(image: ImageMetadata & { fsPath?: string
|
||||
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
} catch (error) {
|
||||
console.log(`Could not generate thumbhash for ${image.fsPath}`, error)
|
||||
return ""
|
||||
console.log(
|
||||
`Could not generate thumbhash for ${image.fsPath ?? image.src}`,
|
||||
error,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const allowedExif = [
|
||||
@@ -54,12 +71,19 @@ const allowedExif = [
|
||||
|
||||
export async function getExifData(image: ImageMetadata) {
|
||||
if (image.format === "svg") return undefined; // SVGs don't have EXIF data")
|
||||
const sharp = await getSharp();
|
||||
if (!sharp) return;
|
||||
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath;
|
||||
try {
|
||||
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath ??
|
||||
image.src;
|
||||
|
||||
const buffer = await sharp(imagePath).toBuffer();
|
||||
if (!imagePath) return undefined;
|
||||
|
||||
try {
|
||||
let buffer: ArrayBuffer;
|
||||
if (imagePath.startsWith("https://") || imagePath.startsWith("http://")) {
|
||||
const res = await fetch(imagePath);
|
||||
buffer = await res.arrayBuffer();
|
||||
} else {
|
||||
buffer = await sharp(imagePath).toBuffer() as unknown as ArrayBuffer;
|
||||
}
|
||||
|
||||
const tags = await ExifReader.load(buffer, { async: true });
|
||||
|
||||
@@ -74,9 +98,7 @@ export async function getExifData(image: ImageMetadata) {
|
||||
|
||||
return hasExif ? out : undefined;
|
||||
} catch (error) {
|
||||
|
||||
console.log(`Error reading EXIF data from ${imagePath}`, error);
|
||||
return undefined
|
||||
console.log(`Error reading EXIF data from ${JSON.stringify(image)}`, error);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
45
src/helpers/markdown.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
const parser = new MarkdownIt();
|
||||
|
||||
export function readDuration(markdown: string): number | undefined {
|
||||
const words = markdown.split(" ")?.filter(Boolean)?.length;
|
||||
return words && Math.round(words / 250);
|
||||
}
|
||||
|
||||
export function markdownToHtml(markdown: string): string {
|
||||
const md = new MarkdownIt({
|
||||
html: false, // set to true only if you trust the source
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true,
|
||||
});
|
||||
|
||||
// Convert -> sanitize
|
||||
const unsafeHtml = md.render(markdown);
|
||||
return unsafeHtml;
|
||||
}
|
||||
|
||||
export function markdownToText(markdown: string): string {
|
||||
if (!markdown) return "";
|
||||
return parser
|
||||
.render(markdown)
|
||||
.split("\n")
|
||||
.map((str) => str.trim())
|
||||
.map((str) => {
|
||||
return str.replace(/<\/?[^>]+(>|$)/g, "").split("\n");
|
||||
})
|
||||
.flat()
|
||||
.filter((str) =>
|
||||
!str.startsWith("import") &&
|
||||
!str.startsWith("export") &&
|
||||
!str.startsWith("#") &&
|
||||
!str.startsWith("const") &&
|
||||
!str.startsWith("function") &&
|
||||
!str.startsWith("export") &&
|
||||
!str.startsWith("import") &&
|
||||
!str.startsWith("<") &&
|
||||
!str.startsWith("let") &&
|
||||
str.length > 0
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
const parser = new MarkdownIt();
|
||||
|
||||
export default function markdownToText(markdown: string): string {
|
||||
return parser
|
||||
.render(markdown)
|
||||
.split('\n')
|
||||
.map((str) => str.trim())
|
||||
.map((str) => {
|
||||
return str.replace(/<\/?[^>]+(>|$)/g, '').split('\n');
|
||||
})
|
||||
.flat()
|
||||
.filter((str) => !str.startsWith("import")
|
||||
&& !str.startsWith("export")
|
||||
&& !str.startsWith("#")
|
||||
&& !str.startsWith("const")
|
||||
&& !str.startsWith("function")
|
||||
&& !str.startsWith("export")
|
||||
&& !str.startsWith("import")
|
||||
&& !str.startsWith("<")
|
||||
&& !str.startsWith("let")
|
||||
&& str.length > 0
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
@@ -1,10 +1,59 @@
|
||||
export async function listResource(id: string): Promise<any[]> {
|
||||
export type MemoriumFile = {
|
||||
type: "file";
|
||||
name: string;
|
||||
path: string;
|
||||
modTime: string;
|
||||
mime: string;
|
||||
size: string;
|
||||
content: any;
|
||||
};
|
||||
|
||||
export type MemoriumDir = {
|
||||
type: "dir";
|
||||
name: string;
|
||||
path: string;
|
||||
modTime: string;
|
||||
mime: string;
|
||||
size: string;
|
||||
content: MemoriumEntry[];
|
||||
};
|
||||
|
||||
export type MemoriumEntry = MemoriumFile | MemoriumDir;
|
||||
|
||||
export async function listResource(
|
||||
id: string,
|
||||
): Promise<MemoriumEntry | undefined> {
|
||||
const url = `https://marka.max-richter.dev/resources/${id}`;
|
||||
console.log("Fetching: ", url);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:8080/resources?name=${id}`,
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return []
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
if (json.type == "dir") {
|
||||
return {
|
||||
...json,
|
||||
content: json.content.filter((res) =>
|
||||
res.mime === "application/markdown"
|
||||
),
|
||||
};
|
||||
}
|
||||
return json;
|
||||
}
|
||||
} catch (_e) {
|
||||
console.log("Failed to get: ", url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function getImageUrl(input: string): string {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
if (input.startsWith("https://") || input.startsWith("http://")) {
|
||||
return input;
|
||||
}
|
||||
if (input.startsWith("/")) {
|
||||
return `https://marka.max-richter.dev${input}`;
|
||||
}
|
||||
return `https://marka.max-richter.dev/${input}`;
|
||||
}
|
||||
|
||||
59
src/pages/resources/[resourceType]/[resourceName].astro
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import { useTranslatedPath } from "@i18n/utils";
|
||||
import ResourceDisplay from "@components/resources/Display.astro";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { resources as resourceTypes } from "../resources.ts";
|
||||
import { markdownToText } from "@helpers/markdown";
|
||||
import Image from "@components/Image.astro";
|
||||
|
||||
const { resourceType, resourceName } = Astro?.params;
|
||||
|
||||
const path = useTranslatedPath(Astro.url);
|
||||
|
||||
export async function getStaticPaths() {
|
||||
try {
|
||||
const paths = await Promise.all(
|
||||
resourceTypes.map(async (resourceType) => {
|
||||
const resources = await memorium.listResource(resourceType.id);
|
||||
return resources?.content?.map((res: any) => {
|
||||
return {
|
||||
params: {
|
||||
resourceType: resourceType.id,
|
||||
resourceName: res.name.replace(/\.md$/, ""),
|
||||
},
|
||||
};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return paths.flat().filter(Boolean);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const resource = await memorium.listResource(
|
||||
`${resourceType}/${resourceName}.md`,
|
||||
);
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
<div class="top-info flex items-center place-content-between m-y-2">
|
||||
<a
|
||||
class="flex items-center gap-1 opacity-50"
|
||||
href={path("/resources/" + resourceType)}>
|
||||
<span class="i-tabler-arrow-left"></span> back
|
||||
</a>
|
||||
<div class="date opacity-50">
|
||||
{
|
||||
resource?.content.datePublished?.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<ResourceDisplay resource={resource} />
|
||||
</Layout>
|
||||
59
src/pages/resources/[resourceType]/index.astro
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import HeroCard from "@components/HeroCard.astro";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { resources as resourceTypes } from "../resources.ts";
|
||||
import { markdownToText } from "@helpers/markdown";
|
||||
|
||||
const { resourceType } = Astro.params;
|
||||
|
||||
async function safeGetResource(resType) {
|
||||
try {
|
||||
return await memorium.listResource(resourceType);
|
||||
} catch (error) {
|
||||
return { content: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const resources = await safeGetResource(resourceType);
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return resourceTypes.map((type: any) => {
|
||||
return {
|
||||
params: {
|
||||
resourceType: type.id,
|
||||
resourceName: "Recipe",
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function isValidResource(res) {
|
||||
if (res?.content?.name) return true;
|
||||
if (res?.content?.headline) return true;
|
||||
return false;
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
{
|
||||
resources.content
|
||||
.filter((res) => res && res?.content)
|
||||
.map((resource: any) => (
|
||||
<HeroCard
|
||||
post={{
|
||||
collection: "resources/" + resourceType,
|
||||
id: resource.name.replace(/\.md$/, ""),
|
||||
data: {
|
||||
title: resource.content.name ?? resource.content.headline,
|
||||
date: resource?.content?.datePublished,
|
||||
cover: {
|
||||
src: memorium.getImageUrl(resource.content.image),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Layout>
|
||||
</Layout>
|
||||
@@ -1,64 +1,9 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import HeroCard from "@components/HeroCard.astro";
|
||||
|
||||
const collection = "resources";
|
||||
|
||||
const wiki = {
|
||||
id: "wiki",
|
||||
collection,
|
||||
body: "My knowledge base",
|
||||
data: {
|
||||
title: "Wiki",
|
||||
icon: "🧠",
|
||||
},
|
||||
};
|
||||
|
||||
const articles = {
|
||||
id: "articles",
|
||||
collection,
|
||||
body: "Articles saved",
|
||||
data: {
|
||||
title: "Articles",
|
||||
icon: "📰",
|
||||
},
|
||||
};
|
||||
|
||||
const recipes = {
|
||||
id: "recipes",
|
||||
collection,
|
||||
body: "Recipes",
|
||||
data: {
|
||||
title: "Recipes",
|
||||
icon: "🍲",
|
||||
},
|
||||
};
|
||||
|
||||
const movies = {
|
||||
id: "movies",
|
||||
collection,
|
||||
body: "Movies",
|
||||
data: {
|
||||
title: "Movies",
|
||||
icon: "🎥",
|
||||
},
|
||||
};
|
||||
|
||||
const series = {
|
||||
id: "series",
|
||||
collection,
|
||||
body: "Series",
|
||||
data: {
|
||||
title: "Series",
|
||||
icon: "📺",
|
||||
},
|
||||
};
|
||||
import { resources } from "./resources.ts";
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
<HeroCard post={wiki} />
|
||||
<HeroCard post={recipes} />
|
||||
<HeroCard post={articles} />
|
||||
<HeroCard post={movies} />
|
||||
<HeroCard post={series} />
|
||||
{resources.map((resource) => <HeroCard post={resource} />)}
|
||||
</Layout>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const movieReviews = await memorium.listResource("Media/movies/*");
|
||||
|
||||
const paths = movieReviews.map((review: any) => {
|
||||
return {
|
||||
params: {
|
||||
movieName: review.identifier
|
||||
.replace("Media/movies/", "")
|
||||
.replace(/\.md$/, ""),
|
||||
},
|
||||
};
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
|
||||
const reviews = await memorium.listResource(
|
||||
//@ts-ignore
|
||||
`Media/movies/${Astro.params.movieName}.md`,
|
||||
);
|
||||
|
||||
if (reviews.length === 0) {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
statusText: "Not found",
|
||||
});
|
||||
}
|
||||
const review = reviews[0];
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
<h1>{review.itemReviewed?.name}</h1>
|
||||
<p>{review.reviewBody}</p>
|
||||
<!-- <pre><code>{JSON.stringify(review, null, 2)}</code></pre> -->
|
||||
</Layout>
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import HeroCard from "@components/HeroCard.astro";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
|
||||
const movieReviews = await memorium.listResource("Media/movies/*");
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
{
|
||||
movieReviews.map((review: any) => (
|
||||
<HeroCard
|
||||
post={{
|
||||
collection: "resources/movies",
|
||||
id: review.identifier
|
||||
.replace("Media/movies/", "")
|
||||
.replace(/\.md$/, ""),
|
||||
data: {
|
||||
title: review.itemReviewed.name,
|
||||
description: review.reviewBody,
|
||||
},
|
||||
body: review.reviewBody,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Layout>
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import { useTranslatedPath } from "@i18n/utils";
|
||||
import markdownToText from "@helpers/markdownToText";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
|
||||
const path = useTranslatedPath(Astro.url);
|
||||
|
||||
const collection = "resources/recipes";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
|
||||
const recipes = await memorium.listResource("Recipes/*");
|
||||
|
||||
const paths = recipes.map((recipe: any) => {
|
||||
return {
|
||||
params: {
|
||||
recipeName: recipe.identifier
|
||||
.replace("Recipes/", "")
|
||||
.replace(/\.md$/, ""),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
const recipes = await memorium.listResource(
|
||||
//@ts-ignore
|
||||
`Recipes/${Astro.params.recipeName}.md`,
|
||||
);
|
||||
if (recipes.length === 0) {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
statusText: "Not found",
|
||||
});
|
||||
}
|
||||
const recipe = recipes[0];
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
<div class="top-info flex items-center place-content-between m-y-2">
|
||||
<a class="flex items-center gap-1 opacity-50" href={path("/" + collection)}>
|
||||
<span class="i-tabler-arrow-left"></span> back
|
||||
</a>
|
||||
<div class="date opacity-50">
|
||||
{
|
||||
recipe.date?.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>{recipe.name}</h1>
|
||||
|
||||
<h3>Ingredients</h3>
|
||||
<ol>
|
||||
{
|
||||
recipe.recipeIngredient?.map((ingredient: any) => (
|
||||
<li>{markdownToText(ingredient)}</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
|
||||
<h3>Instructions</h3>
|
||||
<p>{recipe.recipeInstructions}</p>
|
||||
</Layout>
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import HeroCard from "@components/HeroCard.astro";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
|
||||
const recipes = await memorium.listResource("Recipes/*");
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
{
|
||||
recipes.map((recipe: any) => (
|
||||
<HeroCard
|
||||
post={{
|
||||
collection: "resources/recipes",
|
||||
id: recipe.identifier.replace("Recipes/", "").replace(/\.md$/, ""),
|
||||
data: {
|
||||
title: recipe.name,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Layout>
|
||||
60
src/pages/resources/resources.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
const collection = "resources";
|
||||
|
||||
type Resource = {
|
||||
id: string;
|
||||
collection: string;
|
||||
data: {
|
||||
title: string;
|
||||
icon: string;
|
||||
};
|
||||
};
|
||||
|
||||
// const wiki = {
|
||||
// id: "wiki",
|
||||
// collection,
|
||||
// body: "My knowledge base",
|
||||
// data: {
|
||||
// title: "Wiki",
|
||||
// icon: "🧠",
|
||||
// },
|
||||
// };
|
||||
|
||||
const articles = {
|
||||
id: "articles",
|
||||
collection,
|
||||
data: {
|
||||
title: "Articles",
|
||||
icon: "📰",
|
||||
},
|
||||
};
|
||||
|
||||
const recipes = {
|
||||
id: "recipes",
|
||||
collection,
|
||||
data: {
|
||||
title: "Recipes",
|
||||
icon: "🍲",
|
||||
},
|
||||
};
|
||||
|
||||
// const movies = {
|
||||
// id: "Movies",
|
||||
// collection,
|
||||
// body: "Movies",
|
||||
// data: {
|
||||
// title: "Movies",
|
||||
// icon: "🎥",
|
||||
// },
|
||||
// };
|
||||
|
||||
// const series = {
|
||||
// id: "Series",
|
||||
// collection,
|
||||
// body: "Series",
|
||||
// data: {
|
||||
// title: "Series",
|
||||
// icon: "📺",
|
||||
// },
|
||||
// };
|
||||
|
||||
export const resources: Resource[] = [recipes, articles];
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import { useTranslatedPath } from "@i18n/utils";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
|
||||
const collection = "resources/series";
|
||||
|
||||
const path = useTranslatedPath(Astro.url);
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const seriesReviews = await memorium.listResource("Media/series/*");
|
||||
|
||||
const paths = seriesReviews.map((review: any) => {
|
||||
return {
|
||||
params: {
|
||||
seriesName: review.identifier
|
||||
.replace("Media/series/", "")
|
||||
.replace(/\.md$/, ""),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
const reviews = await memorium.listResource(
|
||||
//@ts-ignore
|
||||
`Media/series/${Astro.params.seriesName}.md`,
|
||||
);
|
||||
if (reviews.length === 0) {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
statusText: "Not found",
|
||||
});
|
||||
}
|
||||
const review = reviews[0];
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
<div class="top-info flex items-center place-content-between m-y-2">
|
||||
<a class="flex items-center gap-1 opacity-50" href={path("/" + collection)}>
|
||||
<span class="i-tabler-arrow-left"></span> back
|
||||
</a>
|
||||
<div class="date opacity-50">
|
||||
{
|
||||
review.date?.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>{review.itemReviewed?.name}</h1>
|
||||
<p>{review.reviewBody}</p>
|
||||
</Layout>
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import HeroCard from "@components/HeroCard.astro";
|
||||
import * as memorium from "@helpers/memorium";
|
||||
|
||||
const seriesReviewes = await memorium.listResource("Media/series/*");
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
{
|
||||
seriesReviewes.map((review: any) => (
|
||||
<HeroCard
|
||||
post={{
|
||||
collection: "resources/series",
|
||||
id: review.identifier
|
||||
.replace("Media/series/", "")
|
||||
.replace(/\.md$/, ""),
|
||||
data: {
|
||||
title: review.itemReviewed.name,
|
||||
description: review.reviewBody,
|
||||
},
|
||||
body: review.reviewBody,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Layout>
|
||||
@@ -1,5 +1,10 @@
|
||||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
import { vitePreprocess } from "@astrojs/svelte";
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"vite-plugin-glsl/ext",
|
||||
"unplugin-icons/types"
|
||||
"unplugin-icons/types",
|
||||
"svelte-gestures/globals"
|
||||
],
|
||||
"paths": {
|
||||
"@components/*": [
|
||||
|
||||