Compare commits

...

12 Commits

Author SHA1 Message Date
Max Richter
24d01e44b0 chore: some upgrade
All checks were successful
Deploy to SFTP Server / build (push) Successful in 22m39s
2025-10-28 17:36:09 +01:00
Max Richter
6545a25741 fix: some stuff
All checks were successful
Deploy to SFTP Server / build (push) Successful in 24m33s
2025-10-28 16:04:50 +01:00
Max Richter
c527a13c54 fix: actually transition image heights in sliders
Some checks failed
Deploy to SFTP Server / build (push) Failing after 2m22s
2025-10-28 16:02:13 +01:00
Max Richter
c3299868c0 fix: actually check image sizes
Some checks failed
Deploy to SFTP Server / build (push) Failing after 2m24s
2025-10-28 15:35:43 +01:00
Max Richter
71074a8b49 fix: remove some type errors
All checks were successful
Deploy to SFTP Server / build (push) Successful in 17m7s
2025-10-28 15:14:21 +01:00
Max Richter
ea5c35ee85 fix: some minor layout improvements
Some checks failed
Deploy to SFTP Server / build (push) Failing after 2m29s
2025-10-28 14:55:45 +01:00
6b8b032832 fix: mobile horizontal scrolling
All checks were successful
Deploy to SFTP Server / build (push) Successful in 13m10s
2025-10-27 15:49:25 +01:00
Max Richter
e314d6edcb chore: pnpm upgrade
All checks were successful
Deploy to SFTP Server / build (push) Successful in 13m6s
2025-10-27 00:14:23 +01:00
Max Richter
3263ab9123 feat: initial client side searching for resources
All checks were successful
Deploy to SFTP Server / build (push) Successful in 14m30s
2025-10-27 00:04:02 +01:00
Max Richter
207d5998da feat: track webm with lfs
All checks were successful
Deploy to SFTP Server / build (push) Successful in 14m35s
2025-10-26 22:48:02 +01:00
Max Richter
31fe7e9a37 fix: untrack mdx from src/content/photos 2025-10-26 22:47:35 +01:00
Max Richter
6f59613edf fix: remove git lfs track from content/photos
Some checks failed
Deploy to SFTP Server / build (push) Failing after 2m5s
2025-10-25 17:18:34 +02:00
43 changed files with 2203 additions and 822 deletions

4
.gitattributes vendored
View File

@@ -4,4 +4,6 @@
*.woff2 filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
src/content/photos/** filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.JPEG filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text

View File

@@ -42,9 +42,7 @@ export default defineConfig({
],
server: {
watch: {
// Customize watch behavior to reduce file watchers
ignored: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
usePolling: process.env.NODE_ENV === "production",
},
},
},

View File

@@ -11,15 +11,15 @@
},
"dependencies": {
"@astrojs/check": "^0.9.5",
"@astrojs/mdx": "^4.3.7",
"@astrojs/svelte": "^7.2.0",
"@astrojs/mdx": "^4.3.9",
"@astrojs/svelte": "^7.2.1",
"@astrojs/tailwind": "^6.0.2",
"astro": "^5.14.8",
"astro": "^5.15.2",
"astro-i18n-aut": "^0.7.3",
"exifreader": "^4.32.0",
"svelte": "^5.39.8",
"svelte": "^5.42.3",
"svelte-gestures": "^5.2.2",
"tailwindcss": "^4.1.14",
"tailwindcss": "^4.1.16",
"thumbhash": "^0.1.1",
"typescript": "^5.9.3"
},
@@ -27,16 +27,16 @@
"@astrojs/sitemap": "^3.6.0",
"@iconify-json/tabler": "^1.2.23",
"@types/markdown-it": "^14.1.2",
"@unocss/preset-icons": "^66.5.2",
"@unocss/reset": "^66.5.2",
"@unocss/preset-icons": "^66.5.4",
"@unocss/reset": "^66.5.4",
"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.4",
"unocss": "^66.5.2",
"unplugin-icons": "^22.4.2",
"unocss": "^66.5.4",
"unplugin-icons": "^22.5.0",
"vite-plugin-glsl": "^1.5.4"
}
}

1273
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { onMount } from "svelte";
let searchTerm = "";
let staticContainer: HTMLElement | null;
onMount(async () => {
staticContainer = document.getElementById("resource-list-static");
});
function search() {
if (!staticContainer) return;
const lowerCaseSearchTerm = searchTerm.toLowerCase().trim();
if (!lowerCaseSearchTerm) {
[...staticContainer?.children].forEach((element: HTMLElement) => {
element.style.display = "";
});
return;
}
for (const element of staticContainer?.children) {
const el = element as HTMLElement;
const isMatch = el.dataset.searchTerm?.includes(lowerCaseSearchTerm);
if (isMatch) {
el.style.display = "";
} else {
el.style.display = "none";
}
}
}
$: search(searchTerm);
</script>
<div class="search-wrapper my-4 noise relative">
<input
type="text"
bind:value={searchTerm}
placeholder={`Search...`}
class="w-full p-2 gradient border-1 border-neutral rounded noise" />
</div>

View File

@@ -25,10 +25,14 @@ const translatePath = useTranslatedPath(Astro.url);
const t = useTranslations(Astro.url);
const link = translatePath(`/${collection}/${id.split("/")[0]}`);
const image = cover as unknown;
const hasCover = typeof image === "string" ? !!image?.length : !!cover?.src;
---
<Card
classes={`grid gradient border-1 border-neutral overflow-hidden ${cover ? "grid-rows-[200px_1fr] xs:grid-rows-none xs:grid-cols-[1fr_200px]" : ""}`}>
classes={`grid gradient border-1 border-neutral overflow-hidden ${hasCover ? "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 || rating !== undefined) && (
@@ -57,14 +61,15 @@ const link = translatePath(`/${collection}/${id.split("/")[0]}`);
<Card.ReadMoreButton link={link} text={t("read-more")} />
</Card.Content>
{
cover && (
hasCover && (
<a href={link}>
<Image
hash
loader={false}
src={cover as ImageMetadata}
alt={"cover for " + title}
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
class="h-full right-0 object-cover object-center rounded-none border-l border-neutral"
pictureClass="h-full"
thumbnail
/>
</a>

View File

@@ -15,23 +15,25 @@ interface Props {
thumbnail?: boolean;
}
async function checkImage(image: ImageMetadata) {
async function checkImage(
image: ImageMetadata,
): Promise<{ height: number; width: number } | undefined> {
const src = typeof image === "string" ? image : image.src;
if (!src) return false;
if (!src) return;
try {
if (src.startsWith("/@fs") || src.startsWith("/_astro")) return true;
if (src.startsWith("/@fs") || src.startsWith("/_astro")) return image;
const res = await inferRemoteSize(src);
if (res.format) {
image.format = res.format;
return true;
return res;
} else {
console.log("Failed to load: ", src);
}
return false;
return;
} catch (err) {
console.log("\n");
console.log("Failed to fetch: ", src);
return false;
return;
}
}
@@ -70,7 +72,7 @@ const definedSizes = [
];
const sizes = thumbnail
? [definedSizes[0]]
? [definedSizes[1]]
: definedSizes.filter((size) => !maxWidth || size.width <= maxWidth);
---
@@ -81,16 +83,19 @@ const sizes = thumbnail
alt={alt}
data-thumbhash={thumbhash}
data-exif={JSON.stringify(exif)}
inferSize={true}
width={imageOk?.width}
height={imageOk?.height}
pictureAttributes={{
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
}}
class={`${Astro.props.class} h-full w-full`}
class={`${Astro.props.class} w-full`}
widths={sizes.map((size) => size.width)}
sizes={sizes
.map((size) => `${size.media || "100vw"} ${size.width}px`)
.join(", ")}>
<slot />
</AstroImage>
) : undefined
) : (
<div>{JSON.stringify({ "imageOk":imageOk, image })}</div>
)
}

View File

@@ -8,11 +8,14 @@
let height: number;
let loaded = false;
function hide(img: HTMLPictureElement) {
function hide(index: number) {
const img = images[index];
img.classList.remove("active");
}
function show(img: HTMLPictureElement) {
const heightCache = [];
function show(index: number) {
const img = images[index];
img.classList.add("active");
const _img = img.querySelector("img") || img;
if (!_img) return;
@@ -21,9 +24,12 @@
_img.style.opacity = "1";
});
altText = _img["alt"] ?? _img.getAttribute("alt") ?? "";
height = _img.getBoundingClientRect().height;
if (heightCache[index]) {
height = heightCache[index];
}
setTimeout(() => {
height = _img.getBoundingClientRect().height;
height = heightCache[index] ?? _img.getBoundingClientRect().height;
heightCache[index] = height;
}, 100);
}
@@ -31,16 +37,16 @@
function setIndex(i: number) {
if (i < 0) i = images.length - 1;
if (i >= images.length) i = 0;
hide(images[index]);
hide(index);
index = i;
show(images[index]);
show(index);
}
$: if (slot && !images?.length) {
images = Array.from(slot.querySelectorAll("picture"));
if (images?.length) {
images.forEach(hide);
show(images[index]);
images.forEach((_, i) => hide(i));
show(index);
images[index].onload = () => {
loaded = true;
height = images[index].getBoundingClientRect().height;
@@ -54,29 +60,27 @@
<div
class="wrapper grid overflow-hidden rounded-xl border border-neutral"
class:title
class:title={true}
class:not-loaded={!loaded}
class:loaded
style={`--height:${height}px`}>
{#if title}
<div class="flex items-center p-x-4 p-y-6 bg justify-between">
<h3>{title}</h3>
<div class="flex items-center p-x-4 p-y-6 bg justify-between">
<h3>{title}</h3>
<div
class="overflow-hidden rounded-md bg-light gap-2 flex p-2 border border-light">
<button
class="flex-1 i-tabler-arrow-left"
aria-label="previous image"
on:click={() => setIndex(index - 1)}></button>
<button
class="flex-1 i-tabler-arrow-right"
aria-label="next image"
on:click={() => setIndex(index + 1)}></button>
</div>
<div
class="overflow-hidden rounded-md bg-light gap-2 flex p-2 border border-light">
<button
class="flex-1 i-tabler-arrow-left"
aria-label="previous image"
on:click={() => setIndex(index - 1)}></button>
<button
class="flex-1 i-tabler-arrow-right"
aria-label="next image"
on:click={() => setIndex(index + 1)}></button>
</div>
{/if}
</div>
<div class="images border-t-1 border-b-1 border-neutral" bind:this={slot}>
<div class="images border-block border-neutral" bind:this={slot}>
<slot />
</div>
<div class="px-4 flex items-center place-content-between bg">

View File

@@ -8,9 +8,9 @@
typeof d === "string" ? new Date(d) : d;
const iso = (d: string | Date) => {
if(!d) return ""
if (!d) return "";
const v = toDate(d);
if(!v?.getTime) return ""
if (!v?.getTime) return "";
return isNaN(v.getTime()) ? "" : v.toISOString();
};
@@ -34,22 +34,23 @@
};
</script>
<div class="flex gap-3 wrapper">
{#if rating}
<div class="text-sm bg-light">{formatRating(rating)}</div>
{/if}
{#if date}
<time class="text-sm bg-light" datetime={iso(date)}
>{formatDate(date)}</time>
{/if}
{#if readDuration > 1}
<div class="text-sm bg-light">{readDuration} mins read</div>
{/if}
{#if author}
<div class="text-sm bg-light">{author}</div>
{/if}
</div>
{#if rating || date || readDuration || author}
<div class="flex flex-wrap gap-3 wrapper">
{#if rating}
<div class="text-sm bg-light">{formatRating(rating)}</div>
{/if}
{#if date}
<time class="text-sm bg-light" datetime={iso(date)}
>{formatDate(date)}</time>
{/if}
{#if readDuration > 1}
<div class="text-sm bg-light">{readDuration} mins read</div>
{/if}
{#if author}
<div class="text-sm bg-light">{author}</div>
{/if}
</div>
{/if}
<style>
.wrapper > * {
@@ -57,5 +58,6 @@
border-radius: 14px;
font-size: 11px;
opacity: 0.7;
white-space: nowrap;
}
</style>

View File

@@ -6,6 +6,6 @@
<a
href={link}
data-astro-prefetch
class="bg-light p-2 text-s rounded-md px-4 flex flex-0 items-center gap-2 w-fit"
class="mt-auto bg-light p-2 text-s rounded-md px-4 flex flex-0 items-center gap-2 w-fit"
>{text}<span class="i-tabler-arrow-right inline-block w-4 h-4"></span>
</a>

View File

@@ -13,12 +13,17 @@ const { resource } = Astro.props;
resource?.content?.image && (
<Image
hash
src={{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata}
src={
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
}
alt="Cover for {resource?.content?.name}"
class="rounded-2xl overflow-hidden"
pictureClass="rounded-2xl"
pictureClass="rounded-2xl box-shadow"
/>
)
}
</div>
<div set:html={markdownToHtml(resource?.content?.articleBody)} />
<div
class="flex flex-col gap-4"
set:html={markdownToHtml(resource?.content?.articleBody)}
/>

View File

@@ -4,30 +4,45 @@ import { markdownToHtml } from "@helpers/markdown";
import Image from "@components/Image.astro";
import type { ImageMetadata } from "astro";
const { resource } = Astro.props
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)} as ImageMetadata} 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: string) => (
<li set:html={markdownToHtml(ingredient)}/>
resource?.content?.image && (
<Image
hash
src={
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
}
alt="Cover for {resource?.content?.name}"
class="rounded-2xl overflow-hidden"
pictureClass="rounded-2xl box-shadow"
/>
)
}
</div>
<div
class="flex flex-col gap-4 px-4"
set:html={markdownToHtml(resource?.content?.description ?? "")}
/>
<h2 class="text-2xl px-4">Ingredients</h2>
<ul class="list-disc px-10">
{
ingredients.filter((s:string) => !!s?.length).map((ingredient: string) => (
<li set:html={markdownToHtml(ingredient)} />
))
}
</ul>
<h2 class="text-2xl">Steps</h2>
<ol>
<h2 class="text-2xl px-4">Steps</h2>
<ol class="list-decimal px-10">
{
instructions.map((ingredient: string) => (
<li set:html={markdownToHtml(ingredient)}/>
instructions.filter((s:string) => !!s?.length).map((ingredient: string) => (
<li set:html={markdownToHtml(ingredient)} />
))
}
</ol>

View File

@@ -24,7 +24,7 @@ function formatRating(rating: string | number) {
}
alt="Cover for {resource?.content?.name}"
class="rounded-2xl overflow-hidden"
pictureClass="rounded-2xl w-1/2 mr-4 mb-4 float-left"
pictureClass="rounded-2xl w-1/2 mr-4 mb-4 float-left box-shadow"
/>
)
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,26 +23,33 @@ export type MemoriumEntry = MemoriumFile | MemoriumDir;
const SERVER_URL = "https://marka.max-richter.dev";
//const SERVER_URL = "http://localhost:8080";
const cache = {};
export async function listResource(
id: string,
): Promise<MemoriumEntry | undefined> {
const url = `${SERVER_URL}/resources/${id}`;
if (cache[url]) return cache[url];
try {
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (response.ok) {
const json = await response.json();
if (json.type == "dir") {
return {
const res = {
...json,
content: json.content.filter((res: MemoriumEntry) =>
res.mime === "application/markdown"
),
};
cache[url] = res;
return res;
}
cache[url] = json;
return json;
}
} catch (_e) {
console.log("Failed to get: ", url);
cache[url] = undefined;
return;
}
}

View File

@@ -2,6 +2,10 @@ p {
font-size: 16px;
}
body {
overflow-x: hidden;
}
h1,
h2,
h3 {
@@ -103,14 +107,23 @@ picture.thumb-loading::before {
header::before {
content: "";
position: absolute;
width: 10px;
height: 10px;
width: 20px;
height: 20px;
right: 3rem;
bottom: -10px;
transform: rotate(90deg);
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 249 249' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M248.5 0H0V249C0.268799 111.435 111.423 0 248.5 0Z' fill='white'/%3E%3C/svg%3E");
bottom: -20px;
background: var(--background-dark);
mask-image: radial-gradient(circle at bottom left,
transparent 19.5px,
rgba(0, 0, 0, .5) 20px,
#000 20.5px);
box-shadow: 0px 0px 10px red;
}
.dark header::before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 249 249' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M248.5 0H0V249C0.268799 111.435 111.423 0 248.5 0Z' fill='%2316161e'/%3E%3C/svg%3E");
.box-shadow {
box-shadow:
0px 1px 1px rgba(3, 7, 18, 0.02),
0px 5px 4px rgba(3, 7, 18, 0.03),
0px 12px 9px rgba(3, 7, 18, 0.05),
0px 20px 15px rgba(3, 7, 18, 0.06),
0px 32px 24px rgba(3, 7, 18, 0.08);
}

View File

@@ -1,5 +0,0 @@
---
import Layout from "@layouts/Layout.astro";
---
<Layout title="Home"> Sup people :) </Layout>

View File

@@ -3,7 +3,7 @@ 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 { resources as resourceTypes } from "@content/resources.ts";
const { resourceType, resourceName } = Astro?.params;
@@ -14,7 +14,6 @@ export async function getStaticPaths() {
const paths = await Promise.all(
resourceTypes.map(async (resourceType) => {
const resources = await memorium.listResource(resourceType.id);
console.log({resources:resources?.content[0]})
return resources?.content?.map((res: any) => {
return {
params: {
@@ -48,7 +47,7 @@ const resource = await memorium.listResource(
{
resource?.content?.url && (
<a class="flex gap-1 items-center" href={resource?.content?.url}>
link
source link
<span class="inline-block w-3 h-3 i-tabler-external-link" />
</a>
)

View File

@@ -2,8 +2,9 @@
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 { resources as resourceTypes } from "@content/resources.ts";
import { useTranslations } from "@i18n/utils";
import ClientSearch from "@components/ClientSearch.svelte";
const { resourceType } = Astro.params;
@@ -32,33 +33,55 @@ export async function getStaticPaths() {
function isValidResource(res: any) {
return !!res?.content?._type;
}
function buildSearchTerm(res: any) {
if (!res) return "";
return Object.keys(res)
.map((key) => {
if (key.startsWith("_")) return;
const value = res[key];
if (Array.isArray(value)) return value.join(" ");
if (typeof value === "object") return buildSearchTerm(value);
return value;
})
.filter((s) => !!s?.length)
.join(" ")
.toLowerCase();
}
---
<Layout title="Max Richter">
<h1 class="text-4xl mb-4">{t(resourceType)}</h1>
<p>{t(`${resourceType as "articles"}.description`)}</p>
{
resources.content
.filter((res: any) => isValidResource(res))
.map((resource: any) => (
<HeroCard
post={{
collection: "resources/" + resourceType,
id: resource.name.replace(/\.md$/, ""),
data: {
title:
resource?.content?.name ??
resource?.content?.headline ??
resource.content?.itemReviewed?.name,
date: resource?.content?.datePublished,
author: resource?.content?.author?.name,
rating: resource?.content?.reviewRating?.ratingValue,
cover: {
src: memorium.getImageUrl(resource.content.image),
},
},
}}
/>
))
}
<ClientSearch resourceType={resourceType} client:load />
<div id="resource-list-static" class="flex flex-col gap-6">
{
resources?.content
.filter((res: any) => isValidResource(res))
.map((resource: any) => (
<div data-search-term={buildSearchTerm(resource?.content)}>
<HeroCard
post={{
collection: "resources/" + resourceType,
id: resource.name.replace(/\.md$/, ""),
data: {
title:
resource?.content?.name ??
resource?.content?.headline ??
resource.content?.itemReviewed?.name,
date: resource?.content?.datePublished,
author: resource?.content?.author?.name,
rating: resource?.content?.reviewRating?.ratingValue,
cover: {
src: memorium.getImageUrl(resource.content.image),
},
},
}}
/>
</div>
))
}
</div>
</Layout>

View File

@@ -1,18 +1,53 @@
---
import Layout from "@layouts/Layout.astro";
import HeroCard from "@components/HeroCard.astro";
import { resources } from "./resources.ts";
import {useTranslations} from "@i18n/utils";
import { resources } from "@content/resources.ts";
import { useTranslations } from "@i18n/utils";
import * as memorium from "@helpers/memorium";
const t = useTranslations(Astro.url);
async function getCoverImage(resourceName: string) {
const resources = await memorium.listResource(resourceName);
if (!resources?.content) return "";
let amount = 0;
while (true) {
amount++;
const randomResource =
resources?.content[Math.floor(Math.random() * resources?.content.length)];
const cover =
randomResource?.content?.cover || randomResource?.content?.image;
if (cover) {
if (cover.startsWith("https://") || cover.startsWith("http://")) {
continue;
}
return `https://marka.max-richter.dev/${cover}`;
}
if (amount > 50) {
break;
}
}
}
---
<Layout title="Max Richter">
{resources.map((resource) =>
<HeroCard post={{
...resource,
body: t(`${resource.id}.description`),
data: {
cover:{src:resource.cover}, title: t(resource.id)}}} />
)}
{
await Promise.all(
resources.map(async (resource) => {
const cover = await getCoverImage(resource.id);
return (
<HeroCard
post={{
...resource,
body: t(`${resource.id}.description`),
data: {
cover: { src: cover },
title: t(resource.id),
},
}}
/>
);
}),
)
}
</Layout>