Compare commits
49 Commits
main
..
7c331406a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c331406a5 | |||
| 0ab1e1068d | |||
| f76477db98 | |||
| 96c053d5ff | |||
| 68431e6b9c | |||
| f86661bbba | |||
| 5d59e2171d | |||
| 82eb0657e2 | |||
| 613ab7aef9 | |||
| 38a7f83096 | |||
| 74ab286f15 | |||
| 67d82f05f1 | |||
| fe1c2cc218 | |||
| 3a8a3987b1 | |||
| d1c4d505fe | |||
| 7b7e02ad28 | |||
| f76f3dd96e | |||
| 2a09d65863 | |||
| ee391e128f | |||
| f9072c3cfc | |||
| 3ee3879db7 | |||
| 1e04a7be6f | |||
| d025f7e01b | |||
| 46230f2140 | |||
| 4c1a6ed9f6 | |||
| d1a235369d | |||
| f52a13171c | |||
| feb9b21ff8 | |||
| 58b74bb801 | |||
| 5e931f15d3 | |||
| 6168a386fa | |||
| ced2d230a3 | |||
| 54ac6555c4 | |||
| 31864f616a | |||
| da8a16c416 | |||
| ba4a3c0af2 | |||
| 9105249c87 | |||
| e02321f5c3 | |||
| fe3454f046 | |||
| 116ea85df7 | |||
| 59d6095278 | |||
| 91e60bb090 | |||
| e4b941a1dc | |||
| dba9d51133 | |||
| e800314589 | |||
| 93baa3b6b0 | |||
| d4128840b9 | |||
| 31b24de86c | |||
| f0129ecc76 |
@@ -3,7 +3,3 @@
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||
*.JPEG filter=lfs diff=lfs merge=lfs -text
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -16,53 +16,59 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.max-richter.dev/max/website:latest
|
||||
steps:
|
||||
|
||||
- name: 🔄 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: 🔢 Prepare cache keys
|
||||
- name: CHeck tar varsion
|
||||
run: tar --version
|
||||
|
||||
- name: 🔢 Calculate cache IDs
|
||||
run: |
|
||||
# Calculate cache IDs for Git LFS and PNPM
|
||||
git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
|
||||
LFS_CACHE_ID=$(cat .lfs-assets-id | md5sum)-v1
|
||||
PNPM_CACHE_ID=$(cat pnpm-lock.yaml | md5sum)-v1
|
||||
echo "LFS_CACHE_ID=$LFS_CACHE_ID" >> $GITHUB_ENV
|
||||
echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_ENV
|
||||
echo "PNPM_CACHE_ID=$PNPM_CACHE_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: 🗄️ Cache Git LFS objects
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .git/lfs
|
||||
key: ${{ runner.os }}-lfs-${{ env.LFS_CACHE_ID }}
|
||||
|
||||
- name: 🛠️ Cache PNPM dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.PNPM_STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: 📷 Cache Astro Images
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules/.astro
|
||||
key: ${{ runner.os }}-astro-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-astro-
|
||||
key: ${{ runner.os }}-astro-v1
|
||||
|
||||
- name: 🔄 Pull Git LFS files
|
||||
run: git lfs pull
|
||||
|
||||
- name: 🏗️ Build site
|
||||
run: |
|
||||
# Install dependencies, build, and generate site output
|
||||
pnpm i && pnpm build
|
||||
|
||||
- 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 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: 🚀 Deploy Changed Files via rclone
|
||||
run: |
|
||||
echo "Uploading _astro assets"
|
||||
rclone sync --update -v --progress --size-only --fast-list --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/** --fast-list --stats 2s --stats-one-line ./dist/ sftp-remote:${REMOTE_DIR} --transfers 4
|
||||
env:
|
||||
REMOTE_DIR: ${{ vars.REMOTE_DIR }}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"bracketSameLine": true
|
||||
}
|
||||
+1
-3
@@ -1,12 +1,10 @@
|
||||
FROM node:25-alpine
|
||||
FROM node:21-alpine
|
||||
|
||||
# Install necessary packages
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
git-lfs \
|
||||
tar \
|
||||
rclone \
|
||||
openssh \
|
||||
bash
|
||||
|
||||
# Install PNPM globally
|
||||
|
||||
+18
-25
@@ -1,20 +1,20 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import { filterSitemapByDefaultLocale, i18n } from "astro-i18n-aut/integration";
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { i18n, filterSitemapByDefaultLocale } from "astro-i18n-aut/integration";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import Icons from "unplugin-icons/vite";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import glsl from "vite-plugin-glsl";
|
||||
import Icons from 'unplugin-icons/vite'
|
||||
import mdx from '@astrojs/mdx';
|
||||
import glsl from 'vite-plugin-glsl';
|
||||
|
||||
import svelte from "@astrojs/svelte";
|
||||
import UnoCSS from "unocss/astro";
|
||||
import UnoCSS from 'unocss/astro'
|
||||
|
||||
const defaultLocale = "de";
|
||||
const locales = {
|
||||
en: "en",
|
||||
en: "en", // the `defaultLocale` value must present in `locales` keys
|
||||
de: "de",
|
||||
};
|
||||
|
||||
const DEFAULT_LAYOUT = "@layouts/Post.astro";
|
||||
const DEFAULT_LAYOUT = '@layouts/Post.astro';
|
||||
|
||||
function setDefaultLayout() {
|
||||
return function (_, file) {
|
||||
@@ -23,13 +23,11 @@ function setDefaultLayout() {
|
||||
};
|
||||
}
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://max-richter.dev",
|
||||
trailingSlash: "never",
|
||||
prefetch: true,
|
||||
image: {
|
||||
remotePatterns: [{ protocol: "https" }],
|
||||
},
|
||||
build: {
|
||||
format: "file",
|
||||
},
|
||||
@@ -37,28 +35,23 @@ export default defineConfig({
|
||||
plugins: [
|
||||
glsl(),
|
||||
Icons({
|
||||
compiler: "svelte",
|
||||
compiler: 'svelte',
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
|
||||
},
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
remarkPlugins: [setDefaultLayout],
|
||||
remarkPlugins: [setDefaultLayout]
|
||||
},
|
||||
integrations: [
|
||||
i18n({
|
||||
exclude: ["pages/**/*.json.ts", "pages/api/**/*"],
|
||||
locales,
|
||||
defaultLocale,
|
||||
}),
|
||||
mdx(),
|
||||
svelte(),
|
||||
UnoCSS({
|
||||
injectReset: true,
|
||||
injectReset: true
|
||||
}),
|
||||
i18n({
|
||||
exclude: ["pages/**/*.json.ts", "pages/api/**/*",],
|
||||
locales,
|
||||
defaultLocale,
|
||||
}),
|
||||
sitemap({
|
||||
i18n: {
|
||||
@@ -67,5 +60,5 @@ export default defineConfig({
|
||||
},
|
||||
filter: filterSitemapByDefaultLocale({ defaultLocale }),
|
||||
}),
|
||||
],
|
||||
]
|
||||
});
|
||||
|
||||
+23
-22
@@ -10,31 +10,32 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
"@astrojs/mdx": "^4.3.13",
|
||||
"@astrojs/svelte": "^7.2.4",
|
||||
"astro": "^5.16.6",
|
||||
"astro-i18n-aut": "^0.7.3",
|
||||
"exifreader": "^4.33.1",
|
||||
"svelte": "^5.46.1",
|
||||
"svelte-gestures": "^5.2.2",
|
||||
"@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",
|
||||
"thumbhash": "^0.1.1",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@iconify-json/tabler": "^1.2.26",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@unocss/preset-icons": "^66.5.12",
|
||||
"@unocss/reset": "^66.5.12",
|
||||
"astro-font": "^1.1.0",
|
||||
"@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",
|
||||
"markdown-it": "^14.1.0",
|
||||
"ogl": "^1.0.11",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"sharp": "^0.34.5",
|
||||
"unocss": "^66.5.12",
|
||||
"unplugin-icons": "^22.5.0",
|
||||
"vite-plugin-glsl": "^1.5.5"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4816
-4515
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- exifreader
|
||||
- sharp
|
||||
@@ -2,10 +2,6 @@ p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@@ -15,6 +11,7 @@ h3 {
|
||||
|
||||
article>h1 {
|
||||
font-size: 28px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
article>h2 {
|
||||
@@ -102,28 +99,3 @@ picture.thumb-loading::before {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
right: 3rem;
|
||||
bottom: -20px;
|
||||
background: var(--background-dark);
|
||||
mask-image: radial-gradient(circle at bottom left,
|
||||
transparent 19.5px,
|
||||
rgba(0, 0, 0, .5) 20px,
|
||||
#000 20.5px);
|
||||
box-shadow: 0px 0px 10px red;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow:
|
||||
0px 1px 1px rgba(3, 7, 18, 0.02),
|
||||
0px 5px 4px rgba(3, 7, 18, 0.03),
|
||||
0px 12px 9px rgba(3, 7, 18, 0.05),
|
||||
0px 20px 15px rgba(3, 7, 18, 0.06),
|
||||
0px 32px 24px rgba(3, 7, 18, 0.08);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 781 B |
@@ -1,43 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let searchTerm = "";
|
||||
let staticContainer: HTMLElement | null;
|
||||
|
||||
onMount(async () => {
|
||||
staticContainer = document.getElementById("resource-list-static");
|
||||
});
|
||||
|
||||
function search() {
|
||||
if (!staticContainer) return;
|
||||
|
||||
const lowerCaseSearchTerm = searchTerm.toLowerCase().trim();
|
||||
|
||||
if (!lowerCaseSearchTerm) {
|
||||
[...staticContainer?.children].forEach((element: HTMLElement) => {
|
||||
element.style.display = "";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const element of staticContainer?.children) {
|
||||
const el = element as HTMLElement;
|
||||
const isMatch = el.dataset.searchTerm?.includes(lowerCaseSearchTerm);
|
||||
if (isMatch) {
|
||||
el.style.display = "";
|
||||
} else {
|
||||
el.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: search();
|
||||
</script>
|
||||
|
||||
<div class="search-wrapper my-4 noise relative">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={searchTerm}
|
||||
placeholder={`Search...`}
|
||||
class="w-full p-2 gradient border-1 border-neutral rounded noise" />
|
||||
</div>
|
||||
@@ -95,7 +95,8 @@
|
||||
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>
|
||||
@@ -109,10 +110,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
width: 10vw;
|
||||
height: 10vw;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -120,10 +119,8 @@
|
||||
.eye {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 50px;
|
||||
max-height: 50px;
|
||||
width: 10vw;
|
||||
height: 10vw;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
|
||||
@@ -1,76 +1,58 @@
|
||||
---
|
||||
import { markdownToText, readDuration } from "@helpers/markdown";
|
||||
import markdownToText from "@helpers/markdownToText";
|
||||
import { Card } from "./card";
|
||||
import { useTranslatedPath, useTranslations } from "@i18n/utils";
|
||||
import Image from "@components/Image.astro";
|
||||
import type { InferEntrySchema } from "astro:content";
|
||||
import type { ImageMetadata } from "astro";
|
||||
|
||||
interface Props {
|
||||
post: {
|
||||
data: InferEntrySchema<"projects">;
|
||||
data: {
|
||||
title: string;
|
||||
icon?: string;
|
||||
cover?: ImageMetadata;
|
||||
};
|
||||
collection: string;
|
||||
id: string;
|
||||
body?: string;
|
||||
slug: string;
|
||||
body: string;
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
data: { title, cover, icon, date, rating, author },
|
||||
data: { title, cover, icon },
|
||||
collection,
|
||||
body,
|
||||
id,
|
||||
slug,
|
||||
} = Astro.props.post;
|
||||
|
||||
const translatePath = useTranslatedPath(Astro.url);
|
||||
const t = useTranslations(Astro.url);
|
||||
|
||||
const link = translatePath(`/${collection}/${id.split("/")[0]}`);
|
||||
|
||||
const image = cover as unknown;
|
||||
|
||||
const hasCover = typeof image === "string" ? !!image?.length : !!cover?.src;
|
||||
const link = translatePath(`/${collection}/${slug.split("/")[0]}`);
|
||||
---
|
||||
|
||||
<Card
|
||||
classes={`grid gradient border-1 border-neutral overflow-hidden ${hasCover ? "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">
|
||||
{
|
||||
(date || body || rating !== undefined) && (
|
||||
<Card.Metadata
|
||||
date={date}
|
||||
readDuration={readDuration(body)}
|
||||
rating={rating}
|
||||
author={author}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Card.Title classes="text-4xl flex items-center gap-2">
|
||||
{
|
||||
icon &&
|
||||
(icon?.length > 5 ? (
|
||||
<img class="h-6 w-6" src={icon} />
|
||||
) : (
|
||||
<span class="p-r-4 text-md">{icon}</span>
|
||||
))
|
||||
}
|
||||
{icon && <img src={icon} class="h-6" />}
|
||||
{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>
|
||||
{
|
||||
hasCover && (
|
||||
cover && (
|
||||
<a href={link}>
|
||||
<Image
|
||||
hash
|
||||
loader={false}
|
||||
src={cover as ImageMetadata}
|
||||
src={cover}
|
||||
alt={"cover for " + title}
|
||||
class="h-full right-0 object-cover object-center rounded-none border-l border-neutral"
|
||||
pictureClass="h-full"
|
||||
thumbnail
|
||||
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { Picture as AstroImage } from "astro:assets";
|
||||
import { inferRemoteSize } from "astro/assets/utils";
|
||||
import { getProcessedImage } from "@helpers/image";
|
||||
import { generateThumbHash } from "@helpers/image";
|
||||
interface Props {
|
||||
src: ImageMetadata & { fsPath?: string; src?: string };
|
||||
src: ImageMetadata;
|
||||
alt: string;
|
||||
pictureClass?: string;
|
||||
class?: string;
|
||||
@@ -12,29 +11,6 @@ interface Props {
|
||||
hash?: boolean;
|
||||
loader?: boolean;
|
||||
maxWidth?: number;
|
||||
thumbnail?: boolean;
|
||||
}
|
||||
|
||||
async function checkImage(
|
||||
image: ImageMetadata,
|
||||
): Promise<{ height: number; width: number } | undefined> {
|
||||
const src = typeof image === "string" ? image : image.src;
|
||||
if (!src) return;
|
||||
try {
|
||||
if (src.startsWith("/@fs") || src.startsWith("/_astro")) return image;
|
||||
const res = await inferRemoteSize(src);
|
||||
if (res.format) {
|
||||
image.format = res.format;
|
||||
return res;
|
||||
} else {
|
||||
console.log("Failed to load: ", src);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log("\n");
|
||||
console.log("Failed to fetch: ", src);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -44,16 +20,11 @@ const {
|
||||
hash = true,
|
||||
alt,
|
||||
maxWidth,
|
||||
thumbnail = false,
|
||||
} = Astro.props;
|
||||
|
||||
const imageOk = await checkImage(image);
|
||||
let thumbhash = hash ? await generateThumbHash(image) : "";
|
||||
|
||||
const { thumbhash, exif } = imageOk
|
||||
? await getProcessedImage(image)
|
||||
: { thumbhash: undefined, exif: undefined };
|
||||
|
||||
const definedSizes = [
|
||||
const sizes = [
|
||||
{
|
||||
width: 240,
|
||||
media: "(max-width: 360px)",
|
||||
@@ -69,33 +40,19 @@ const definedSizes = [
|
||||
{
|
||||
width: image.width,
|
||||
},
|
||||
];
|
||||
|
||||
const sizes = thumbnail
|
||||
? [definedSizes[1]]
|
||||
: definedSizes.filter((size) => !maxWidth || size.width <= maxWidth);
|
||||
].filter((size) => !maxWidth || size.width <= maxWidth);
|
||||
---
|
||||
|
||||
{
|
||||
imageOk ? (
|
||||
<AstroImage
|
||||
<AstroImage
|
||||
src={image}
|
||||
alt={alt}
|
||||
data-thumbhash={thumbhash}
|
||||
data-exif={JSON.stringify(exif)}
|
||||
width={imageOk?.width}
|
||||
height={imageOk?.height}
|
||||
pictureAttributes={{
|
||||
class: `${hash ? "block h-full relative" : ""} ${loader ? "thumb" : ""} ${pictureClass}`,
|
||||
}}
|
||||
class={`${Astro.props.class} w-full`}
|
||||
class={Astro.props.class}
|
||||
widths={sizes.map((size) => size.width)}
|
||||
sizes={sizes
|
||||
.map((size) => `${size.media || "100vw"} ${size.width}px`)
|
||||
.join(", ")}>
|
||||
<slot />
|
||||
</AstroImage>
|
||||
) : (
|
||||
<div>{JSON.stringify({ imageOk: imageOk, image })}</div>
|
||||
)
|
||||
}
|
||||
.join(", ")}
|
||||
/>
|
||||
|
||||
@@ -2,21 +2,11 @@
|
||||
import { onMount } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import normalizeWheel from "@helpers/normalizeWheel";
|
||||
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 images = [];
|
||||
let progress = [];
|
||||
let currentIndex = -1;
|
||||
const maxZoom = 5;
|
||||
import { useSwipe } from "svelte-gestures";
|
||||
import { swipe } from "svelte-gestures";
|
||||
|
||||
const mod = (a: number, b: number) => ((a % b) + b) % b;
|
||||
|
||||
@@ -44,14 +34,14 @@
|
||||
currentIndex = index;
|
||||
};
|
||||
|
||||
const handleKeyDown = ({ key }: KeyboardEvent) => {
|
||||
const handleKeyDown = ({ key }) => {
|
||||
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: Image) => {
|
||||
const handleOriginalLoading = async (image) => {
|
||||
if (!image.startedLoading) {
|
||||
image.startedLoading = true;
|
||||
let cIndex = currentIndex;
|
||||
@@ -64,8 +54,7 @@
|
||||
},
|
||||
});
|
||||
const total = Number(response.headers.get("content-length"));
|
||||
const reader = response?.body?.getReader();
|
||||
if (!reader) return;
|
||||
const reader = response.body.getReader();
|
||||
let bytesReceived = 0;
|
||||
let chunks = [];
|
||||
console.log("[SLIDER] started loading " + image.original);
|
||||
@@ -141,15 +130,6 @@
|
||||
// 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"),
|
||||
@@ -165,31 +145,18 @@
|
||||
console.log("Error loading", image);
|
||||
});
|
||||
|
||||
let exif: string[] = [];
|
||||
let exif = null;
|
||||
|
||||
try {
|
||||
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)
|
||||
: "",
|
||||
];
|
||||
}
|
||||
let rawExif = image.getAttribute("data-exif");
|
||||
exif = JSON.parse(rawExif);
|
||||
} catch (error) {
|
||||
// No biggie
|
||||
}
|
||||
|
||||
return {
|
||||
exif,
|
||||
startedLoading: false,
|
||||
loaded: "",
|
||||
// preview: preview.getAttribute("src"),
|
||||
src: image.getAttribute("srcset"),
|
||||
alt: image.getAttribute("alt"),
|
||||
sizes: image.getAttribute("sizes"),
|
||||
@@ -217,51 +184,42 @@
|
||||
<div class="controls">
|
||||
{#each images as _, i}
|
||||
<button
|
||||
aria-label={`Image ${i + 1}`}
|
||||
class="rounded bg-light"
|
||||
class:active={currentIndex === i}
|
||||
class:bg-light={currentIndex === i}
|
||||
on:click={() => {
|
||||
currentIndex = i;
|
||||
}}></button>
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="left flex-1 bg-light"
|
||||
aria-label="previous image"
|
||||
on:click={() => addIndex(- 1)}>
|
||||
<span class="i-tabler-arrow-left" />
|
||||
</button>
|
||||
<button
|
||||
class="right flex-1 bg-light"
|
||||
aria-label="next image"
|
||||
on:click={() => addIndex( + 1)}>
|
||||
<span class="i-tabler-arrow-right" />
|
||||
</button>
|
||||
<button
|
||||
class="close bg-light"
|
||||
aria-label="close gallery"
|
||||
on:click={() => setIndex(-1)}>
|
||||
<span class="i-tabler-x" />
|
||||
</button>
|
||||
<button class="left" on:click={() => addIndex(-1)}><</button>
|
||||
<button class="right" on:click={() => addIndex(+1)}>></button>
|
||||
<button class="close" on:click={() => setIndex(-1)}>X</button>
|
||||
|
||||
{#if currentIndex > -1}
|
||||
<div
|
||||
{...useSwipe(handleSwipe)}
|
||||
class="image"
|
||||
role="dialog"
|
||||
use:swipe
|
||||
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: ${
|
||||
@@ -269,15 +227,28 @@
|
||||
}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">
|
||||
{#each exif as e}
|
||||
<span> {e} </span>
|
||||
{/each}
|
||||
<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>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -290,12 +261,11 @@
|
||||
height: 10px;
|
||||
z-index: 99;
|
||||
left: 47px;
|
||||
background: var(--neutral-800);
|
||||
background-color: black;
|
||||
width: calc(100% - 94px);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.gallery-wrapper {
|
||||
position: fixed;
|
||||
z-index: 199;
|
||||
@@ -303,7 +273,7 @@
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: var(--neutral-800);
|
||||
background-color: rgba(24, 24, 24, 0.99);
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
@@ -343,11 +313,21 @@
|
||||
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: var(--neutral-800);
|
||||
background: black;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
@@ -358,7 +338,7 @@
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-style: none;
|
||||
background: transparent;
|
||||
background: black;
|
||||
border: solid 1px white;
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
@@ -375,49 +355,35 @@
|
||||
left: 50vw;
|
||||
transform: translateX(-50%);
|
||||
z-index: 99;
|
||||
background: var(--neutral-800);
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding: 5px;
|
||||
padding-inline: 10px;
|
||||
font-size: 0.9em;
|
||||
font-size: 0.8em;
|
||||
white-space: pre;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.exif > span {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.exif > span:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.gallery-wrapper > button {
|
||||
border-radius: 6px;
|
||||
padding: 0;
|
||||
background: black;
|
||||
color: white;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
position: fixed;
|
||||
z-index: 200;
|
||||
font-size: 1.5rem;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.close {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,28 +8,24 @@
|
||||
let height: number;
|
||||
let loaded = false;
|
||||
|
||||
function hide(index: number) {
|
||||
const img = images[index];
|
||||
function hide(img: HTMLPictureElement) {
|
||||
img.classList.remove("active");
|
||||
}
|
||||
|
||||
const heightCache = [];
|
||||
function show(index: number) {
|
||||
const img = images[index];
|
||||
function show(img: HTMLPictureElement) {
|
||||
img.classList.add("active");
|
||||
const _img = img.querySelector("img") || img;
|
||||
const _img = img.querySelector("img");
|
||||
if (!_img) return;
|
||||
_img.addEventListener("load", () => {
|
||||
img.classList.remove("thumb-loading");
|
||||
_img.style.opacity = "1";
|
||||
console.log("loaded");
|
||||
});
|
||||
altText = _img["alt"] ?? _img.getAttribute("alt") ?? "";
|
||||
if (heightCache[index]) {
|
||||
height = heightCache[index];
|
||||
}
|
||||
if (_img?.alt) altText = _img.alt;
|
||||
else altText = "";
|
||||
height = _img.getBoundingClientRect().height;
|
||||
setTimeout(() => {
|
||||
height = heightCache[index] ?? _img.getBoundingClientRect().height;
|
||||
heightCache[index] = height;
|
||||
height = _img.getBoundingClientRect().height;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -37,16 +33,16 @@
|
||||
function setIndex(i: number) {
|
||||
if (i < 0) i = images.length - 1;
|
||||
if (i >= images.length) i = 0;
|
||||
hide(index);
|
||||
hide(images[index]);
|
||||
index = i;
|
||||
show(index);
|
||||
show(images[index]);
|
||||
}
|
||||
|
||||
$: if (slot && !images?.length) {
|
||||
images = Array.from(slot.querySelectorAll("picture"));
|
||||
if (images?.length) {
|
||||
images.forEach((_, i) => hide(i));
|
||||
show(index);
|
||||
images.forEach(hide);
|
||||
show(images[index]);
|
||||
images[index].onload = () => {
|
||||
loaded = true;
|
||||
height = images[index].getBoundingClientRect().height;
|
||||
@@ -60,30 +56,34 @@
|
||||
|
||||
<div
|
||||
class="wrapper grid overflow-hidden rounded-xl border border-neutral"
|
||||
class:title={true}
|
||||
class:title
|
||||
class:not-loaded={!loaded}
|
||||
class:loaded
|
||||
style={`--height:${height}px`}>
|
||||
<div class="flex items-center p-x-4 p-y-6 bg justify-between">
|
||||
style={`--height:${height}px`}
|
||||
>
|
||||
{#if title}
|
||||
<div class="flex items-center p-x-4 bg justify-between">
|
||||
<h3>{title}</h3>
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-md bg-light gap-2 flex p-2 border border-light">
|
||||
<div class="overflow-hidden rounded-md bg-light gap-2 flex p-1">
|
||||
<button
|
||||
class="flex-1 i-tabler-arrow-left"
|
||||
aria-label="previous image"
|
||||
on:click={() => setIndex(index - 1)}></button>
|
||||
on:click={() => setIndex(index - 1)}
|
||||
/>
|
||||
<button
|
||||
class="flex-1 i-tabler-arrow-right"
|
||||
aria-label="next image"
|
||||
on:click={() => setIndex(index + 1)}></button>
|
||||
on:click={() => setIndex(index + 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="images border-block border-neutral" bind:this={slot}>
|
||||
<div class="images" bind:this={slot}>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="px-4 flex items-center place-content-between bg">
|
||||
<div class="p-2 flex place-content-between bg">
|
||||
<p>
|
||||
{#if images?.length}
|
||||
{index + 1}/{images?.length}
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
grid-template-rows: 1fr 50px;
|
||||
grid-template-rows: 1fr 40px;
|
||||
transition: height 0.3s;
|
||||
}
|
||||
|
||||
@@ -106,11 +106,11 @@
|
||||
}
|
||||
|
||||
.wrapper.title {
|
||||
grid-template-rows: 50px 1fr 50px;
|
||||
grid-template-rows: 40px 1fr 40px;
|
||||
}
|
||||
|
||||
.loaded {
|
||||
height: calc(var(--height) + 50px);
|
||||
height: calc(var(--height) + 40px);
|
||||
}
|
||||
|
||||
.not-loaded .images :global(picture):first-child {
|
||||
@@ -118,7 +118,7 @@
|
||||
}
|
||||
|
||||
.wrapper.title.loaded {
|
||||
height: calc(var(--height) + 100px);
|
||||
height: calc(var(--height) + 80px);
|
||||
}
|
||||
|
||||
.images :global(picture) {
|
||||
@@ -126,10 +126,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.images :global(.thumb-loading)::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.images :global(.active) {
|
||||
position: relative;
|
||||
display: block !important;
|
||||
|
||||
+10
-17
@@ -10,15 +10,15 @@ const t = useTranslations(Astro.url);
|
||||
---
|
||||
|
||||
<Card
|
||||
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">
|
||||
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"
|
||||
>
|
||||
<div
|
||||
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">
|
||||
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-contain"
|
||||
class="object-bottom h-full object-cover w-1/2 xs:w-full"
|
||||
hash={false}
|
||||
maxWidth={700}
|
||||
loader={false}
|
||||
@@ -30,17 +30,10 @@ const t = useTranslations(Astro.url);
|
||||
<GoogleyEye client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content flex flex-col p-8 pl-4 gap-3 justify-center">
|
||||
<div class="content flex flex-col p-8 pl-4 gap-2 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>
|
||||
|
||||
@@ -88,11 +81,11 @@ const t = useTranslations(Astro.url);
|
||||
position: absolute;
|
||||
}
|
||||
.eye.left {
|
||||
top: 24%;
|
||||
right: 27%;
|
||||
top: 29%;
|
||||
right: 28%;
|
||||
}
|
||||
.eye.right {
|
||||
top: 26%;
|
||||
right: 13%;
|
||||
top: 31%;
|
||||
right: 12%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
cover?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
cover,
|
||||
description = "A personal blog and portfolio by Max Richter.",
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
{cover && <meta property="og:image" content={cover} />}
|
||||
<meta property="og:description" content={description} />
|
||||
<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.startsWith(path);
|
||||
return Astro.url.pathname === path;
|
||||
}
|
||||
|
||||
const t = useTranslations(Astro.url);
|
||||
@@ -24,8 +24,8 @@ const paths = [
|
||||
text: t("nav.photos"),
|
||||
},
|
||||
{
|
||||
link: translatePath("/resources"),
|
||||
text: t("nav.resources"),
|
||||
link: translatePath("/videos"),
|
||||
text: t("nav.videos"),
|
||||
},
|
||||
];
|
||||
---
|
||||
@@ -33,10 +33,12 @@ 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>
|
||||
@@ -48,11 +50,13 @@ 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>
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
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,16 +1,20 @@
|
||||
---
|
||||
import { markdownToText } from "@helpers/markdown";
|
||||
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: InferEntrySchema<"blog">;
|
||||
data: {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
collection: string;
|
||||
body?: string;
|
||||
id: string;
|
||||
body: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,20 +22,15 @@ const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="rounded-diag-md border border-neutral p-4 overflow-hidden">
|
||||
<a href={tp(`/${post.collection}/${post.id.split("/")[0]}`)}>
|
||||
<a href={tp(`/${post.collection}/${post.slug.split("/")[0]}`)}>
|
||||
<h2
|
||||
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
|
||||
}
|
||||
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" />}
|
||||
{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>
|
||||
{
|
||||
@@ -40,7 +39,8 @@ 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,9 +10,10 @@ type Props = {
|
||||
const { headings } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="toc-wrapper lg:fixed lg:left-6 lg:top-6 z-2">
|
||||
<div class="toc-wrapper lg:fixed lg:left-6 lg:top-6">
|
||||
<details
|
||||
class="py-2 lg:px-4 rounded-xl lg:border lg:border-neutral flex flex-col lg:bg gap-2">
|
||||
class="py-2 px-4 rounded-xl border border-neutral flex flex-col bg gap-2"
|
||||
>
|
||||
<summary class="text-lg cursor-pointer select-none"
|
||||
>{t("toc.title")}</summary
|
||||
>
|
||||
@@ -23,7 +24,8 @@ 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,7 +3,6 @@
|
||||
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("");
|
||||
|
||||
@@ -11,12 +10,6 @@
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script>
|
||||
import { thumbHashToRGBA, rgbaToDataURL } from "thumbhash";
|
||||
|
||||
function show(img: HTMLImageElement) {
|
||||
img.style.opacity = "1";
|
||||
img.style.filter = "blur(0px)";
|
||||
setTimeout(() => {
|
||||
if (img.parentNode) {
|
||||
(img.parentNode as HTMLPictureElement).style.background = "";
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
console.log("Thumbhash script loaded");
|
||||
|
||||
document.querySelectorAll("[data-thumbhash]").forEach((entry) => {
|
||||
const parent = entry?.parentNode as HTMLPictureElement;
|
||||
const img = entry as HTMLImageElement;
|
||||
|
||||
if (parent?.nodeName !== "PICTURE") return;
|
||||
|
||||
const hash = img.getAttribute("data-thumbhash");
|
||||
if (!hash) return;
|
||||
|
||||
// its only browser, why you have to be mad
|
||||
const decodedString = window.atob(hash);
|
||||
|
||||
// Create Uint8Array from decoded string
|
||||
const buffer = new Uint8Array(decodedString.length);
|
||||
for (let i = 0; i < decodedString.length; i++) {
|
||||
buffer[i] = decodedString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const image = thumbHashToRGBA(buffer);
|
||||
const dataURL = rgbaToDataURL(image.w, image.h, image.rgba);
|
||||
|
||||
parent.style.background = `url(${dataURL})`;
|
||||
parent.style.backgroundSize = "cover";
|
||||
|
||||
if (img.complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
img.style.opacity = "0";
|
||||
img.style.filter = "blur(5px)";
|
||||
img.style.transition = "opacity 0.6s ease, filter 0.8s ease";
|
||||
|
||||
const sources = parent.querySelectorAll("source");
|
||||
|
||||
img.onload = () => show(img);
|
||||
for (const source of sources) {
|
||||
source.addEventListener("load", () => show(img));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -12,7 +12,6 @@ const t = useTranslations(Astro.url);
|
||||
.arrow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
transform: translateX(-110%) translateY(-160px);
|
||||
}
|
||||
.arrow > p {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let date: string | Date;
|
||||
export let readDuration: number | undefined;
|
||||
export let rating: string | number | undefined;
|
||||
export let author: string | undefined;
|
||||
|
||||
const toDate = (d: string | Date) =>
|
||||
typeof d === "string" ? new Date(d) : d;
|
||||
|
||||
const iso = (d: string | Date) => {
|
||||
if (!d) return "";
|
||||
const v = toDate(d);
|
||||
if (!v?.getTime) return "";
|
||||
return isNaN(v.getTime()) ? "" : v.toISOString();
|
||||
};
|
||||
|
||||
function formatRating(rating: string | number) {
|
||||
if (typeof rating === "number") {
|
||||
return "⭐".repeat(rating);
|
||||
}
|
||||
return rating;
|
||||
}
|
||||
|
||||
const formatDate = (d: string | Date) => {
|
||||
try {
|
||||
return new Intl.DateTimeFormat("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(toDate(d));
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if rating || date || readDuration || author}
|
||||
<div class="flex flex-wrap gap-3 wrapper">
|
||||
{#if rating}
|
||||
<div class="text-sm bg-light">{formatRating(rating)}</div>
|
||||
{/if}
|
||||
{#if date}
|
||||
<time class="text-sm bg-light" datetime={iso(date)}
|
||||
>{formatDate(date)}</time>
|
||||
{/if}
|
||||
{#if readDuration > 1}
|
||||
<div class="text-sm bg-light">{readDuration} mins read</div>
|
||||
{/if}
|
||||
{#if author}
|
||||
<div class="text-sm bg-light">{author}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.wrapper > * {
|
||||
padding: 2px 11px;
|
||||
border-radius: 14px;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,6 @@
|
||||
<a
|
||||
href={link}
|
||||
data-astro-prefetch
|
||||
class="mt-auto bg-light p-2 text-s rounded-md px-4 flex flex-0 items-center gap-2 w-fit"
|
||||
>{text}<span class="i-tabler-arrow-right inline-block w-4 h-4"></span>
|
||||
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" />
|
||||
</a>
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import Wrapper from "./Wrapper.svelte";
|
||||
import Image from "./Image.svelte";
|
||||
import Content from "./Content.svelte";
|
||||
import Title from "./Title.svelte";
|
||||
import Description from "./Description.svelte";
|
||||
import ReadMoreButton from "./ReadMoreButton.svelte";
|
||||
import Metadata from "./Metadata.svelte";
|
||||
import Wrapper from './Wrapper.svelte';
|
||||
import Image from './Image.svelte';
|
||||
import Content from './Content.svelte';
|
||||
import Title from './Title.svelte';
|
||||
import Description from './Description.svelte';
|
||||
import ReadMoreButton from './ReadMoreButton.svelte';
|
||||
|
||||
const Card = Wrapper as typeof Wrapper & {
|
||||
const Card = {
|
||||
...Wrapper,
|
||||
Image,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
ReadMoreButton
|
||||
} as typeof Wrapper & {
|
||||
Image: typeof Image;
|
||||
Metadata: typeof Metadata;
|
||||
Content: typeof Content;
|
||||
Title: typeof Title;
|
||||
Description: typeof Description;
|
||||
ReadMoreButton: typeof ReadMoreButton;
|
||||
};
|
||||
|
||||
Card.Image = Image;
|
||||
Card.Metadata = Metadata;
|
||||
Card.Content = Content;
|
||||
Card.Title = Title;
|
||||
Card.Description = Description;
|
||||
Card.ReadMoreButton = ReadMoreButton;
|
||||
}
|
||||
|
||||
export { Card };
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { markdownToHtml } from "@helpers/markdown";
|
||||
import Image from "@components/Image.astro";
|
||||
import type { ImageMetadata } from "astro";
|
||||
|
||||
const { resource } = Astro.props;
|
||||
---
|
||||
|
||||
<h1 class="text-4xl">{resource?.content?.headline}</h1>
|
||||
<div>
|
||||
{
|
||||
resource?.content?.image && (
|
||||
<Image
|
||||
hash
|
||||
src={
|
||||
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
|
||||
}
|
||||
alt="Cover for {resource?.content?.name}"
|
||||
class="rounded-2xl overflow-hidden"
|
||||
pictureClass="rounded-2xl box-shadow"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-4"
|
||||
set:html={markdownToHtml(resource?.content?.articleBody)}
|
||||
/>
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
import { Code } from 'astro:components';
|
||||
import Recipe from "./Recipe.astro";
|
||||
import Article from "./Article.astro";
|
||||
import Review from "./Review.astro";
|
||||
const { resource } = Astro.props;
|
||||
const type = resource?.content?._type ?? "unknown";
|
||||
---
|
||||
|
||||
{type === "Recipe" && <Recipe resource={resource} />}
|
||||
{type === "Article" && <Article resource={resource} />}
|
||||
{type === "Review" && <Review resource={resource} />}
|
||||
|
||||
{
|
||||
type === "unknown" && (
|
||||
<div>
|
||||
<h3>Unknown resource type</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<details >
|
||||
<summary class="flex"><span class="i-tabler-code w-6 h-6 inline"/></summary>
|
||||
<Code code={JSON.stringify(resource??"{}", null, 2)} lang="json" theme="dark-plus" />
|
||||
</details>
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { markdownToHtml } from "@helpers/markdown";
|
||||
import Image from "@components/Image.astro";
|
||||
import type { ImageMetadata } from "astro";
|
||||
|
||||
const { resource } = Astro.props;
|
||||
const ingredients = resource?.content?.recipeIngredient || [];
|
||||
const instructions = resource?.content?.recipeInstructions || [];
|
||||
---
|
||||
|
||||
<h1 class="text-4xl">{resource?.content?.name}</h1>
|
||||
<div>
|
||||
{
|
||||
resource?.content?.image && (
|
||||
<Image
|
||||
hash
|
||||
src={
|
||||
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
|
||||
}
|
||||
alt="Cover for {resource?.content?.name}"
|
||||
class="rounded-2xl overflow-hidden"
|
||||
pictureClass="rounded-2xl box-shadow"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-4 px-4"
|
||||
set:html={markdownToHtml(resource?.content?.description ?? "")}
|
||||
/>
|
||||
<h2 class="text-2xl px-4">Ingredients</h2>
|
||||
<ul class="list-disc px-10">
|
||||
{
|
||||
ingredients.filter((s:string) => !!s?.length).map((ingredient: string) => (
|
||||
<li set:html={markdownToHtml(ingredient)} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<h2 class="text-2xl px-4">Steps</h2>
|
||||
<ol class="list-decimal px-10">
|
||||
{
|
||||
instructions.filter((s:string) => !!s?.length).map((ingredient: string) => (
|
||||
<li set:html={markdownToHtml(ingredient)} />
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
import * as memorium from "@helpers/memorium";
|
||||
import { markdownToHtml } from "@helpers/markdown";
|
||||
import Image from "@components/Image.astro";
|
||||
import type { ImageMetadata } from "astro";
|
||||
|
||||
const { resource } = Astro.props;
|
||||
|
||||
function formatRating(rating: string | number) {
|
||||
if (typeof rating === "number") {
|
||||
return "⭐".repeat(rating);
|
||||
}
|
||||
return rating;
|
||||
}
|
||||
---
|
||||
|
||||
<div>
|
||||
{
|
||||
resource?.content?.image && (
|
||||
<Image
|
||||
hash
|
||||
src={
|
||||
{ src: memorium.getImageUrl(resource.content.image) } as ImageMetadata
|
||||
}
|
||||
alt="Cover for {resource?.content?.name}"
|
||||
class="rounded-2xl overflow-hidden"
|
||||
pictureClass="rounded-2xl w-1/2 mr-4 mb-4 float-left box-shadow"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<h1 class="text-4xl mb-4">
|
||||
{resource?.content?.itemReviewed?.name || "Unknown Name"}
|
||||
</h1>
|
||||
{
|
||||
resource?.content?.reviewRating?.ratingValue !== undefined && (
|
||||
<div>{formatRating(resource?.content?.reviewRating?.ratingValue)}</div>
|
||||
)
|
||||
}
|
||||
<div set:html={markdownToHtml(resource?.content?.reviewBody)} />
|
||||
</div>
|
||||
@@ -1,41 +0,0 @@
|
||||
import { glob } from "astro/loaders";
|
||||
import { defineCollection, type ImageFunction, z } 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(),
|
||||
rating: z.union([z.string(), z.number()]).optional(),
|
||||
coverAlt: z.string().optional(),
|
||||
toc: z.boolean().optional(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
author: 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,
|
||||
}),
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-af9e72fcc6.gif
|
||||
HostUrl=https://ezgif.com/save/ezgif-1-78ce3365b7.webm
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-74cf771d87.gif
|
||||
HostUrl=https://ezgif.com/save/ezgif-1-93f790072e.webm
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://ezgif.com/gif-to-webm/ezgif-1-70f1c50104.gif
|
||||
HostUrl=https://ezgif.com/save/ezgif-1-28f4d917d4.webm
|
||||
@@ -2,36 +2,38 @@
|
||||
import { onMount } from "svelte";
|
||||
import createFishes from "./fishes/webgl-fishes";
|
||||
import { Color } from "ogl";
|
||||
import { colors, rgbToHex } from "@helpers/colors";
|
||||
import { rgbToHex } from "@helpers/colors";
|
||||
|
||||
let canvasBottom: HTMLCanvasElement;
|
||||
|
||||
let speed = 0;
|
||||
let timeOffset = Math.random() * 100000;
|
||||
|
||||
let fishCanvasBack: ReturnType<typeof createFishes>;
|
||||
let fishCanvasBack: { resize: any; update: any };
|
||||
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();
|
||||
@@ -46,12 +48,15 @@
|
||||
});
|
||||
</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 />
|
||||
|
||||
@@ -65,10 +70,10 @@
|
||||
left: 0px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 4s;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
canvas.loaded {
|
||||
opacity: 0.8;
|
||||
opacity: 0.2;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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,9 +2,6 @@
|
||||
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,8 +3,6 @@ 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"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,104 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
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>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user