Compare commits
51 Commits
5b11664a3f
...
main
Author | SHA1 | Date | |
---|---|---|---|
b32ca7d65e | |||
c74b314b1e | |||
50ce8b3ff7 | |||
8e293c204d | |||
972c2382f3 | |||
e0543f2a58 | |||
3d78b9e56c | |||
84e56f2668 | |||
8af8db0714 | |||
59eeadd4b3 | |||
6aa6ddabb0 | |||
19a703367d | |||
aa333eb2cd | |||
4c3e35efbf | |||
dccd816f6e | |||
d57cded06d | |||
57ef1426ee | |||
e68f433f1e | |||
6f433e08ce | |||
f55ae8a267 | |||
e5726437ed | |||
08c852a9e8 | |||
91d0e55d3c | |||
de16fcea56 | |||
f9c5d4c421 | |||
f1c6bfdf87 | |||
d1d6867130 | |||
e41ef2fceb | |||
146c8d5279 | |||
187fd302fb | |||
44c03ff4c6 | |||
3216d79bd0 | |||
cf26695719 | |||
d1e4e93e4a | |||
6fb457c7ce | |||
13f3312bb2 | |||
f9833fa5eb | |||
cf74b0f7ef | |||
93bb879baf | |||
015acaf14d | |||
f49a25cfc8 | |||
21eb2e8c0c | |||
1cdce4c223 | |||
94b60be9b6 | |||
2df825187e | |||
c513605de6 | |||
0f1ece8a3e | |||
db17e56559 | |||
5e7a63230b | |||
dcd19b6da0 | |||
4b0f214568 |
37
.github/workflows/default.yaml
vendored
37
.github/workflows/default.yaml
vendored
@ -16,12 +16,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.max-richter.dev/max/website:latest
|
||||
steps:
|
||||
|
||||
- name: 🔄 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: CHeck tar varsion
|
||||
run: tar --version
|
||||
|
||||
- name: 🔢 Calculate cache IDs
|
||||
run: |
|
||||
# Calculate cache IDs for Git LFS and PNPM
|
||||
@ -55,20 +53,25 @@ jobs:
|
||||
|
||||
- name: 🏗️ Build site
|
||||
run: |
|
||||
# Install dependencies, build, and generate site output
|
||||
pnpm i && pnpm build
|
||||
|
||||
- name: 🚀 Deploy files via SFTP
|
||||
uses: pressidium/lftp-mirror-action@v1
|
||||
with:
|
||||
host: ${{ secrets.FTP_HOST }}
|
||||
port: ${{ secrets.FTP_PORT || 21 }}
|
||||
user: ${{ secrets.FTP_USERNAME }}
|
||||
pass: ${{ secrets.FTP_PASSWORD }}
|
||||
onlyNewer: true
|
||||
parallel: '4'
|
||||
settings: 'sftp:auto-confirm=yes'
|
||||
localDir: 'dist'
|
||||
remoteDir: '/share/new-website'
|
||||
options: '--verbose'
|
||||
- name: 🔑 Configure rclone
|
||||
run: |
|
||||
echo "$SSH_PRIVATE_KEY" > /tmp/id_rsa
|
||||
chmod 600 /tmp/id_rsa
|
||||
mkdir -p ~/.config/rclone
|
||||
echo -e "[sftp-remote]\ntype = sftp\nhost = ${SSH_HOST}\nuser = ${SSH_USER}\nport = ${SSH_PORT}\nkey_file = /tmp/id_rsa" > ~/.config/rclone/rclone.conf
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||
SSH_USER: ${{ vars.SSH_USER }}
|
||||
|
||||
- name: 🚀 Deploy Changed Files via rclone
|
||||
run: |
|
||||
echo "Uploading _astro assets"
|
||||
rclone sync --update -v --progress --size-only --stats 2s --stats-one-line ./dist/_astro sftp-remote:${REMOTE_DIR}/_astro --transfers 4
|
||||
echo "Uploading the rest"
|
||||
rclone sync --update -v --progress --exclude _astro/** --stats 2s --stats-one-line ./dist/ sftp-remote:${REMOTE_DIR} --transfers 4
|
||||
env:
|
||||
REMOTE_DIR: ${{ vars.REMOTE_DIR }}
|
||||
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"bracketSameLine": true
|
||||
}
|
@ -5,6 +5,8 @@ RUN apk add --no-cache \
|
||||
git \
|
||||
git-lfs \
|
||||
tar \
|
||||
rclone \
|
||||
openssh \
|
||||
bash
|
||||
|
||||
# Install PNPM globally
|
||||
|
@ -10,7 +10,7 @@ import UnoCSS from 'unocss/astro'
|
||||
|
||||
const defaultLocale = "de";
|
||||
const locales = {
|
||||
en: "en", // the `defaultLocale` value must present in `locales` keys
|
||||
en: "en",
|
||||
de: "de",
|
||||
};
|
||||
|
||||
@ -38,21 +38,28 @@ 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]
|
||||
},
|
||||
integrations: [
|
||||
mdx(),
|
||||
svelte(),
|
||||
UnoCSS({
|
||||
injectReset: true
|
||||
}),
|
||||
i18n({
|
||||
exclude: ["pages/**/*.json.ts", "pages/api/**/*",],
|
||||
locales,
|
||||
defaultLocale,
|
||||
}),
|
||||
mdx(),
|
||||
svelte(),
|
||||
UnoCSS({
|
||||
injectReset: true
|
||||
}),
|
||||
sitemap({
|
||||
i18n: {
|
||||
locales,
|
||||
|
47
package.json
47
package.json
@ -10,32 +10,33 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.5.10",
|
||||
"@astrojs/mdx": "^2.2.4",
|
||||
"@astrojs/svelte": "^5.3.0",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"astro": "^4.5.16",
|
||||
"astro-i18n-aut": "^0.7.0",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-gestures": "^4.0.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.2.6",
|
||||
"@astrojs/svelte": "^7.0.13",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"astro": "^5.7.13",
|
||||
"astro-i18n-aut": "^0.7.3",
|
||||
"exifreader": "^4.30.1",
|
||||
"svelte": "^5.28.6",
|
||||
"svelte-gestures": "^5.1.4",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"thumbhash": "^0.1.1",
|
||||
"typescript": "^5.4.4"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.1.2",
|
||||
"@iconify-json/tabler": "^1.1.109",
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"@unocss/preset-icons": "^0.59.0",
|
||||
"@unocss/reset": "^0.59.0",
|
||||
"astro-font": "^0.0.80",
|
||||
"@astrojs/sitemap": "^3.4.0",
|
||||
"@iconify-json/tabler": "^1.2.17",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@unocss/preset-icons": "^66.1.1",
|
||||
"@unocss/reset": "^66.1.1",
|
||||
"astro-font": "^1.1.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"ogl": "^1.0.6",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-astro": "^0.13.0",
|
||||
"sharp": "^0.33.3",
|
||||
"unocss": "^0.59.0",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite-plugin-glsl": "^1.3.0"
|
||||
"ogl": "^1.0.11",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"sharp": "^0.34.1",
|
||||
"unocss": "^66.1.1",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite-plugin-glsl": "^1.4.1"
|
||||
}
|
||||
}
|
||||
|
9535
pnpm-lock.yaml
generated
9535
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/max-richter-resume.pdf
Normal file
BIN
public/max-richter-resume.pdf
Normal file
Binary file not shown.
9
public/projects/plantarium/favicon.svg
Normal file
9
public/projects/plantarium/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M55.2154 77.6602L52.2788 78.3944L49.2607 60.6933L52.2788 33.0404L47.6293 18.0312L45.9162 18.6838L41.6745 28.7987L31.6412 33.4483H20.2211L21.6894 24.0675L31.6412 15.9919L45.9162 15.1762L49.7501 15.9919L55.2154 32.6326L54.5628 38.5873L64.8409 33.0404L69.5721 37.69L80.1764 43.1553L84.1734 52.8624L83.113 64.1193L73.8954 61.8353L66.3092 52.4545V38.5873L64.1068 36.7112L54.155 42.4212L52.2788 60.6933L55.2154 77.6602Z" fill="url(#paint0_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="34.3903" y1="15.1762" x2="52.1972" y2="78.3944" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4CAF7B"/>
|
||||
<stop offset="1" stop-color="#347452"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 781 B |
@ -95,8 +95,7 @@
|
||||
role="img"
|
||||
aria-label="Toggle Googley Eyes"
|
||||
aria-hidden="true"
|
||||
on:keydown={(ev) => (ev.key === "Enter" ? ($visible = !$visible) : "")}
|
||||
>
|
||||
on:keydown={(ev) => (ev.key === "Enter" ? ($visible = !$visible) : "")}>
|
||||
{#if $visible}
|
||||
<div class="eye" bind:this={eye} transition:scale>
|
||||
<div class="pupil"></div>
|
||||
@ -110,8 +109,10 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
width: 10vw;
|
||||
height: 10vw;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@ -119,8 +120,10 @@
|
||||
.eye {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
width: 10vw;
|
||||
height: 10vw;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
|
@ -3,18 +3,14 @@ import markdownToText from "@helpers/markdownToText";
|
||||
import { Card } from "./card";
|
||||
import { useTranslatedPath, useTranslations } from "@i18n/utils";
|
||||
import Image from "@components/Image.astro";
|
||||
import type { ImageMetadata } from "astro";
|
||||
import type { InferEntrySchema } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
post: {
|
||||
data: {
|
||||
title: string;
|
||||
icon?: string;
|
||||
cover?: ImageMetadata;
|
||||
};
|
||||
data: InferEntrySchema<"projects">;
|
||||
collection: string;
|
||||
slug: string;
|
||||
body: string;
|
||||
id: string;
|
||||
body?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -22,25 +18,31 @@ const {
|
||||
data: { title, cover, icon },
|
||||
collection,
|
||||
body,
|
||||
slug,
|
||||
id,
|
||||
} = Astro.props.post;
|
||||
|
||||
const translatePath = useTranslatedPath(Astro.url);
|
||||
const t = useTranslations(Astro.url);
|
||||
|
||||
const link = translatePath(`/${collection}/${slug.split("/")[0]}`);
|
||||
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]" : ""}`}
|
||||
>
|
||||
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">
|
||||
<Card.Title classes="text-4xl flex items-center gap-2">
|
||||
{icon && <img src={icon} class="h-6" />}
|
||||
{
|
||||
icon &&
|
||||
(
|
||||
icon?.length > 5
|
||||
? <img class="h-6 w-6" src={icon} />
|
||||
: <span class="p-r-4 text-md">{icon}</span>
|
||||
)
|
||||
}
|
||||
{title}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{markdownToText(body).slice(0, 200)}
|
||||
{markdownToText(body ?? "").slice(0, 200)}
|
||||
</Card.Description>
|
||||
<Card.ReadMoreButton link={link} text={t("read-more")} />
|
||||
</Card.Content>
|
||||
@ -50,7 +52,7 @@ const link = translatePath(`/${collection}/${slug.split("/")[0]}`);
|
||||
<Image
|
||||
hash
|
||||
loader={false}
|
||||
src={cover}
|
||||
src={cover as ImageMetadata}
|
||||
alt={"cover for " + title}
|
||||
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
|
||||
/>
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { Picture as AstroImage } from "astro:assets";
|
||||
import { generateThumbHash } from "@helpers/image";
|
||||
import { generateThumbHash, getExifData } from "@helpers/image";
|
||||
interface Props {
|
||||
src: ImageMetadata;
|
||||
src: ImageMetadata & { fsPath?: string };
|
||||
alt: string;
|
||||
pictureClass?: string;
|
||||
class?: string;
|
||||
@ -22,7 +22,9 @@ const {
|
||||
maxWidth,
|
||||
} = Astro.props;
|
||||
|
||||
let thumbhash = hash ? await generateThumbHash(image) : "";
|
||||
let thumbhash = hash && image.fsPath ? await generateThumbHash(image) : "";
|
||||
|
||||
let exif = await getExifData(image);
|
||||
|
||||
const sizes = [
|
||||
{
|
||||
@ -47,6 +49,7 @@ const sizes = [
|
||||
src={image}
|
||||
alt={alt}
|
||||
data-thumbhash={thumbhash}
|
||||
data-exif={JSON.stringify(exif)}
|
||||
pictureAttributes={{
|
||||
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
|
||||
}}
|
||||
@ -54,5 +57,6 @@ const sizes = [
|
||||
widths={sizes.map((size) => size.width)}
|
||||
sizes={sizes
|
||||
.map((size) => `${size.media || "100vw"} ${size.width}px`)
|
||||
.join(", ")}
|
||||
/>
|
||||
.join(", ")}>
|
||||
<slot />
|
||||
</AstroImage>
|
||||
|
@ -2,8 +2,18 @@
|
||||
import { onMount } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import normalizeWheel from "@helpers/normalizeWheel";
|
||||
let images = [];
|
||||
let progress = [];
|
||||
type Image = {
|
||||
exif: string[];
|
||||
src: string;
|
||||
alt: string;
|
||||
sizes: string;
|
||||
original: string;
|
||||
loaded: string;
|
||||
originalLoaded: boolean;
|
||||
startedLoading: boolean;
|
||||
};
|
||||
let images: Image[] = [];
|
||||
let progress: number[] = [];
|
||||
let currentIndex = -1;
|
||||
const maxZoom = 5;
|
||||
import { swipe } from "svelte-gestures";
|
||||
@ -34,14 +44,14 @@
|
||||
currentIndex = index;
|
||||
};
|
||||
|
||||
const handleKeyDown = ({ key }) => {
|
||||
const handleKeyDown = ({ key }: KeyboardEvent) => {
|
||||
if (currentIndex < 0) return;
|
||||
if (key === "Escape" && currentIndex > -1) setIndex(-1);
|
||||
if (key === "ArrowLeft") addIndex(-1);
|
||||
if (key === "ArrowRight") addIndex(+1);
|
||||
};
|
||||
|
||||
const handleOriginalLoading = async (image) => {
|
||||
const handleOriginalLoading = async (image: Image) => {
|
||||
if (!image.startedLoading) {
|
||||
image.startedLoading = true;
|
||||
let cIndex = currentIndex;
|
||||
@ -54,7 +64,8 @@
|
||||
},
|
||||
});
|
||||
const total = Number(response.headers.get("content-length"));
|
||||
const reader = response.body.getReader();
|
||||
const reader = response?.body?.getReader();
|
||||
if (!reader) return;
|
||||
let bytesReceived = 0;
|
||||
let chunks = [];
|
||||
console.log("[SLIDER] started loading " + image.original);
|
||||
@ -130,6 +141,15 @@
|
||||
// console.log(ev);
|
||||
};
|
||||
|
||||
function formatExposureTime(num: string) {
|
||||
if (num.includes("/")) {
|
||||
const [a, b] = num.split("/");
|
||||
return `${a}/${b}s`;
|
||||
} else {
|
||||
return num + "s";
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const wrappers = Array.prototype.slice.call(
|
||||
document.querySelectorAll("picture > img"),
|
||||
@ -145,18 +165,31 @@
|
||||
console.log("Error loading", image);
|
||||
});
|
||||
|
||||
let exif = null;
|
||||
let exif: string[] = [];
|
||||
|
||||
try {
|
||||
let rawExif = image.getAttribute("data-exif");
|
||||
exif = JSON.parse(rawExif);
|
||||
const rawExif = image.getAttribute("data-exif");
|
||||
const exifData = JSON.parse(rawExif);
|
||||
if (exifData) {
|
||||
exif = [
|
||||
"Model" in exifData ? exifData.Model : "",
|
||||
"FocalLength" in exifData
|
||||
? exifData.FocalLength.replace(" mm", "mm")
|
||||
: "",
|
||||
"FNumber" in exifData ? exifData.FNumber : "",
|
||||
"ExposureTime" in exifData
|
||||
? formatExposureTime(exifData.ExposureTime)
|
||||
: "",
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
// No biggie
|
||||
}
|
||||
|
||||
return {
|
||||
exif,
|
||||
// preview: preview.getAttribute("src"),
|
||||
startedLoading: false,
|
||||
loaded: "",
|
||||
src: image.getAttribute("srcset"),
|
||||
alt: image.getAttribute("alt"),
|
||||
sizes: image.getAttribute("sizes"),
|
||||
@ -184,11 +217,11 @@
|
||||
<div class="controls">
|
||||
{#each images as _, i}
|
||||
<button
|
||||
aria-label={`Image ${i + 1}`}
|
||||
class:active={currentIndex === i}
|
||||
on:click={() => {
|
||||
currentIndex = i;
|
||||
}}
|
||||
/>
|
||||
}}></button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@ -201,25 +234,18 @@
|
||||
<div
|
||||
class="image"
|
||||
use:swipe
|
||||
role="dialog"
|
||||
on:swipe={handleSwipe}
|
||||
on:wheel|passive={handleScroll}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:pointermove={handlePointerMove}
|
||||
>
|
||||
on:pointermove={handlePointerMove}>
|
||||
{#if progress[currentIndex] && progress[currentIndex] < 0.99}
|
||||
<div
|
||||
transition:fade
|
||||
id="progress"
|
||||
style={`transform: scaleX(${progress[currentIndex]});`}
|
||||
/>
|
||||
style={`transform: scaleX(${progress[currentIndex]});`} />
|
||||
{/if}
|
||||
|
||||
<img
|
||||
class="background"
|
||||
src={images[currentIndex].preview}
|
||||
alt="background blur"
|
||||
/>
|
||||
|
||||
<span>
|
||||
<img
|
||||
style={`transform: scale(${scale}); transform-origin: ${
|
||||
@ -227,28 +253,15 @@
|
||||
}px ${window.innerHeight - my}px`}
|
||||
srcset={images[currentIndex].loaded ? "" : images[currentIndex].src}
|
||||
src={images[currentIndex].loaded}
|
||||
alt={images[currentIndex].alt}
|
||||
/>
|
||||
alt={images[currentIndex].alt} />
|
||||
</span>
|
||||
</div>
|
||||
{#if images[currentIndex].exif}
|
||||
{@const exif = images[currentIndex].exif}
|
||||
<div class="exif" on:click={() => console.log(exif)}>
|
||||
{#if "FocalLength" in exif}
|
||||
{exif.FocalLength}mm |
|
||||
{/if}
|
||||
|
||||
{#if "FNumber" in exif}
|
||||
<i>f</i>{exif.FNumber} |
|
||||
{/if}
|
||||
|
||||
{#if "ExposureTime" in exif}
|
||||
{exif.ExposureTime.replace(" s", "s")} |
|
||||
{/if}
|
||||
|
||||
{#if "Date" in exif}
|
||||
{exif.Date}
|
||||
{/if}
|
||||
<div class="exif">
|
||||
{#each exif as e}
|
||||
<span> {e} </span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@ -261,7 +274,7 @@
|
||||
height: 10px;
|
||||
z-index: 99;
|
||||
left: 47px;
|
||||
background-color: black;
|
||||
background: var(--neutral-800);
|
||||
width: calc(100% - 94px);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
@ -273,7 +286,7 @@
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: rgba(24, 24, 24, 0.99);
|
||||
background: var(--neutral-800);
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
@ -313,21 +326,11 @@
|
||||
transform-origin 0.1s linear;
|
||||
}
|
||||
|
||||
.image > .background {
|
||||
filter: brightness(0.2);
|
||||
position: absolute;
|
||||
opacity: 0.3;
|
||||
width: 100%;
|
||||
z-index: 98;
|
||||
height: 100%;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: fit-content;
|
||||
left: 50vw;
|
||||
transform: translateX(-50%);
|
||||
background: black;
|
||||
background: var(--neutral-800);
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
@ -338,7 +341,7 @@
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-style: none;
|
||||
background: black;
|
||||
background: transparent;
|
||||
border: solid 1px white;
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
@ -355,15 +358,25 @@
|
||||
left: 50vw;
|
||||
transform: translateX(-50%);
|
||||
z-index: 99;
|
||||
background-color: black;
|
||||
background: var(--neutral-800);
|
||||
color: white;
|
||||
padding: 5px;
|
||||
font-size: 0.8em;
|
||||
padding-inline: 10px;
|
||||
font-size: 0.9em;
|
||||
white-space: pre;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.exif > span {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.exif > span:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.gallery-wrapper > button {
|
||||
background: black;
|
||||
background: var(--neutral-800);
|
||||
color: white;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
|
@ -10,30 +10,37 @@ const t = useTranslations(Astro.url);
|
||||
---
|
||||
|
||||
<Card
|
||||
classes="googley-eye-target relative rounded-diag-md border border-neutral bg-dark grid xs:grid-cols-[250px_1fr] min-h-[180px] sm:h-[180px] mt-8"
|
||||
>
|
||||
classes="googley-eye-target relative rounded-diag-md border border-neutral bg-dark grid xs:grid-cols-[200px_1fr] sm:grid-cols-[300px_1fr] mt-8">
|
||||
<div
|
||||
class="image relative h-[130%] self-end items-end flex overflow-hidden order-last xs:order-first"
|
||||
>
|
||||
<Image
|
||||
src={MaxImg}
|
||||
alt="its mee"
|
||||
class="object-bottom h-full object-cover w-1/2 xs:w-full"
|
||||
hash={false}
|
||||
maxWidth={700}
|
||||
loader={false}
|
||||
/>
|
||||
<div class="eye right">
|
||||
<GoogleyEye client:load />
|
||||
</div>
|
||||
<div class="eye left">
|
||||
<GoogleyEye client:load />
|
||||
class="relative xs:h-full self-end items-end flex order-last xs:order-first">
|
||||
<div
|
||||
class="image xs:absolute inline w-1/2 xs:w-full xs:h-[110%] overflow-hidden">
|
||||
<Image
|
||||
src={MaxImg}
|
||||
alt="its mee"
|
||||
class="object-bottom h-full object-contain"
|
||||
hash={false}
|
||||
maxWidth={700}
|
||||
loader={false}
|
||||
/>
|
||||
<div class="eye right">
|
||||
<GoogleyEye client:load />
|
||||
</div>
|
||||
<div class="eye left">
|
||||
<GoogleyEye client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content flex flex-col p-8 pl-4 gap-2 justify-center">
|
||||
<div class="content flex flex-col p-8 pl-4 gap-3 justify-center">
|
||||
<h1 class="text-2xl">{t("home.title")}</h1>
|
||||
<p>{t("home.subtitle")}</p>
|
||||
<a
|
||||
class="bg gradient flex items-center border border-neutral gap-2 w-fit p-2 px-4 rounded-2xl"
|
||||
href="/max-richter-resume.pdf">
|
||||
{t("resume").toLowerCase()}.pdf
|
||||
<span class="i-tabler-download w-4 h-4 dark:opacity-50"></span>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -81,11 +88,11 @@ const t = useTranslations(Astro.url);
|
||||
position: absolute;
|
||||
}
|
||||
.eye.left {
|
||||
top: 29%;
|
||||
right: 28%;
|
||||
top: 24%;
|
||||
right: 27%;
|
||||
}
|
||||
.eye.right {
|
||||
top: 31%;
|
||||
right: 12%;
|
||||
top: 26%;
|
||||
right: 13%;
|
||||
}
|
||||
</style>
|
||||
|
13
src/components/MetaTags.astro
Normal file
13
src/components/MetaTags.astro
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
cover: string;
|
||||
}
|
||||
---
|
||||
|
||||
<meta property="og:title" content={Astro.props.title} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:image" content={Astro.props.cover} />
|
||||
<meta property="og:description" content="Max Richters personal blog" />
|
||||
<meta property="og:site_name" content="Max Richter" />
|
@ -4,7 +4,7 @@ import Logo from "./Logo.astro";
|
||||
import ToggleTheme from "@components/ThemeToggle.svelte";
|
||||
|
||||
function isActive(path: string) {
|
||||
return Astro.url.pathname === path;
|
||||
return Astro.url.pathname.startsWith(path);
|
||||
}
|
||||
|
||||
const t = useTranslations(Astro.url);
|
||||
@ -33,12 +33,10 @@ const paths = [
|
||||
<ul class="flex my-4 h-12">
|
||||
<li><a href="#main-content" class="skip-link">Skip to main content</a></li>
|
||||
<li
|
||||
class="border-none bg-transparent my-2 mr-4 logo grid place-content-center"
|
||||
>
|
||||
class="border-none bg-transparent my-2 mr-4 logo grid place-content-center">
|
||||
<a
|
||||
href={translatePath("/")}
|
||||
class="text-neutral h-9 flex items-center justify-center lowercase"
|
||||
>
|
||||
class="text-neutral h-9 flex items-center justify-center lowercase">
|
||||
<Logo />
|
||||
</a>
|
||||
</li>
|
||||
@ -50,13 +48,11 @@ const paths = [
|
||||
${isActive(link) ? "bg-light underline" : "bg"}
|
||||
${i === 0 ? "rounded-bl-md border-l-1" : "border-l-1"}
|
||||
${i === paths.length - 1 ? "rounded-tr-md !border-r-1" : ""}
|
||||
`}
|
||||
>
|
||||
`}>
|
||||
<a
|
||||
class="text-neutral w-full h-full flex items-center justify-center lowercase"
|
||||
href={link}
|
||||
data-astro-prefetch
|
||||
>
|
||||
data-astro-prefetch>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
|
27
src/components/Picture.astro
Normal file
27
src/components/Picture.astro
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
import Image from "./Image.astro";
|
||||
const {src, alt, caption} = Astro.props;
|
||||
---
|
||||
|
||||
<figure>
|
||||
<Image src={src} alt={alt}/>
|
||||
<figcaption>
|
||||
{caption}
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
|
||||
figure {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
</style>
|
@ -1,20 +1,16 @@
|
||||
---
|
||||
import markdownToText from "@helpers/markdownToText";
|
||||
import { useTranslatedPath } from "@i18n/utils";
|
||||
import type { InferEntrySchema } from "astro:content";
|
||||
|
||||
const tp = useTranslatedPath(Astro.url);
|
||||
|
||||
interface Props {
|
||||
post: {
|
||||
data: {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
data: InferEntrySchema<"blog">;
|
||||
collection: string;
|
||||
body: string;
|
||||
slug: string;
|
||||
body?: string;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -22,15 +18,20 @@ const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="rounded-diag-md border border-neutral p-4 overflow-hidden">
|
||||
<a href={tp(`/${post.collection}/${post.slug.split("/")[0]}`)}>
|
||||
<a href={tp(`/${post.collection}/${post.id.split("/")[0]}`)}>
|
||||
<h2
|
||||
class="text-2xl flex gap-2 items-center line-clamp text-ellipsis overflow-hidden"
|
||||
>
|
||||
{post.data.icon && <img src={post.data.icon} class="h-6" />}
|
||||
class="text-2xl flex gap-2 items-center line-clamp text-ellipsis overflow-hidden">
|
||||
{
|
||||
post.data.icon?.length > 3 ? (
|
||||
<img src={post.data.icon} class="h-6" />
|
||||
) : post.data.icon?.length ? (
|
||||
<span>{post.data.icon}</span>
|
||||
) : null
|
||||
}
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<p class="text-ellipsis overflow-hidden line-clamp-2">
|
||||
{post.data.description || markdownToText(post.body).slice(0, 200)}
|
||||
{post.data.description || markdownToText(post?.body || "").slice(0, 200)}
|
||||
</p>
|
||||
</a>
|
||||
{
|
||||
@ -39,8 +40,7 @@ const { post } = Astro.props;
|
||||
{post.data.tags.map((tag) => (
|
||||
<a
|
||||
href={tp(`/tag/${tag}`)}
|
||||
class="text-xs border border-neutral p-2 rounded-md"
|
||||
>
|
||||
class="text-xs border border-neutral p-2 rounded-md">
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
|
@ -10,10 +10,9 @@ type Props = {
|
||||
const { headings } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="toc-wrapper lg:fixed lg:left-6 lg:top-6">
|
||||
<div class="toc-wrapper lg:fixed lg:left-6 lg:top-6 z-2">
|
||||
<details
|
||||
class="py-2 lg:px-4 rounded-xl lg:border lg:border-neutral flex flex-col lg:bg gap-2"
|
||||
>
|
||||
class="py-2 lg:px-4 rounded-xl lg:border lg:border-neutral flex flex-col lg:bg gap-2">
|
||||
<summary class="text-lg cursor-pointer select-none"
|
||||
>{t("toc.title")}</summary
|
||||
>
|
||||
@ -24,8 +23,7 @@ const { headings } = Astro.props;
|
||||
<a
|
||||
href={`#${heading.slug}`}
|
||||
style={{ marginLeft: `${(heading.depth - 1) * 1}rem` }}
|
||||
class={`block text my-0`}
|
||||
>
|
||||
class={`block text my-0`}>
|
||||
{heading.text}
|
||||
</a>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { writable } from "svelte/store";
|
||||
import IconSun from "~icons/tabler/Sun";
|
||||
import IconMoon from "~icons/tabler/Moon";
|
||||
import { colors } from "@helpers/colors";
|
||||
|
||||
let theme = writable("");
|
||||
|
||||
@ -10,6 +11,12 @@
|
||||
document.documentElement.classList.remove("light", "dark");
|
||||
document.documentElement.classList.add($theme);
|
||||
localStorage.setItem("theme", $theme);
|
||||
|
||||
const background = window.getComputedStyle(document.body);
|
||||
|
||||
$colors = {
|
||||
background: background.backgroundColor,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
|
@ -12,6 +12,7 @@ const t = useTranslations(Astro.url);
|
||||
.arrow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
transform: translateX(-110%) translateY(-160px);
|
||||
}
|
||||
.arrow > p {
|
||||
|
@ -7,5 +7,5 @@
|
||||
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"
|
||||
>{text}<span class="i-tabler-arrow-right inline-block w-4 h-4" />
|
||||
>{text}<span class="i-tabler-arrow-right inline-block w-4 h-4"></span>
|
||||
</a>
|
||||
|
@ -5,14 +5,7 @@ import Title from './Title.svelte';
|
||||
import Description from './Description.svelte';
|
||||
import ReadMoreButton from './ReadMoreButton.svelte';
|
||||
|
||||
const Card = {
|
||||
...Wrapper,
|
||||
Image,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
ReadMoreButton
|
||||
} as typeof Wrapper & {
|
||||
const Card = Wrapper as typeof Wrapper & {
|
||||
Image: typeof Image;
|
||||
Content: typeof Content;
|
||||
Title: typeof Title;
|
||||
@ -20,4 +13,10 @@ const Card = {
|
||||
ReadMoreButton: typeof ReadMoreButton;
|
||||
}
|
||||
|
||||
Card.Image = Image;
|
||||
Card.Content = Content;
|
||||
Card.Title = Title;
|
||||
Card.Description = Description;
|
||||
Card.ReadMoreButton = ReadMoreButton;
|
||||
|
||||
export { Card };
|
||||
|
33
src/content.config.ts
Normal file
33
src/content.config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z, type ImageFunction } from 'astro:content';
|
||||
|
||||
const defaultSchema = ({ image }: { image: ImageFunction }) => z.object({
|
||||
title: z.string(),
|
||||
date: z.date(),
|
||||
cover: image().optional(),
|
||||
links: z.array(z.array(z.string())).optional(),
|
||||
coverAlt: z.string().optional(),
|
||||
toc: z.boolean().optional(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
featured: z.boolean().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
_layout: z.enum(['normal', 'transparent']).optional(),
|
||||
})
|
||||
|
||||
|
||||
export const collections = {
|
||||
'blog': defineCollection({
|
||||
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/blog" }),
|
||||
schema: defaultSchema,
|
||||
}),
|
||||
"projects": defineCollection({
|
||||
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/projects" }),
|
||||
schema: defaultSchema,
|
||||
}),
|
||||
"photos": defineCollection({
|
||||
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/photos" }),
|
||||
schema: defaultSchema,
|
||||
})
|
||||
};
|
@ -2,38 +2,36 @@
|
||||
import { onMount } from "svelte";
|
||||
import createFishes from "./fishes/webgl-fishes";
|
||||
import { Color } from "ogl";
|
||||
import { rgbToHex } from "@helpers/colors";
|
||||
import { colors, rgbToHex } from "@helpers/colors";
|
||||
|
||||
let canvasBottom: HTMLCanvasElement;
|
||||
|
||||
let speed = 0;
|
||||
let timeOffset = Math.random() * 100000;
|
||||
|
||||
let fishCanvasBack: { resize: any; update: any };
|
||||
let fishCanvasBack: ReturnType<typeof createFishes>;
|
||||
let render = true;
|
||||
|
||||
const color = new Color("#ffffff");
|
||||
|
||||
function updateColor(c: string) {
|
||||
const d = new Color(rgbToHex(c));
|
||||
color.set(d.r, d.g, d.b);
|
||||
fishCanvasBack?.resize();
|
||||
}
|
||||
|
||||
$: if ($colors.background) {
|
||||
updateColor($colors.background);
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
fishCanvasBack.resize();
|
||||
|
||||
render = window.innerWidth > 500;
|
||||
};
|
||||
|
||||
const updateBackgroundColor = () => {
|
||||
const background = window.getComputedStyle(document.body);
|
||||
const d = new Color(rgbToHex(background.backgroundColor));
|
||||
color.set(d.r, d.g, d.b);
|
||||
fishCanvasBack.resize();
|
||||
};
|
||||
|
||||
let loaded = false;
|
||||
|
||||
onMount(async () => {
|
||||
const background = window.getComputedStyle(document.body);
|
||||
const d = new Color(rgbToHex(background.backgroundColor));
|
||||
color.set(d.r, d.g, d.b);
|
||||
|
||||
fishCanvasBack = createFishes(canvasBottom, { amount: 100, color });
|
||||
|
||||
fishCanvasBack.resize();
|
||||
@ -48,15 +46,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:body on:transitionend={updateBackgroundColor} />
|
||||
|
||||
<svelte:window
|
||||
on:resize={handleResize}
|
||||
on:scroll={() => {
|
||||
speed = Math.min(speed + 0.001, 0.15);
|
||||
timeOffset += speed;
|
||||
}}
|
||||
/>
|
||||
}} />
|
||||
|
||||
<canvas id="bottom" bind:this={canvasBottom} class:loaded />
|
||||
|
||||
@ -70,10 +65,10 @@
|
||||
left: 0px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
transition: opacity 4s;
|
||||
}
|
||||
|
||||
canvas.loaded {
|
||||
opacity: 0.2;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const blogCollection = defineCollection({
|
||||
schema: ({ image }) => z.object({
|
||||
title: z.string(),
|
||||
date: z.date(),
|
||||
cover: image().refine((img) => img.width >= 720, {
|
||||
message: "Cover image must be at least 720 pixels wide!",
|
||||
}).optional(),
|
||||
links: z.array(z.array(z.string())).optional(),
|
||||
coverAlt: z.string().optional(),
|
||||
toc: z.boolean().optional(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
featured: z.boolean().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
_layout: z.enum(['normal', 'transparent']).optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
'blog': blogCollection,
|
||||
"projects": blogCollection,
|
||||
"photos": blogCollection,
|
||||
};
|
@ -2,6 +2,9 @@
|
||||
title: "Erasmus Valencia"
|
||||
date: 2022-09-02
|
||||
cover: ./images/MAX_8218 - MAX_8230.jpg
|
||||
toc: true
|
||||
icon: 🍊
|
||||
tags: ["valencia", "erasmus"]
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro"
|
||||
|
@ -3,6 +3,8 @@ title: "Erasmus Valencia"
|
||||
date: 2022-09-02
|
||||
cover: ./images/MAX_8218 - MAX_8230.jpg
|
||||
toc: true
|
||||
icon: 🍊
|
||||
tags: ["valencia", "erasmus"]
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro"
|
||||
|
BIN
src/content/photos/madeira-2025/images/MAX_0354.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0354.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0369.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0369.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0442.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0442.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0447-Panorama.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0447-Panorama.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0463.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0463.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0505.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0505.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0603.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0603.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/MAX_0646.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/MAX_0646.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/PXL_20250209_132548610.MP.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/PXL_20250209_132548610.MP.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/PXL_20250209_133848550.MP.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/PXL_20250209_133848550.MP.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/PXL_20250210_150929413.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/PXL_20250210_150929413.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/PXL_20250211_181321910.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/PXL_20250211_181321910.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/PXL_20250212_181932180.RAW-02.ORIGINAL.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/PXL_20250212_181932180.RAW-02.ORIGINAL.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 6.5 MiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 MiB |
Binary file not shown.
After Width: | Height: | Size: 8.2 MiB |
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_125919043.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_125919043.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_134733435.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_134733435.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_173041299.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_173041299.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_174719071.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_174719071.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_175134355.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250209_175134355.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250211_151038493.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250211_151038493.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_142126974.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_142126974.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_142920774.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_142920774.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_144259621.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_144259621.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_153335886.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_153335886.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_154036484.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_154036484.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_161706087.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_161706087.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_163000971.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/madeira-2025/images/plants/PXL_20250212_163000971.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
104
src/content/photos/madeira-2025/index.en.mdx
Normal file
104
src/content/photos/madeira-2025/index.en.mdx
Normal file
@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Madeira
|
||||
date: 2025-02-16
|
||||
license: "CC-BY-SA:4.0"
|
||||
comments: true
|
||||
icon: 🏝️
|
||||
cover: ./images/MAX_0603.jpg
|
||||
tags: ["madeira", "travel"]
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro";
|
||||
import Picture from "@components/Picture.astro";
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
import ImageSlider from "@components/ImageSlider.svelte"
|
||||
|
||||
import MAX_0354 from "./images/MAX_0354.jpg"
|
||||
import MAX_0369 from "./images/MAX_0369.jpg"
|
||||
import MAX_0442 from "./images/MAX_0442.jpg"
|
||||
import MAX_0447_Panorama from "./images/MAX_0447-Panorama.jpg"
|
||||
import MAX_0463 from "./images/MAX_0463.jpg"
|
||||
import MAX_0505 from "./images/MAX_0505.jpg"
|
||||
import MAX_0603 from "./images/MAX_0603.jpg"
|
||||
import MAX_0646 from "./images/MAX_0646.jpg"
|
||||
|
||||
import PXL_20250209_132548610 from "./images/PXL_20250209_132548610.MP.jpg"
|
||||
import PXL_20250209_133848550 from "./images/PXL_20250209_133848550.MP.jpg"
|
||||
import PXL_20250210_150929413 from "./images/PXL_20250210_150929413.jpg"
|
||||
import PXL_20250211_174301568 from "./images/PXL_20250211_174301568.webm"
|
||||
import PXL_20250211_181321910 from "./images/PXL_20250211_181321910.jpg"
|
||||
import PXL_20250212_181932180 from "./images/PXL_20250212_181932180.RAW-02.ORIGINAL.jpg"
|
||||
|
||||
|
||||
import plant1 from "./images/plants/2025-02-09T11_05_08+00_00.JPEG"
|
||||
import plant2 from "./images/plants/2025-02-09T12_48_48+00_00.JPEG"
|
||||
import plant3 from "./images/plants/2025-02-09T12_55_24+00_00.JPEG"
|
||||
import PXL_20250209_125919043 from "./images/plants/PXL_20250209_125919043.jpg"
|
||||
import PXL_20250209_134733435 from "./images/plants/PXL_20250209_134733435.jpg"
|
||||
import PXL_20250209_173041299 from "./images/plants/PXL_20250209_173041299.jpg"
|
||||
import PXL_20250209_174719071 from "./images/plants/PXL_20250209_174719071.jpg"
|
||||
import PXL_20250209_175134355 from "./images/plants/PXL_20250209_175134355.jpg"
|
||||
import PXL_20250211_151038493 from "./images/plants/PXL_20250211_151038493.jpg"
|
||||
import PXL_20250212_142126974 from "./images/plants/PXL_20250212_142126974.jpg"
|
||||
import PXL_20250212_142920774 from "./images/plants/PXL_20250212_142920774.jpg"
|
||||
import PXL_20250212_144259621 from "./images/plants/PXL_20250212_144259621.jpg"
|
||||
import PXL_20250212_153335886 from "./images/plants/PXL_20250212_153335886.jpg"
|
||||
import PXL_20250212_154036484 from "./images/plants/PXL_20250212_154036484.jpg"
|
||||
import PXL_20250212_161706087 from "./images/plants/PXL_20250212_161706087.jpg"
|
||||
import PXL_20250212_163000971 from "./images/plants/PXL_20250212_163000971.jpg"
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
Photos from my trip to Madeira in February 2025.
|
||||
|
||||
## Porto Moniz
|
||||
|
||||
<Picture caption="The evening coast of Madeira near Porto Moniz" src={MAX_0603} alt="Evening coastal scene, waves crashing over rocks in front of a restaurant"/>
|
||||
<Picture caption="Waves in Porto Moniz" src={MAX_0505} alt="Wave crashes over rocks, spray flies high into the air, resembling a horse"/>
|
||||
<Picture caption="More pictures of the waves at Porto Moniz" src={MAX_0442} alt="A wave frozen in time breaks over rocks"/>
|
||||
<Picture caption="Panorama from Porto Moniz towards Seixal" src={MAX_0447_Panorama} alt="Panoramic view of Madeira’s coastline, large rocks in the water, blue mountains in the distance"/>
|
||||
<Picture caption="People fishing in Porto Moniz" src={MAX_0463} alt="Three people standing in the harbor fishing in front of a red car"/>
|
||||
|
||||
## Levada Nova Hike
|
||||
|
||||
<Picture caption="Maidenhair fern, Adiantum Capillus-veneris, is one of my favorite plants, it is very widespread" src={MAX_0354} alt="Close-up of a green maidenhair fern"/>
|
||||
<Image src={PXL_20250209_132548610} alt="Standing on a metal platform in the mountains"/>
|
||||
<Image src={PXL_20250209_133848550} alt="Standing in front of a waterfall"/>
|
||||
|
||||
## Seixal
|
||||
|
||||
<Picture src={PXL_20250210_150929413} caption="Beautiful room with an ocean view in the post office of Seixal" alt=""/>
|
||||
<Picture caption="Misty mountain peaks over Seixal" src={MAX_0369} alt="Foggy, densely forested mountain peaks rising high above each other"/>
|
||||
|
||||
## Hike around the Pico Areeiro
|
||||
|
||||
<video src={PXL_20250211_174301568} controls alt="POV of a person walking over a rocky bridge in the fog"/>
|
||||
<Image src={PXL_20250211_181321910} alt="Curvy road between red, earthy hills"/>
|
||||
|
||||
## Hike around Porto da Cruz
|
||||
|
||||
<Image src={PXL_20250212_181932180} alt="Sitting in a giant fern"/>
|
||||
|
||||
## Whale watching
|
||||
|
||||
<Picture caption="A young humpback whale practicing jumps for mating season off the coast of Calheta" src={MAX_0646} alt="Humpback whale breaching out of the water"/>
|
||||
|
||||
While traveling, I enjoy photographing plants that catch my eye. Here are some pictures of interesting plants I saw on Madeira. To identify them, I used the app [PlantNet](https://identify.plantnet.org/en).
|
||||
|
||||
<ImageSlider title="Interesting plants of the island" client:load>
|
||||
<Image src={plant1} alt="Dracaena reflexa (Song of India)"/>
|
||||
<Image src={plant2} alt="Oxalis pes-caprae (Bermuda buttercup)"/>
|
||||
<Image src={PXL_20250212_144259621} alt="Oxalis purpurea (Purple woodsorrel)"/>
|
||||
<Image src={PXL_20250212_154036484} alt="Bituminaria bituminosa (Pitch trefoil)"/>
|
||||
<Image src={PXL_20250209_125919043} alt="Galactites tomentosus (Purple milk thistle)"/>
|
||||
<Image src={PXL_20250209_134733435} alt="Sonchus fruticosus (Tree dandelion)"/>
|
||||
<Image src={PXL_20250209_174719071} alt="Persicaria capitata (Pink knotweed)"/>
|
||||
<Image src={PXL_20250209_175134355} alt="Rhaphiolepis bibas (Loquat)"/>
|
||||
<Image src={PXL_20250211_151038493} alt="Psidium cattleyanum (Strawberry guava)"/>
|
||||
<Image src={PXL_20250212_142920774} alt="Tecoma stans (Yellow trumpetbush)"/>
|
||||
<Image src={PXL_20250212_153335886} alt="Roldana petasitis (Velvet groundsel)"/>
|
||||
<Image src={PXL_20250212_161706087} alt="Bidens pilosa (Spanish needle)"/>
|
||||
<Image src={PXL_20250212_163000971} alt="Crassula multicava (Fairy crassula)"/>
|
||||
<Image src={PXL_20250209_173041299} alt="Some kind of liverwort, possibly Targionia lorbeeriana"/>
|
||||
<Image src={plant3} alt="Aeonium canariense (Canary Island aeonium)"/>
|
||||
</ImageSlider>
|
104
src/content/photos/madeira-2025/index.mdx
Normal file
104
src/content/photos/madeira-2025/index.mdx
Normal file
@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Madeira
|
||||
date: 2025-02-16
|
||||
license: "CC-BY-SA:4.0"
|
||||
comments: true
|
||||
icon: 🏝️
|
||||
cover: ./images/MAX_0603.jpg
|
||||
tags: ["madeira", "travel"]
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro";
|
||||
import Picture from "@components/Picture.astro";
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
import ImageSlider from "@components/ImageSlider.svelte"
|
||||
|
||||
import MAX_0354 from "./images/MAX_0354.jpg"
|
||||
import MAX_0369 from "./images/MAX_0369.jpg"
|
||||
import MAX_0442 from "./images/MAX_0442.jpg"
|
||||
import MAX_0447_Panorama from "./images/MAX_0447-Panorama.jpg"
|
||||
import MAX_0463 from "./images/MAX_0463.jpg"
|
||||
import MAX_0505 from "./images/MAX_0505.jpg"
|
||||
import MAX_0603 from "./images/MAX_0603.jpg"
|
||||
import MAX_0646 from "./images/MAX_0646.jpg"
|
||||
|
||||
import PXL_20250209_132548610 from "./images/PXL_20250209_132548610.MP.jpg"
|
||||
import PXL_20250209_133848550 from "./images/PXL_20250209_133848550.MP.jpg"
|
||||
import PXL_20250210_150929413 from "./images/PXL_20250210_150929413.jpg"
|
||||
import PXL_20250211_174301568 from "./images/PXL_20250211_174301568.webm"
|
||||
import PXL_20250211_181321910 from "./images/PXL_20250211_181321910.jpg"
|
||||
import PXL_20250212_181932180 from "./images/PXL_20250212_181932180.RAW-02.ORIGINAL.jpg"
|
||||
|
||||
import plant1 from "./images/plants/2025-02-09T11_05_08+00_00.JPEG"
|
||||
import plant2 from "./images/plants/2025-02-09T12_48_48+00_00.JPEG"
|
||||
import plant3 from "./images/plants/2025-02-09T12_55_24+00_00.JPEG"
|
||||
import PXL_20250209_125919043 from "./images/plants/PXL_20250209_125919043.jpg"
|
||||
import PXL_20250209_134733435 from "./images/plants/PXL_20250209_134733435.jpg"
|
||||
import PXL_20250209_173041299 from "./images/plants/PXL_20250209_173041299.jpg"
|
||||
import PXL_20250209_174719071 from "./images/plants/PXL_20250209_174719071.jpg"
|
||||
import PXL_20250209_175134355 from "./images/plants/PXL_20250209_175134355.jpg"
|
||||
import PXL_20250211_151038493 from "./images/plants/PXL_20250211_151038493.jpg"
|
||||
import PXL_20250212_142126974 from "./images/plants/PXL_20250212_142126974.jpg"
|
||||
import PXL_20250212_142920774 from "./images/plants/PXL_20250212_142920774.jpg"
|
||||
import PXL_20250212_144259621 from "./images/plants/PXL_20250212_144259621.jpg"
|
||||
import PXL_20250212_153335886 from "./images/plants/PXL_20250212_153335886.jpg"
|
||||
import PXL_20250212_154036484 from "./images/plants/PXL_20250212_154036484.jpg"
|
||||
import PXL_20250212_161706087 from "./images/plants/PXL_20250212_161706087.jpg"
|
||||
import PXL_20250212_163000971 from "./images/plants/PXL_20250212_163000971.jpg"
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
|
||||
Bilder von meiner Reise nach Madeira im Februar 2025.
|
||||
|
||||
## Porto Moniz
|
||||
|
||||
<Picture caption="Die abendliche Küste Madeiras for Porto Moniz" src={MAX_0603} alt="Abendliche Küstenszene, Welle brechen über Steinen vor einem Restaurant"/>
|
||||
<Picture caption="Wellen in Porto Moniz" src={MAX_0505} alt="Welle bricht über Steinen, Gischt fliegt hoch in die Luft, sieht aus wie ein Pferd"/>
|
||||
<Picture caption="Weitere Bilder von den Wellen vor Porto Moniz" src={MAX_0442} alt="Eine in der Zeit gefrorene Welle bricht über Felsen"/>
|
||||
<Picture caption="Panorama aus Porto Moniz Richtung Seixal" src={MAX_0447_Panorama} alt="Panorama Aufnahme der Küste von Madeira große Felsen im Wasser, blaue Berge in der Ferne"/>
|
||||
<Picture caption="Angelnde Menschen in Porto Moniz" src={MAX_0463} alt="Drei Menschen stehen im Hafen und Angeln vor einem roten Auto"/>
|
||||
|
||||
## Levada Nova Wanderung
|
||||
|
||||
<Picture caption="Frauenhaarfarn, Adiantum Capillus-veneris ist eine meiner Lieblingspflanzen, sie ist sehr weit verbreitet" src={MAX_0354} alt="Nahaufnahme eines grünen Frauenhaarfarns"/>
|
||||
<Image src={PXL_20250209_132548610} alt="Ich stehe auf einem Metalgerüst in den Bergen"/>
|
||||
<Image src={PXL_20250209_133848550} alt="Ich vor einem Wasserfall"/>
|
||||
|
||||
## Seixal
|
||||
|
||||
<Picture src={PXL_20250210_150929413} caption="Wunderschöner Raum mit Aussicht aufs Meer im Postbüro von Seixal" alt=""/>
|
||||
<Picture caption="Nebelige Bergspitzen über Seixal" src={MAX_0369} alt="Nebelige, dicht bewaldete Bergspitzen erheben sich hoch übereinander"/>
|
||||
|
||||
## Wanderung um den Pico Arieiro
|
||||
|
||||
<video src={PXL_20250211_174301568} controls alt="POV von einer Person die über eine felsige Brücke im Nebel geht"/>
|
||||
<Image src={PXL_20250211_181321910} alt="Kurvige Straße, zwischen roten, erdigen Hügeln"/>
|
||||
|
||||
## Wanderung um Porto da Cruz
|
||||
|
||||
<Image src={PXL_20250212_181932180} alt="Ich sitze in einem Riesenfarn"/>
|
||||
|
||||
## Whale watching
|
||||
|
||||
<Picture caption="Ein junger Buckelwal übt Sprünge für die Paarungszeit an der Küste vor Calheta" src={MAX_0646} alt="Buckelwal springt aus dem Wasser"/>
|
||||
|
||||
Während ich unterwegs bin, fotografiere ich gerne Pflanzen die mir auffallen. Hier sind ein paar Bilder von interessanten Pflanzen die ich auf Madeira gesehen habe. Zum identifizieren der Pflanzen habe ich die App [PlantNet](https://identify.plantnet.org/de) benutzt.
|
||||
|
||||
<ImageSlider title="Interessante Pflanzen der Insel" client:load>
|
||||
<Image src={plant1} alt="Dracaena reflexa (Drachenbaum)"/>
|
||||
<Image src={plant2} alt="Oxalis pes-caprae (Nickender Sauerklee)"/>
|
||||
<Image src={PXL_20250212_144259621} alt="Oxalis purpurea (Herbst-Sauerklee)"/>
|
||||
<Image src={PXL_20250212_154036484} alt="Bituminaria bituminosa (Gewöhnlicher-Asphaltklee)"/>
|
||||
<Image src={PXL_20250209_125919043} alt="Galactites tomentosus (Milchdistel)"/>
|
||||
<Image src={PXL_20250209_134733435} alt="Sonchus fruticosus (Strauchartige Gänsedistel)"/>
|
||||
<Image src={PXL_20250209_174719071} alt="Persicaria capitata (Kopf-Knöterich)"/>
|
||||
<Image src={PXL_20250209_175134355} alt="Rhaphiolepis bibas (Japanische Mispel)"/>
|
||||
<Image src={PXL_20250211_151038493} alt="Psidium cattleyanum (Erdbeerguave)"/>
|
||||
<Image src={PXL_20250212_142920774} alt="Tecoma stans (Gelber Trompetenstrauch)"/>
|
||||
<Image src={PXL_20250212_153335886} alt="Roldana petasitis (Pestwurz-Greiskraut)"/>
|
||||
<Image src={PXL_20250212_161706087} alt="Bidens pilosa (Zweizahn)"/>
|
||||
<Image src={PXL_20250212_163000971} alt="Crassula multicava (Affenbrotbaum)"/>
|
||||
<Image src={PXL_20250209_173041299} alt="Irgendeine Sorte Liverwort evt. Targionia lorbeeria"/>
|
||||
<Image src={plant3} alt="Aeonium canariense (Kanaren-Aeonium)"/>
|
||||
</ImageSlider>
|
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9689.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9689.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9708.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9708.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9727.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9727.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9747.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9747.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9749.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9749.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9775.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9775.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9839-Panorama.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9839-Panorama.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9849.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9849.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9851.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9851.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9857.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9857.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9861.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9861.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9865.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9865.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9873.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9873.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9882.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9882.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9892-Verbessert-NR.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/MAX_9892-Verbessert-NR.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240521_081658742.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240521_081658742.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240526_180330542.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240526_180330542.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240527_063005374.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240527_063005374.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240529_060714783.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240529_060714783.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240530_114819240.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240530_114819240.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240531_115332318.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240531_115332318.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240531_132410431.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240531_132410431.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240601_105417352.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240601_105417352.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240601_123821026.PORTRAIT.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240601_123821026.PORTRAIT.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240602_153636710.jpg
(Stored with Git LFS)
Normal file
BIN
src/content/photos/peaks-of-the-balkans/images/PXL_20240602_153636710.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
Binary file not shown.
155
src/content/photos/peaks-of-the-balkans/index.en.mdx
Normal file
155
src/content/photos/peaks-of-the-balkans/index.en.mdx
Normal file
@ -0,0 +1,155 @@
|
||||
---
|
||||
title: "Peaks of the Balkans"
|
||||
date: 2024-06-19
|
||||
cover: ./images/MAX_9861.jpg
|
||||
license: "CC-BY-SA:4.0"
|
||||
icon: 🏔️
|
||||
tags: ["balkans", "travel"]
|
||||
comments: true
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro";
|
||||
import Picture from "@components/Picture.astro";
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
|
||||
import MAX9689 from "./images/MAX_9689.jpg";
|
||||
import MAX9708 from "./images/MAX_9708.jpg";
|
||||
import MAX9727 from "./images/MAX_9727.jpg";
|
||||
import MAX9747 from "./images/MAX_9747.jpg";
|
||||
import MAX9749 from "./images/MAX_9749.jpg";
|
||||
import MAX9775 from "./images/MAX_9775.jpg";
|
||||
import MAX9839 from "./images/MAX_9839-Panorama.jpg";
|
||||
import MAX9849 from "./images/MAX_9849.jpg";
|
||||
import MAX9851 from "./images/MAX_9851.jpg";
|
||||
import MAX9857 from "./images/MAX_9857.jpg";
|
||||
import MAX9861 from "./images/MAX_9861.jpg";
|
||||
import MAX9865 from "./images/MAX_9865.jpg";
|
||||
import MAX9873 from "./images/MAX_9873.jpg";
|
||||
import MAX9882 from "./images/MAX_9882.jpg";
|
||||
import MAX9892 from "./images/MAX_9892-Verbessert-NR.jpg";
|
||||
|
||||
import PXL_20240527_063005374 from "./images/PXL_20240527_063005374.jpg"
|
||||
import PXL_20240526_180330542 from "./images/PXL_20240526_180330542.jpg"
|
||||
import PXL_20240601_123821026 from "./images/PXL_20240601_123821026.PORTRAIT.jpg"
|
||||
import PXL_20240521_081658742 from "./images/PXL_20240521_081658742.jpg";
|
||||
import PXL_20240522_153908533 from "./images/PXL_20240522_153908533.mp4";
|
||||
import PXL_20240529_060714783 from "./images/PXL_20240529_060714783.jpg";
|
||||
import PXL_20240530_114819240 from "./images/PXL_20240530_114819240.jpg";
|
||||
import PXL_20240531_115332318 from "./images/PXL_20240531_115332318.jpg";
|
||||
import PXL_20240601_105417352 from "./images/PXL_20240601_105417352.jpg";
|
||||
import PXL_20240603_092020564 from "./images/PXL_20240603_092020564.mp4";
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
Dies sind die Bilder die bei einer kurzen Reise durch den Balkan und meiner Wanderung über den "Peaks of the Balkan" Trail entstanden sind.
|
||||
|
||||
Eigentlich wollte ich nur den Trail wandern, aber ich habe mich dann doch entschieden noch ein paar Tage länger zu bleiben und ein paar Städte zu besuchen. Die Reise war sehr schön und ich habe viele interessante Menschen getroffen.
|
||||
|
||||
Angefangen hat die Reise das mich Freunde bis nach Villach in Österreich gebracht haben, von dort aus bin ich mit dem Zug nach Zagreb gefahren wo ich drei Tage im günstigsten Hostel der Stadt verbracht habe.
|
||||
|
||||
<Picture src={PXL_20240521_081658742} alt="" caption="Regnerisches Hotel in Villach"/>
|
||||
|
||||
Die folgenden Bilder sind in der verlassenen Universitätsklinik in Zagreb entstanden. Die Klinik wurde 1991 während des Kroatienkrieges geschlossen und ist seitdem verlassen.
|
||||
|
||||
<Image src={MAX9689} alt=""/>
|
||||
<Image src={MAX9708} alt=""/>
|
||||
<Image src={MAX9727} alt=""/>
|
||||
<Image src={MAX9747} alt=""/>
|
||||
<Image src={MAX9749} alt=""/>
|
||||
<video controls="" autoplay="" src={PXL_20240522_153908533}/>
|
||||
|
||||
<Picture src={MAX9775} alt="" caption="Dieses Bild ist in einem verlassenen Sanatorium in den Bergen um Zagreb entstanden"/>
|
||||
|
||||
Danach ging es mit dem Flugzeug nach Podgorica. Eigentlich wollte ich direkt von Podgorica nach Shkodër fahren, da aber der Flug 2,5 Stunden Verspätung hatte, habe ich den letzten Bus verpasst und musste eine Nacht in Podgorica verbringen.
|
||||
|
||||
<Picture src={MAX9839} alt="" caption="Verlassenes Haus in Shkoder"/>
|
||||
|
||||
Shkoder war ganz anders als die Städte die ich vorher besucht habe. Man merkt langsam das man sich von Zentraleuropa entfernt. Ich bin im Wanderers Hostel untergekommen, abends gab es in dem Hostel Cocktail Happy Hour und kostenlosen Rakia und dazu noch viele interessante Menschen.
|
||||
|
||||
## Tag 1 Theth - Valbona
|
||||
|
||||
Am nächsten Morgen brachte mich der erste Bus um 7:30 dann endlich nach Theth und so mit zum Anfang des Peaks of the Balkans Trails. Nach den paar Tagen in großen Städten war es sehr schön in der Natur zu sein.
|
||||
|
||||
<Picture src={MAX9851} alt="" caption="Auf dem Pass zwischen Theth und Valbona"/>
|
||||
|
||||
Der erste Aufstieg war echt steil, circa 1200 Höhenmeter in etwa 3 Stunden. Dieser Teil des Trails war mit Abstand der touristischste aber so kam es auch zu vielen netten Gesprächen.
|
||||
|
||||
<Picture src={MAX9849} alt="" caption="Der Ausblick ins Valbona Tal"/>
|
||||
|
||||
<Picture src={PXL_20240526_180330542} alt="" caption="Mein erster Schlafplatz im Tarp"/>
|
||||
|
||||
Am Abend habe ich mich dazu entschieden im Tarp zu schlafen, weil das Wetter recht gut aussah. Nach etwa einer halben Stunde hatte ich ein schönes Plätzchen in einem Feld aus Bohnenkraut gefunden.
|
||||
|
||||
## Tag 2 Valbone - Cerem
|
||||
|
||||
<Picture src={PXL_20240527_063005374} alt="" caption="Am nächsten Morgen gab es Porridge mit gesammelten Walderdbeeren"/>
|
||||
|
||||
Zum nächsten Ort (Cerem) gab es zwei Routen, einer durchs Tal entlang der Straße und einer über die Berge. Ich habe mich für die Route über die Berge entschieden.
|
||||
|
||||
<Image src={MAX9857} alt=""/>
|
||||
|
||||
Auf dem Weg über die Berge habe ich Sam und Juliette aus Quebec getroffen. Zusammen haben wir dann versucht über einen Bergsattel nach Cerem zu kommen. Leider haben wir uns vertan und es gab keinen Weg vom Bergsattel nach Cerem.
|
||||
|
||||
import PXL_20240527_121312366_small from "./images/PXL_20240527_121312366_small.mp4"
|
||||
|
||||
<video src={PXL_20240527_121312366_small} controls="" muted />
|
||||
|
||||
<Picture src={MAX9865} alt="" caption="Ich auf dem Prevoj Kolata Bergsattel"/>
|
||||
<Picture src={MAX9861} alt="" caption="Der Dobra Kolata Gipfel auf 2550m"/>
|
||||
|
||||
In Cerem habe ich mir ein Zimmer in einem Guesthouse genommen.
|
||||
|
||||
## Tag 3 Cerem - Cerem
|
||||
|
||||
Leider habe ich am nächsten Morgen den Fehler gemacht meinen Trinkbeutel am Wasserhahn zu füllen. Ich schaffte es noch circa eine Stunde lang zu wandern, danach wurde mir erst übel und ich musste mich übergeben und hatte Durchfall.
|
||||
|
||||
Ich schaffte es gerade noch so mein Tarp aufzubauen und mich hinzulegen. Erst dachte ich es läge an dem Käse den ich am Morgen gegessen hatte. Zum Glück kochte ich mir aus dem restlichen Wasser einen Tee.
|
||||
|
||||
<Picture src={PXL_20240529_060714783} alt="" caption="Gar kein schlechter Ort für eine kleine Lebensmittelvergiftung"/>
|
||||
|
||||
## Tag 4 Cerem - Millishevc
|
||||
|
||||
Am nächsten Morgen ging es mir schon wieder viel besser und so entschied ich mich dazu direkt zwei Tagesetappen zu laufen und so nach Millishevc zu kommen.
|
||||
|
||||
<Picture src={MAX9873} alt="" caption="Der Ausblick ins Doberdol Tal"/>
|
||||
|
||||
In Millishevc habe ich mein Tarp im Garten eines Guesthouses aufgeschlagen. Am nächsten Morgen ging es dann weiter nach Babino Polje.
|
||||
|
||||
## Tag 5 Millishevc - Babino Polje
|
||||
|
||||
<Picture src={PXL_20240530_114819240} alt="" caption="Auf dem Guri i Kuq Gipfel"/>
|
||||
|
||||
|
||||
Weil es auf dem Weg nach Babino Polje geregnet hatte und meine Schuhe nass waren habe ich mich dazu entschieden in einem Guesthouse zu übernachten.
|
||||
|
||||
## Tag 6 Babino Polje - Plav
|
||||
|
||||
import PXL_20240531_132410431 from "./images/PXL_20240531_132410431.jpg"
|
||||
|
||||
<Image src={MAX9882} alt=""/>
|
||||
|
||||
<Picture src={PXL_20240531_115332318} alt="" caption="Schöner Baum"/>
|
||||
|
||||
<Picture src={PXL_20240531_132410431} alt="" caption="Das erste mal wieder Zivilisation (Plav)"/>
|
||||
|
||||
## Tag 7 Plav - Vusanje
|
||||
|
||||
Auf dem Weg nach Vusanje habe ich Sam und Juliette wieder getroffen und mit ihnen kamen 3 wilde Hunde. Die Hunde waren sehr nett und haben uns den ganzen Weg begleitet.
|
||||
|
||||
<Image src={PXL_20240601_105417352} alt=""/>
|
||||
<Image src={PXL_20240601_123821026} alt=""/>
|
||||
<Image src={MAX9892} alt=""/>
|
||||
|
||||
## Tag 8 Vusanje - Valley of Lakes
|
||||
|
||||
import PXL_20240601_153636710 from "./images/PXL_20240602_153636710.jpg"
|
||||
import PXL_20240602_142615670 from "./images/PXL_20240602_142615670.mp4"
|
||||
|
||||
<Picture src={PXL_20240601_153636710} alt="" caption="Der zweithöchste Berg Albaniens, Maja e Jezercës 2694m" />
|
||||
|
||||
<video src={PXL_20240602_142615670} controls muted />
|
||||
|
||||
## Tag 9 Valley of Lakes - Theth
|
||||
|
||||
<video controls muted src={PXL_20240603_092020564} />
|
||||
|
154
src/content/photos/peaks-of-the-balkans/index.mdx
Normal file
154
src/content/photos/peaks-of-the-balkans/index.mdx
Normal file
@ -0,0 +1,154 @@
|
||||
---
|
||||
title: "Peaks of the Balkans"
|
||||
date: 2024-06-19
|
||||
cover: ./images/MAX_9861.jpg
|
||||
license: "CC-BY-SA:4.0"
|
||||
icon: 🏔️
|
||||
tags: ["balkans", "hiking", "travel"]
|
||||
comments: true
|
||||
---
|
||||
|
||||
import Image from "@components/Image.astro";
|
||||
import Picture from "@components/Picture.astro";
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
|
||||
import MAX9689 from "./images/MAX_9689.jpg";
|
||||
import MAX9708 from "./images/MAX_9708.jpg";
|
||||
import MAX9727 from "./images/MAX_9727.jpg";
|
||||
import MAX9747 from "./images/MAX_9747.jpg";
|
||||
import MAX9749 from "./images/MAX_9749.jpg";
|
||||
import MAX9775 from "./images/MAX_9775.jpg";
|
||||
import MAX9839 from "./images/MAX_9839-Panorama.jpg";
|
||||
import MAX9849 from "./images/MAX_9849.jpg";
|
||||
import MAX9851 from "./images/MAX_9851.jpg";
|
||||
import MAX9857 from "./images/MAX_9857.jpg";
|
||||
import MAX9861 from "./images/MAX_9861.jpg";
|
||||
import MAX9865 from "./images/MAX_9865.jpg";
|
||||
import MAX9873 from "./images/MAX_9873.jpg";
|
||||
import MAX9882 from "./images/MAX_9882.jpg";
|
||||
import MAX9892 from "./images/MAX_9892-Verbessert-NR.jpg";
|
||||
|
||||
import PXL_20240527_063005374 from "./images/PXL_20240527_063005374.jpg"
|
||||
import PXL_20240526_180330542 from "./images/PXL_20240526_180330542.jpg"
|
||||
import PXL_20240601_123821026 from "./images/PXL_20240601_123821026.PORTRAIT.jpg"
|
||||
import PXL_20240521_081658742 from "./images/PXL_20240521_081658742.jpg";
|
||||
import PXL_20240522_153908533 from "./images/PXL_20240522_153908533.mp4";
|
||||
import PXL_20240529_060714783 from "./images/PXL_20240529_060714783.jpg";
|
||||
import PXL_20240530_114819240 from "./images/PXL_20240530_114819240.jpg";
|
||||
import PXL_20240531_115332318 from "./images/PXL_20240531_115332318.jpg";
|
||||
import PXL_20240601_105417352 from "./images/PXL_20240601_105417352.jpg";
|
||||
import PXL_20240603_092020564 from "./images/PXL_20240603_092020564.mp4";
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
Dies sind die Bilder die bei einer kurzen Reise durch den Balkan und meiner Wanderung über den "Peaks of the Balkan" Trail entstanden sind.
|
||||
|
||||
Eigentlich wollte ich nur den Trail wandern, aber ich habe mich dann doch entschieden noch ein paar Tage länger zu bleiben und ein paar Städte zu besuchen. Die Reise war sehr schön und ich habe viele interessante Menschen getroffen.
|
||||
|
||||
Angefangen hat die Reise das mich Freunde bis nach Villach in Österreich gebracht haben, von dort aus bin ich mit dem Zug nach Zagreb gefahren wo ich drei Tage im günstigsten Hostel der Stadt verbracht habe.
|
||||
|
||||
<Picture src={PXL_20240521_081658742} alt="" caption="Regnerisches Hotel in Villach"/>
|
||||
|
||||
Die folgenden Bilder sind in der verlassenen Universitätsklinik in Zagreb entstanden. Die Klinik wurde 1991 während des Kroatienkrieges geschlossen und ist seitdem verlassen.
|
||||
|
||||
<Image src={MAX9689} alt=""/>
|
||||
<Image src={MAX9708} alt=""/>
|
||||
<Image src={MAX9727} alt=""/>
|
||||
<Image src={MAX9747} alt=""/>
|
||||
<Image src={MAX9749} alt=""/>
|
||||
<video controls="" autoplay="" src={PXL_20240522_153908533}/>
|
||||
|
||||
<Picture src={MAX9775} alt="" caption="Dieses Bild ist in einem verlassenen Sanatorium in den Bergen um Zagreb entstanden"/>
|
||||
|
||||
Danach ging es mit dem Flugzeug nach Podgorica. Eigentlich wollte ich direkt von Podgorica nach Shkodër fahren, da aber der Flug 2,5 Stunden Verspätung hatte, habe ich den letzten Bus verpasst und musste eine Nacht in Podgorica verbringen.
|
||||
|
||||
<Picture src={MAX9839} alt="" caption="Verlassenes Haus in Shkoder"/>
|
||||
|
||||
Shkoder war ganz anders als die Städte die ich vorher besucht habe. Man merkt langsam das man sich von Zentraleuropa entfernt. Ich bin im Wanderers Hostel untergekommen, abends gab es in dem Hostel Cocktail Happy Hour und kostenlosen Rakia und dazu noch viele interessante Menschen.
|
||||
|
||||
## Tag 1 Theth - Valbona
|
||||
|
||||
Am nächsten Morgen brachte mich der erste Bus um 7:30 dann endlich nach Theth und so mit zum Anfang des Peaks of the Balkans Trails. Nach den paar Tagen in großen Städten war es sehr schön in der Natur zu sein.
|
||||
|
||||
<Picture src={MAX9851} alt="" caption="Auf dem Pass zwischen Theth und Valbona"/>
|
||||
|
||||
Der erste Aufstieg war echt steil, circa 1200 Höhenmeter in etwa 3 Stunden. Dieser Teil des Trails war mit Abstand der touristischste aber so kam es auch zu vielen netten Gesprächen.
|
||||
|
||||
<Picture src={MAX9849} alt="" caption="Der Ausblick ins Valbona Tal"/>
|
||||
|
||||
<Picture src={PXL_20240526_180330542} alt="" caption="Mein erster Schlafplatz im Tarp"/>
|
||||
|
||||
Am Abend habe ich mich dazu entschieden im Tarp zu schlafen, weil das Wetter recht gut aussah. Nach etwa einer halben Stunde hatte ich ein schönes Plätzchen in einem Feld aus Bohnenkraut gefunden.
|
||||
|
||||
## Tag 2 Valbone - Cerem
|
||||
|
||||
<Picture src={PXL_20240527_063005374} alt="" caption="Am nächsten Morgen gab es Porridge mit gesammelten Walderdbeeren"/>
|
||||
|
||||
Zum nächsten Ort (Cerem) gab es zwei Routen, einer durchs Tal entlang der Straße und einer über die Berge. Ich habe mich für die Route über die Berge entschieden.
|
||||
|
||||
<Image src={MAX9857} alt=""/>
|
||||
|
||||
Auf dem Weg über die Berge habe ich Sam und Juliette aus Quebec getroffen. Zusammen haben wir dann versucht über einen Bergsattel nach Cerem zu kommen. Leider haben wir uns vertan und es gab keinen Weg vom Bergsattel nach Cerem.
|
||||
|
||||
import PXL_20240527_121312366_small from "./images/PXL_20240527_121312366_small.mp4"
|
||||
|
||||
<video src={PXL_20240527_121312366_small} controls="" muted />
|
||||
|
||||
<Picture src={MAX9865} alt="" caption="Ich auf dem Prevoj Kolata Bergsattel"/>
|
||||
<Picture src={MAX9861} alt="" caption="Der Dobra Kolata Gipfel auf 2550m"/>
|
||||
|
||||
In Cerem habe ich mir ein Zimmer in einem Guesthouse genommen.
|
||||
|
||||
## Tag 3 Cerem - Cerem
|
||||
|
||||
Leider habe ich am nächsten Morgen den Fehler gemacht meinen Trinkbeutel am Wasserhahn zu füllen. Ich schaffte es noch circa eine Stunde lang zu wandern, danach wurde mir erst übel und ich musste mich übergeben und hatte Durchfall.
|
||||
|
||||
Ich schaffte es gerade noch so mein Tarp aufzubauen und mich hinzulegen. Erst dachte ich es läge an dem Käse den ich am Morgen gegessen hatte. Zum Glück kochte ich mir aus dem restlichen Wasser einen Tee.
|
||||
|
||||
<Picture src={PXL_20240529_060714783} alt="" caption="Gar kein schlechter Ort für eine kleine Lebensmittelvergiftung"/>
|
||||
|
||||
## Tag 4 Cerem - Millishevc
|
||||
|
||||
Am nächsten Morgen ging es mir schon wieder viel besser und so entschied ich mich dazu direkt zwei Tagesetappen zu laufen und so nach Millishevc zu kommen.
|
||||
|
||||
<Picture src={MAX9873} alt="" caption="Der Ausblick ins Doberdol Tal"/>
|
||||
|
||||
In Millishevc habe ich mein Tarp im Garten eines Guesthouses aufgeschlagen. Am nächsten Morgen ging es dann weiter nach Babino Polje.
|
||||
|
||||
## Tag 5 Millishevc - Babino Polje
|
||||
|
||||
<Picture src={PXL_20240530_114819240} alt="" caption="Auf dem Guri i Kuq Gipfel"/>
|
||||
|
||||
|
||||
Weil es auf dem Weg nach Babino Polje geregnet hatte und meine Schuhe nass waren habe ich mich dazu entschieden in einem Guesthouse zu übernachten.
|
||||
|
||||
## Tag 6 Babino Polje - Plav
|
||||
|
||||
import PXL_20240531_132410431 from "./images/PXL_20240531_132410431.jpg"
|
||||
|
||||
<Image src={MAX9882} alt=""/>
|
||||
|
||||
<Picture src={PXL_20240531_115332318} alt="" caption="Schöner Baum"/>
|
||||
|
||||
<Picture src={PXL_20240531_132410431} alt="" caption="Das erste mal wieder Zivilisation (Plav)"/>
|
||||
|
||||
## Tag 7 Plav - Vusanje
|
||||
|
||||
Auf dem Weg nach Vusanje habe ich Sam und Juliette wieder getroffen und mit ihnen kamen 3 wilde Hunde. Die Hunde waren sehr nett und haben uns den ganzen Weg begleitet.
|
||||
|
||||
<Image src={PXL_20240601_105417352} alt=""/>
|
||||
<Image src={PXL_20240601_123821026} alt=""/>
|
||||
<Image src={MAX9892} alt=""/>
|
||||
|
||||
## Tag 8 Vusanje - Valley of Lakes
|
||||
|
||||
import PXL_20240601_153636710 from "./images/PXL_20240602_153636710.jpg"
|
||||
import PXL_20240602_142615670 from "./images/PXL_20240602_142615670.mp4"
|
||||
|
||||
<Picture src={PXL_20240601_153636710} alt="" caption="Der zweithöchste Berg Albaniens, Maja e Jezercës 2694m" />
|
||||
|
||||
<video src={PXL_20240602_142615670} controls muted />
|
||||
|
||||
## Tag 9 Valley of Lakes - Theth
|
||||
|
||||
<video controls muted src={PXL_20240603_092020564} />
|
BIN
src/content/projects/invoice/images/customers.png
(Stored with Git LFS)
Normal file
BIN
src/content/projects/invoice/images/customers.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/projects/invoice/images/edit-customer.png
(Stored with Git LFS)
Normal file
BIN
src/content/projects/invoice/images/edit-customer.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/projects/invoice/images/edit-profile.png
(Stored with Git LFS)
Normal file
BIN
src/content/projects/invoice/images/edit-profile.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/projects/invoice/images/invoices.png
(Stored with Git LFS)
Normal file
BIN
src/content/projects/invoice/images/invoices.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
src/content/projects/invoice/images/overview.png
(Stored with Git LFS)
Normal file
BIN
src/content/projects/invoice/images/overview.png
(Stored with Git LFS)
Normal file
Binary file not shown.
63
src/content/projects/invoice/index.en.mdx
Normal file
63
src/content/projects/invoice/index.en.mdx
Normal file
@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Invoice"
|
||||
date: 2023-08-21
|
||||
cover: ./images/bg.jpg
|
||||
icon: "/projects/invoice.svg"
|
||||
tags: ["sveltekit", "unocss", "prisma", "sqlite"]
|
||||
toc: true
|
||||
links:
|
||||
[
|
||||
["live", "https://invoice.app.max-richter.dev"],
|
||||
["git", "https://git.max-richter.dev/max/invoice"],
|
||||
]
|
||||
---
|
||||
|
||||
import bg from './images/bg.jpg'
|
||||
import invoices from './images/invoices.png'
|
||||
import customers from './images/customers.png'
|
||||
import editCustomers from './images/edit-customer.png'
|
||||
import editProfile from './images/edit-profile.png'
|
||||
import overview from './images/overview.png'
|
||||
import ImageSlider from '@components/ImageSlider.svelte'
|
||||
import Image from '@components/Image.astro'
|
||||
|
||||
|
||||
# Introduction
|
||||
|
||||
In my free time, I like to take on small jobs, such as deliveries, setups, and pickups for others.
|
||||
|
||||
An unavoidable part of these tasks is creating invoices in PDF format. Initially, I followed a manual process and created my first invoices in Figma. But as programmers often say:
|
||||
|
||||
> Why do something manually in 5 minutes when I can automate it in 24 hours?
|
||||
|
||||
This thought led to my latest hobby project – **"Invoice."**
|
||||
|
||||
<ImageSlider title="Invoice Screens" client:load>
|
||||
<Image src={invoices} alt="Invoices" />
|
||||
<Image src={customers} alt="Customers" />
|
||||
<Image src={editCustomers} alt="Edit Customers" />
|
||||
<Image src={editProfile} alt="Edit Profile" />
|
||||
<Image src={overview} alt="Overview" />
|
||||
</ImageSlider>
|
||||
|
||||
# Development
|
||||
|
||||
During development, I always kept the principle 'K.I.S.S.' in mind: Keep it simple, stupid. For this project, that meant choosing "boring" but well-known technologies:
|
||||
|
||||
## [🚀 SvelteKit](https://kit.svelte.dev)
|
||||
For an efficient and reactive user interface.
|
||||
|
||||
## [🎨 UNOcss](https://unocss.dev/)
|
||||
The faster alternative to Tailwind.
|
||||
|
||||
## [🌍 TypesafeI18n](https://github.com/ivanhofer/typesafe-i18n)
|
||||
To enable multilingual support without complex logic.
|
||||
|
||||
## [🛠️ Prisma](https://prisma.io)
|
||||
As a database access layer for smooth data management.
|
||||
|
||||
## [🗃️ SQLite](https://www.sqlite.org/index.html)
|
||||
As a reliable backend, ideal for small projects.
|
||||
|
||||
## [📄 Playwright](https://playwright.dev)
|
||||
For generating PDFs.
|
@ -12,6 +12,18 @@ links:
|
||||
]
|
||||
---
|
||||
|
||||
import bg from './images/bg.jpg'
|
||||
import invoices from './images/invoices.png'
|
||||
import customers from './images/customers.png'
|
||||
import editCustomers from './images/edit-customer.png'
|
||||
import editProfile from './images/edit-profile.png'
|
||||
import overview from './images/overview.png'
|
||||
import ImageSlider from '@components/ImageSlider.svelte'
|
||||
import Image from '@components/Image.astro'
|
||||
|
||||
|
||||
# Einleitung
|
||||
|
||||
In meiner Freizeit übernehme ich gerne kleinere Aufträge und erledige Botengänge, Aufbauten und Abholungen für andere.
|
||||
|
||||
Ein unvermeidlicher Bestandteil dieser Tätigkeiten ist das Erstellen von Rechnungen im PDF-Format. Anfangs habe ich mich dem manuellen Prozess hingegeben und die ersten Rechnungen in Figma erstellt. Doch wie es unter Programmierer*innen oft heißt:
|
||||
@ -20,25 +32,32 @@ Ein unvermeidlicher Bestandteil dieser Tätigkeiten ist das Erstellen von Rechnu
|
||||
|
||||
Aus dieser Überlegung heraus entstand mein neuestes Hobbyprojekt – **"Invoice."**
|
||||
|
||||
## Entwicklung
|
||||
<ImageSlider title="Invoice Screens" client:load>
|
||||
<Image src={invoices} alt="Invoices" />
|
||||
<Image src={customers} alt="Customers" />
|
||||
<Image src={editCustomers} alt="Edit Customers" />
|
||||
<Image src={editProfile} alt="Edit Profile" />
|
||||
<Image src={overview} alt="Overview" />
|
||||
</ImageSlider>
|
||||
|
||||
# Entwicklung
|
||||
|
||||
In der Entwicklung habe ich stets das Prinzip 'K.I.S.S.' im Hinterkopf behalten: Keep it simple, stupid. Für dieses Projekt bedeutete das die Auswahl von "langweiligen", aber mir bestens vertrauten Technologien:
|
||||
|
||||
[-> SvelteKit](https://kit.svelte.dev)
|
||||
## [🚀 SvelteKit](https://kit.svelte.dev)
|
||||
Für eine effiziente und reaktive Benutzeroberfläche.
|
||||
|
||||
[-> UNOcss](https://unocss.dev/)
|
||||
## [🎨 UNOcss](https://unocss.dev/)
|
||||
Die schnellere Tailwind Alternative.
|
||||
|
||||
[-> TypesafeI18n]()
|
||||
## [🌍 TypesafeI18n](https://github.com/ivanhofer/typesafe-i18n)
|
||||
Um mehrsprachige Unterstützung ohne komplizierte Logik zu integrieren.
|
||||
|
||||
[-> Prisma](https://prisma.io)
|
||||
## [🛠️ Prisma](https://prisma.io)
|
||||
Als Datenbankzugriffslayer für eine reibungslose Datenverwaltung.
|
||||
|
||||
[-> SQLite](https://www.sqlite.org/index.html)
|
||||
## [🗃️ SQLite](https://www.sqlite.org/index.html)
|
||||
Als zuverlässiges Backend, das sich ideal für kleinere Projekte eignet.
|
||||
|
||||
Diese bewährten Technologien bildeten das robuste Fundament, auf dem "Invoice" aufgebaut wurde.
|
||||
|
||||
|
||||
## [📄Playwright ](https://playwright.dev)
|
||||
Zum erstellen der PDFs.
|
||||
|
@ -5,20 +5,25 @@ draft: false
|
||||
cover: ./images/main.png
|
||||
icon: "/projects/isyncrasy/favicon.ico"
|
||||
description: "A small fun virtual OS build with svelte"
|
||||
links: [["live", "https://isyncrasy.com"]]
|
||||
tags: ["svelte", "web", "os"]
|
||||
---
|
||||
|
||||
# Isyncrasy
|
||||
|
||||
Isyncrasy ist ein kleines virtuelles Betriebssystem, das mit Svelte erstellt wurde. Es ist ein kleines Projekt, das ich gemacht habe, um Svelte zu lernen. Es ist ein einfaches Betriebssystem, das eine E-Mail-Anwendung und eine Terminalanwendung enthält. Es ist ein einfaches Projekt, aber es war sehr lehrreich.
|
||||
|
||||
import Image from "@components/Image.astro"
|
||||
import Main from "./images/main.png"
|
||||
import Mail from "./images/mail.png"
|
||||
import Terminal from "./images/terminal.png"
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
import ImageSlider from "@components/ImageSlider.svelte"
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
<Image src={Main} alt="Isyncrasy" />
|
||||
<Image src={Mail} alt="Isyncrasy" />
|
||||
<Image src={Terminal} alt="Isyncrasy" />
|
||||
<ImageSlider title="Design" client:load>
|
||||
<Image src={Main} alt="Desktop Overview" />
|
||||
<Image src={Mail} alt="Mail Application" />
|
||||
<Image src={Terminal} alt="Terminal Application" />
|
||||
</ImageSlider>
|
||||
|
||||
|
129
src/content/projects/karl/index.en.mdx
Normal file
129
src/content/projects/karl/index.en.mdx
Normal file
@ -0,0 +1,129 @@
|
||||
---
|
||||
title: "K.A.R.L"
|
||||
date: 2021-04-01
|
||||
cover: ./images/Indicatrices_of_Distortion.png
|
||||
license: "CC-BY-SA:4.0"
|
||||
featured: true
|
||||
toc: true
|
||||
icon: /projects/karl/favicon.png
|
||||
links:
|
||||
[
|
||||
["live", "https://max-richter.dev/karl"],
|
||||
["git", "https://git.max-richter.dev/max/karl"],
|
||||
]
|
||||
---
|
||||
|
||||
import Crosswalk from "./images/crosswalk.jpg"
|
||||
import CrosswalkMask from "./images/crosswalk_mask.png"
|
||||
import Image from "@components/Image.astro"
|
||||
import Distorion from "./images/Indicatrices_of_Distortion.png"
|
||||
import ImageGallery from "@components/ImageGallery.svelte"
|
||||
|
||||
<ImageGallery client:load/>
|
||||
|
||||
*[Header by Justin Kunimune - Own work, CC BY-SA 4.0](https://commons.wikimedia.org/w/index.php?curid=66467577*)*
|
||||
|
||||
|
||||
> K.A.R.L is a web app that helps to divide 360-degree panoramas into sections (sky, ground, trees, etc.) and then determine the proportion of each section in the overall image.
|
||||
|
||||
# Introduction
|
||||
|
||||
The project emerged from a collaboration with two friends. One of them is in the conceptual phase of his bachelor's thesis (Geography), which deals with the impact of vegetation on urban climate. To this end, he conducted albedo measurements at various locations in Cologne—essentially measuring "how much light comes from the sky, and how much of it is reflected by the ground." To put these measurements into context, he took 360-degree panoramas at each measurement location, which look something like this:
|
||||
|
||||
<Image src={Crosswalk} alt="Image of a crosswalk" caption="Image from hdrihaven.com" />
|
||||
|
||||
He then needed data on what percentage of the view was occupied by vegetation, sky, and ground. To measure this, he manually created a segmentation map in Gimp, which looks something like this:
|
||||
|
||||
<Image src={CrosswalkMask} alt="Segmentation map" />
|
||||
|
||||
# Problem Statement
|
||||
|
||||
If we naively count the pixels of each color and use that to calculate a percentage distribution, we encounter the classic distortion problem that humanity has faced for centuries with maps. Spheres do not like being represented in two dimensions, which always leads to distortions, as visualized in the following image.
|
||||
|
||||
<Image src={Distorion} alt="Indicatrices of Distortion" caption="by Justin Kunimune - Own work, CC BY-SA 4.0" />
|
||||
|
||||
Fortunately, this distortion only occurs in width, so we need a formula that provides a weight for the height of a pixel to compensate for this distortion. After many attempts, we arrived at this formula:
|
||||
|
||||
```javascript
|
||||
/*
|
||||
height: height of the image in pixels
|
||||
calibrationFactor: 1.333, somehow this works, don't ask why, we don't either
|
||||
y: y position of the pixel
|
||||
*/
|
||||
const pixelValue = Math.cos(
|
||||
(((360 / height ** 2) * y ** 2 + (-360 / height) * y + 90) / 360) *
|
||||
(2 + calibrationFactor) *
|
||||
Math.PI
|
||||
)
|
||||
```
|
||||
|
||||
This formula is actually designed to calculate the distance between two longitudes for a given latitude, but it works very well for our purposes.
|
||||
|
||||
Here are some of the first attempts in Desmos (fantastic tool, by the way):
|
||||
|
||||
<iframe src="https://www.desmos.com/calculator/52ph4thjah?embed"/>
|
||||
|
||||
# Technologies
|
||||
|
||||
I wanted to build a web app that theoretically works completely offline. Therefore, after the initial page load, no further data is sent; everything runs in the browser. This is challenging for an application that processes many pixels and requires a lot of performance since browser resources are limited. The following technologies helped to keep the user experience reasonably smooth:
|
||||
|
||||
## Canvas
|
||||
|
||||
When working with pixels in the browser, you can't really avoid Canvas2D. Its programmatic interface makes it ideal for this job.
|
||||
|
||||
## Web Workers
|
||||
|
||||
Normally, all operations of a website run in a single thread in the browser. This means that a computationally intensive task can bring the entire site to a halt. Web Workers provide a solution by allowing arbitrary code to run in a separate thread, though communication with the main thread can be tricky.
|
||||
|
||||
K.A.R.L uses two Web Workers: the `pixel-worker` handles segmentation map analysis and the fill tool, while the `ai-worker` executes TensorFlow code.
|
||||
|
||||
## TensorFlow
|
||||
|
||||
Why manually color segments when the computer can do it automatically? That was my thought, so I integrated TensorFlow with the [ADE20K](https://groups.csail.mit.edu/vision/datasets/ADE20K/) dataset. This network is excellent at recognizing trees, ground, concrete, and sky. In the editor view, this function is hidden under the "AI" button on the right.
|
||||
|
||||
## IndexDB
|
||||
|
||||
When it comes to storing large amounts of data (especially images) locally in the browser, IndexedDB is the best option *(yes, localStorage with Base64 images could work too, but it's limited to 5MB in some browsers)*. However, the IndexedDB API is one of the [most confusing browser APIs out there](https://nolanlawson.github.io/offlinefirst-2016-03/#/27), so I use [idb](npmjs.com/package/idb), a fantastic small wrapper library for IndexedDB that also supports Promises.
|
||||
|
||||
# Interesting Features...
|
||||
|
||||
## Flood Fill Algorithm
|
||||
|
||||
After manually segmenting several panoramas using the tools, I noticed that I often just painted regions with similar colors. This gave me the idea to build a fill tool similar to Photoshop, but much better.
|
||||
|
||||
The tool works roughly as follows:
|
||||
1. The user clicks on the image with the fill tool.
|
||||
2. The x/y coordinates of the click and all pixels of the image are sent to a Web Worker.
|
||||
3. The Web Worker creates a grayscale image where each pixel value represents the spatial and color distance to the clicked region.
|
||||
4. The image is sent back.
|
||||
5. The canvas code then determines whether individual pixels should be filled based on their values.
|
||||
|
||||
## Svelte Bindings
|
||||
|
||||
I always found it complicated to manage state across multiple components. A good example is the editor, which consists of three separate components (Toolbar, Topbar, PaintArea), all of which need to know which color and tool are currently active. One could store this state globally and let each component access it, but in reality, the active tool is only relevant in the editor context. So now, this state resides in the editor component and is passed down to its subcomponents via binding. It looks something like this:
|
||||
|
||||
```svelte
|
||||
<!-- Editor.svelte -->
|
||||
<script lang="ts">
|
||||
import TopBar from "./TopBar.svelte";
|
||||
|
||||
let activeColor = "ff0000";
|
||||
let _activeColor = localStorage.getItem("activeColor");
|
||||
if (_activeColor) {
|
||||
activeColor = _activeColor;
|
||||
}
|
||||
</script>
|
||||
|
||||
<TopBar bind:activeColor/>
|
||||
|
||||
<!-- TopBar.svelte -->
|
||||
<script>
|
||||
export let activeColor = "ff0000";
|
||||
</script>
|
||||
|
||||
{{activeColor}}
|
||||
```
|
||||
|
||||
## Svelte Stores
|
||||
|
||||
For some other cases, stores work really well, such as for toasts/modals. This allows the toast state (the small messages at the bottom left) to be managed in a single component without having to distribute it across multiple components.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user