Compare commits
2 Commits
main
...
712c6fe11e
Author | SHA1 | Date | |
---|---|---|---|
|
712c6fe11e | ||
|
8bd21a9a23 |
1
.github/workflows/default.yaml
vendored
1
.github/workflows/default.yaml
vendored
@@ -53,6 +53,7 @@ jobs:
|
||||
|
||||
- name: 🏗️ Build site
|
||||
run: |
|
||||
# Install dependencies, build, and generate site output
|
||||
pnpm i && pnpm build
|
||||
|
||||
- name: 🔑 Configure rclone
|
||||
|
@@ -17,12 +17,13 @@ const locales = {
|
||||
const DEFAULT_LAYOUT = '@layouts/Post.astro';
|
||||
|
||||
function setDefaultLayout() {
|
||||
return function(_, file) {
|
||||
return function (_, file) {
|
||||
const { frontmatter } = file.data.astro;
|
||||
if (!frontmatter.layout) frontmatter.layout = DEFAULT_LAYOUT;
|
||||
};
|
||||
}
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://max-richter.dev",
|
||||
trailingSlash: "never",
|
||||
@@ -37,13 +38,6 @@ export default defineConfig({
|
||||
compiler: 'svelte',
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
// Customize watch behavior to reduce file watchers
|
||||
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
|
||||
usePolling: process.env.NODE_ENV === 'production',
|
||||
},
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
remarkPlugins: [setDefaultLayout]
|
||||
|
28
package.json
28
package.json
@@ -11,32 +11,32 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.3.1",
|
||||
"@astrojs/svelte": "^7.1.0",
|
||||
"@astrojs/mdx": "^4.2.6",
|
||||
"@astrojs/svelte": "^7.0.13",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"astro": "^5.12.0",
|
||||
"astro": "^5.7.13",
|
||||
"astro-i18n-aut": "^0.7.3",
|
||||
"exifreader": "^4.31.1",
|
||||
"svelte": "^5.36.10",
|
||||
"exifreader": "^4.30.1",
|
||||
"svelte": "^5.28.6",
|
||||
"svelte-gestures": "^5.1.4",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"thumbhash": "^0.1.1",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.4.1",
|
||||
"@iconify-json/tabler": "^1.2.19",
|
||||
"@astrojs/sitemap": "^3.4.0",
|
||||
"@iconify-json/tabler": "^1.2.17",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@unocss/preset-icons": "^66.3.3",
|
||||
"@unocss/reset": "^66.3.3",
|
||||
"@unocss/preset-icons": "^66.1.1",
|
||||
"@unocss/reset": "^66.1.1",
|
||||
"astro-font": "^1.1.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"ogl": "^1.0.11",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"sharp": "^0.34.3",
|
||||
"unocss": "^66.3.3",
|
||||
"sharp": "^0.34.1",
|
||||
"unocss": "^66.1.1",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite-plugin-glsl": "^1.5.1"
|
||||
"vite-plugin-glsl": "^1.4.1"
|
||||
}
|
||||
}
|
||||
|
1809
pnpm-lock.yaml
generated
1809
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- exifreader
|
||||
- sharp
|
@@ -3,7 +3,7 @@ import type { ImageMetadata } from "astro";
|
||||
import { Picture as AstroImage } from "astro:assets";
|
||||
import { generateThumbHash, getExifData } from "@helpers/image";
|
||||
interface Props {
|
||||
src: ImageMetadata & { fsPath?: string };
|
||||
src: ImageMetadata;
|
||||
alt: string;
|
||||
pictureClass?: string;
|
||||
class?: string;
|
||||
@@ -22,7 +22,7 @@ const {
|
||||
maxWidth,
|
||||
} = Astro.props;
|
||||
|
||||
let thumbhash = hash && image.fsPath ? await generateThumbHash(image) : "";
|
||||
let thumbhash = hash ? await generateThumbHash(image) : "";
|
||||
|
||||
let exif = await getExifData(image);
|
||||
|
||||
|
@@ -24,8 +24,8 @@ const paths = [
|
||||
text: t("nav.photos"),
|
||||
},
|
||||
{
|
||||
link: translatePath("/resources"),
|
||||
text: t("nav.resources"),
|
||||
link: translatePath("/videos"),
|
||||
text: t("nav.videos"),
|
||||
},
|
||||
];
|
||||
---
|
||||
|
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: Resources
|
||||
title: Videos
|
||||
menu: nav
|
||||
---
|
||||
|
1036
src/helpers/exif_
Normal file
1036
src/helpers/exif_
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ async function getSharp(): Promise<typeof import("sharp") | undefined> {
|
||||
return s;
|
||||
}
|
||||
|
||||
export async function generateThumbHash(image: ImageMetadata & { fsPath?: string }) {
|
||||
export async function generateThumbHash(image: { width: number, height: number }) {
|
||||
|
||||
const sharp = await getSharp();
|
||||
if (!sharp) return;
|
||||
@@ -19,21 +19,16 @@ export async function generateThumbHash(image: ImageMetadata & { fsPath?: string
|
||||
const smallWidth = Math.floor(image.width * scaleFactor);
|
||||
const smallHeight = Math.floor(image.height * scaleFactor);
|
||||
|
||||
try {
|
||||
const smallImg = await sharp(image.fsPath)
|
||||
.resize(smallWidth, smallHeight)
|
||||
.withMetadata()
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.toBuffer();
|
||||
|
||||
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 ""
|
||||
}
|
||||
//@ts-ignore
|
||||
const smallImg = await sharp(image.fsPath)
|
||||
.resize(smallWidth, smallHeight)
|
||||
.withMetadata()
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.toBuffer();
|
||||
|
||||
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
}
|
||||
|
||||
const allowedExif = [
|
||||
@@ -53,15 +48,10 @@ 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 buffer = await sharp(imagePath).toBuffer();
|
||||
|
||||
const tags = await ExifReader.load(buffer, { async: true });
|
||||
const tags = await ExifReader.load((image as ImageMetadata & { fsPath: string }).fsPath, { async: true });
|
||||
|
||||
const out: Record<string, any> = {};
|
||||
let hasExif = false;
|
||||
@@ -75,7 +65,7 @@ export async function getExifData(image: ImageMetadata) {
|
||||
return hasExif ? out : undefined;
|
||||
} catch (error) {
|
||||
|
||||
console.log(`Error reading EXIF data from ${imagePath}`, error);
|
||||
console.log("Error reading EXIF data", error);
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
@@ -1,10 +0,0 @@
|
||||
export async function listResource(id: string): Promise<any[]> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:8080/resources?name=${id}`,
|
||||
);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
@@ -20,7 +20,7 @@ export const ui = {
|
||||
'home.subtitle': 'Trained Media Designer, Blender Nerd, Developer and Hardware Tinkerer.',
|
||||
'nav.blog': 'Blog',
|
||||
'nav.projects': 'Projects',
|
||||
'nav.resources': 'Resources',
|
||||
'nav.videos': 'Videos',
|
||||
'nav.photos': 'Photos',
|
||||
'toc.title': 'Table of Contents',
|
||||
"resume": "Resume",
|
||||
@@ -38,7 +38,7 @@ export const ui = {
|
||||
'home.subtitle': 'Ausgebildeter Mediengestalter, Blender Nerd, Entwickler und Hardware Bastler.',
|
||||
'nav.blog': 'Blog',
|
||||
'nav.projects': 'Projekte',
|
||||
'nav.resources': 'Resources',
|
||||
'nav.videos': 'Videos',
|
||||
'nav.photos': 'Fotos',
|
||||
"resume": "Lebenslauf",
|
||||
'toc.title': 'Inhaltsverzeichnis',
|
||||
|
@@ -16,10 +16,11 @@ const { frontmatter, headings } = Astro.props;
|
||||
const t = useTranslations(Astro.url);
|
||||
const { title, url, date: dateString, links, toc, cover } = frontmatter;
|
||||
const collection = url?.split("/")[2];
|
||||
//@ts-ignore
|
||||
const backlinkContent = t(`nav.${collection}`).toLowerCase();
|
||||
const date = new Date(dateString);
|
||||
const path = useTranslatedPath(Astro.url);
|
||||
|
||||
//@ts-ignore
|
||||
const backlinkContent = t(`nav.${collection}`).toLowerCase();
|
||||
---
|
||||
|
||||
<Layout title={title} image={cover}>
|
||||
|
@@ -41,6 +41,10 @@ const list = [...posts, ...photos];
|
||||
list.sort((a, b) => {
|
||||
return a.data.date > b.data.date ? -1 : 1;
|
||||
});
|
||||
// .sort((a, b) => {
|
||||
// return a.data.date.getDate() > b.data.date.getDate() ? 1 : -1;
|
||||
// });
|
||||
console.log(list.map((post) => [post.data.date, post.data.title]));
|
||||
|
||||
const featuredPost = list.find((post) => post.data?.featured);
|
||||
const otherPosts = list.filter((post) => featuredPost !== post).slice(0, 3);
|
||||
|
@@ -1,64 +0,0 @@
|
||||
---
|
||||
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: "📺",
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title="Max Richter">
|
||||
<HeroCard post={wiki} />
|
||||
<HeroCard post={recipes} />
|
||||
<HeroCard post={articles} />
|
||||
<HeroCard post={movies} />
|
||||
<HeroCard post={series} />
|
||||
</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>
|
@@ -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,10 +1,10 @@
|
||||
// uno.config.ts
|
||||
import { defineConfig, presetWind3 } from 'unocss'
|
||||
import { defineConfig, presetUno } from 'unocss'
|
||||
import presetIcons from '@unocss/preset-icons'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetWind3(),
|
||||
presetUno(),
|
||||
presetIcons(),
|
||||
],
|
||||
shortcuts: {
|
||||
|
Reference in New Issue
Block a user