feat: some shit

This commit is contained in:
2024-04-03 14:27:48 +02:00
parent fa5d08e549
commit 195d7dab5d
67 changed files with 2400 additions and 582 deletions

View File

@@ -0,0 +1,60 @@
---
import markdownToText from "@helpers/markdownToText";
import { Card } from "./card";
import { useTranslatedPath } from "@i18n/utils";
import Image from "@components/Image.astro";
interface Props {
post: {
data: {
title: string;
icon?: string;
headerImg?: string;
};
collection: string;
slug: string;
body: string;
};
}
const {
data: { title, headerImg, icon },
collection,
body,
slug,
} = Astro.props.post;
const translatePath = useTranslatedPath(Astro);
const imagePath = `../content/${collection}/${slug.split("/")[0]}/${headerImg}`;
const image = headerImg && (await import(imagePath)).default;
const link = translatePath(`/${collection}/${slug.split("/")[0]}`);
---
<Card
classes={`grid gradient border-1 border-neutral overflow-hidden ${image ? "grid-rows-[200px_1fr] sm:grid-rows-none sm:grid-cols-[1fr_200px]" : ""}`}
>
<Card.Content classes="px-8 py-7 order-last sm:order-first">
<Card.Title classes="text-4xl flex items-center gap-2">
{icon && <img src={icon} class="h-6" />}
{title}
</Card.Title>
<Card.Description>
{markdownToText(body).slice(0, 200)}
</Card.Description>
<Card.ReadMoreButton link={link} />
</Card.Content>
{
image?.format && (
<a href={link}>
<Image
src={image}
alt={"cover for " + title}
class="right-0 h-full object-cover object-center rounded-none border-l border-neutral"
/>
</a>
)
}
</Card>

View File

@@ -1,46 +0,0 @@
<script lang="ts">
import Card from "./Card.svelte";
export let title: string;
export let subtitle: string;
export let image: string;
export let description: string;
export let link: string;
</script>
<Card></Card>
<style>
.hero-card-wrapper {
display: flex;
border-radius: var(--border-radius-md);
background-color: var(--background);
border: solid thin var(--outline);
}
.hero-card-image {
flex: 1;
background-size: cover;
background-position: center;
border-radius: var(--border-radius-md) 0px 0px var(--border-radius-md);
min-height: 200px;
}
.hero-card-content {
flex: 1;
padding: var(--spacing-md);
}
h2 {
margin: 0;
}
h3 {
margin: 0;
}
a {
color: var(--primary);
text-decoration: none;
}
</style>

View File

@@ -1,15 +1,18 @@
---
import type { ImageMetadata } from "astro";
import { Image as AstroImage } from "astro:assets";
import { Picture as AstroImage } from "astro:assets";
interface Props {
src: ImageMetadata;
alt: string;
class?: string;
caption?: string;
maxWidth?: number;
}
const { src: image, alt, maxWidth } = Astro.props;
console.log({ image, alt, maxWidth });
const sizes = [
{
width: 240,
@@ -32,14 +35,9 @@ const sizes = [
<AstroImage
src={image}
alt={alt}
class={Astro.props.class}
widths={sizes.map((size) => size.width)}
sizes={sizes
.map((size) => `${size.media || "100vw"} ${size.width}px`)
.join(", ")}
/>
<style>
img {
border-radius: 0.5rem;
}
</style>

View File

@@ -0,0 +1,391 @@
<script lang="ts">
import { onMount } from "svelte";
import { fade } from "svelte/transition";
import normalizeWheel from "@helpers/normalizeWheel";
let images = [];
let progress = [];
let currentIndex = -1;
const maxZoom = 5;
import { swipe } from "svelte-gestures";
const mod = (a: number, b: number) => ((a % b) + b) % b;
const lerp = (a: number, b: number, t: number) => a * t + b * (1 - t);
const addIndex = (offset: number) => {
return setIndex(mod(currentIndex + offset, images.length));
};
let mx = 0,
my = 0,
_mx = 0,
_my = 0,
scale = 1;
const setIndex = (index: number) => {
mx = window.innerWidth / 2;
my = window.innerHeight / 2;
scale = 1;
if (index < 0) {
document.body.style.overflowY = "auto";
} else {
document.body.style.overflowY = "hidden";
}
currentIndex = index;
};
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) => {
if (!image.startedLoading) {
image.startedLoading = true;
let cIndex = currentIndex;
if (image.original) {
const response = await fetch(image.original, {
headers: {
"Accept-Encoding": "gzip, deflate, br",
"Content-Type":
"image/avif,image/webp,image/apng,image/svg+xml,image/jpeg,image/png,image/*,*/*;q=0.8",
},
});
const total = Number(response.headers.get("content-length"));
const reader = response.body.getReader();
let bytesReceived = 0;
let chunks = [];
console.log("[SLIDER] started loading " + image.original);
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("[SLIDER] Image complete");
break;
}
chunks.push(value);
if (total) {
bytesReceived += value.length;
progress[cIndex] = bytesReceived / total;
console.log(
"[SLIDER] " + Math.floor(progress[cIndex] * 1000) / 10 + "%",
);
progress = progress;
}
}
const b = new Blob(chunks);
images[cIndex].loaded = URL.createObjectURL(b);
images = images;
}
}
};
function handleSwipe(ev: any) {
if (currentIndex === -1) return;
if (ev.detail.direction === "right") addIndex(-1);
if (ev.detail.direction === "left") addIndex(+1);
}
const handleScroll = (ev: WheelEvent) => {
const { pixelY } = normalizeWheel(ev);
scale = Math.min(Math.max(scale - pixelY / 100, 1), maxZoom);
if (scale > 3) {
handleOriginalLoading(images[currentIndex]);
}
};
let isUpdating = false;
const update = () => {
isUpdating && requestAnimationFrame(update);
let dist = Math.abs(mx - _mx) + Math.abs(my - _my);
if (dist < 0.1 || scale == 1) {
mx = _mx;
my = _my;
isUpdating = false;
} else {
let s = (0.2 + (1 - scale / maxZoom) * 0.8) * 0.3;
mx = lerp(mx, _mx, 1 - s);
my = lerp(my, _my, 1 - s);
}
};
const startUpdate = () => {
if (!isUpdating && scale !== 1) {
isUpdating = true;
update();
}
};
const handleMouseMove = (ev: MouseEvent) => {
_mx = ev.clientX;
_my = ev.clientY;
startUpdate();
};
const handlePointerMove = () => {
// console.log(ev);
};
onMount(() => {
const wrappers = Array.prototype.slice.call(
document.querySelectorAll("[data-image-component]"),
);
console.log({ wrappers });
images = wrappers.map((image, i: number) => {
console.log({ image });
image.classList.add("image-active");
image.addEventListener("click", () => setIndex(i));
image.addEventListener("touch", () => setIndex(i));
image.style.cursor = "pointer";
image.addEventListener("error", () => {
console.log("Error loading", image);
});
let exif = null;
try {
let rawExif = image.getAttribute("data-exif");
exif = JSON.parse(rawExif);
} catch (error) {
// No biggie
}
return {
exif,
// preview: preview.getAttribute("src"),
src: image.getAttribute("srcset"),
alt: image.getAttribute("alt"),
sizes: image.getAttribute("sizes"),
original: image.getAttribute("data-original"),
originalLoaded: false,
};
});
return () => {
wrappers.forEach(({ children: [_, image] }, i: number) => {
image.removeEventListener("click", () => setIndex(i));
});
};
});
</script>
<svelte:window on:keydown={handleKeyDown} />
<div class="image-wrapper">
<slot />
</div>
<div class="gallery-wrapper" class:visible={currentIndex > -1}>
{#if images.length > 1}
<div class="controls">
{#each images as _, i}
<button
class:active={currentIndex === i}
on:click={() => {
currentIndex = i;
}}
/>
{/each}
</div>
{/if}
<button class="left" on:click={() => addIndex(-1)}>&lt;</button>
<button class="right" on:click={() => addIndex(+1)}>&gt;</button>
<button class="close" on:click={() => setIndex(-1)}>X</button>
{#if currentIndex > -1}
<div
class="image"
use:swipe
on:swipe={handleSwipe}
on:wheel|passive={handleScroll}
on:mousemove={handleMouseMove}
on:pointermove={handlePointerMove}
>
{#if progress[currentIndex] && progress[currentIndex] < 0.99}
<div
transition:fade
id="progress"
style={`transform: scaleX(${progress[currentIndex]});`}
/>
{/if}
<img
class="background"
src={images[currentIndex].preview}
alt="background blur"
/>
<span>
<img
style={`transform: scale(${scale}); transform-origin: ${
window.innerWidth - mx
}px ${window.innerHeight - my}px`}
srcset={images[currentIndex].loaded ? "" : images[currentIndex].src}
src={images[currentIndex].loaded}
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>
{/if}
{/if}
</div>
<style>
#progress {
position: absolute;
bottom: 0px;
height: 10px;
z-index: 99;
left: 47px;
background-color: black;
width: calc(100% - 94px);
transform-origin: left;
transition: transform 0.3s ease;
}
.gallery-wrapper {
position: fixed;
z-index: 199;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
background-color: rgba(24, 24, 24, 0.99);
opacity: 0;
transition: opacity 0.1s ease;
pointer-events: none;
backdrop-filter: blur(20px);
}
.gallery-wrapper.visible {
pointer-events: all;
opacity: 1;
}
.image {
position: relative;
display: grid;
place-items: center;
width: 100%;
height: 100%;
}
.image > span {
height: calc(100vh - 70px);
width: 95%;
}
.image img {
position: relative;
max-width: 100vw;
max-height: 100vh;
width: 100%;
height: 100%;
z-index: 98;
object-fit: contain;
image-rendering: pixelated;
transition:
transform 0.2s ease,
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;
padding: 5px;
position: absolute;
z-index: 99;
display: flex;
}
.controls > button {
width: 15px;
height: 15px;
border-style: none;
background: black;
border: solid 1px white;
cursor: pointer;
margin: 2px;
padding: 0px;
}
.controls > button.active {
background: white;
}
.exif {
position: absolute;
bottom: 0px;
left: 50vw;
transform: translateX(-50%);
z-index: 99;
background-color: black;
color: white;
padding: 5px;
font-size: 0.8em;
white-space: pre;
}
.gallery-wrapper > button {
background: black;
color: white;
border-radius: 0px;
border: none;
padding: 10px 20px;
position: fixed;
z-index: 200;
}
.close {
top: 0;
right: 0;
}
.left {
bottom: 0;
left: 0;
}
.right {
bottom: 0;
right: 0;
}
</style>

View File

@@ -0,0 +1,122 @@
<script lang="ts">
import { onMount } from "svelte";
let slot: HTMLDivElement;
let images: HTMLImageElement[];
export let title: string;
let altText: string;
let height: number;
let loaded = false;
function hide(img: HTMLImageElement) {
img.classList.remove("active");
}
function show(img: HTMLImageElement) {
img.classList.add("active");
if (img?.alt) altText = img.alt;
else altText = "";
height = img.getBoundingClientRect().height;
setTimeout(() => {
height = img.getBoundingClientRect().height;
}, 100);
}
let index = 0;
function setIndex(i: number) {
if (i < 0) i = images.length - 1;
if (i >= images.length) i = 0;
hide(images[index]);
index = i;
show(images[index]);
}
$: if (slot) {
images = Array.from(slot.querySelectorAll("img"));
if (images?.length) {
images.forEach(hide);
show(images[index]);
images[index].onload = () => {
loaded = true;
height = images[index].getBoundingClientRect().height;
};
}
setTimeout(() => {
loaded = true;
}, 100);
}
</script>
<div
class="wrapper grid overflow-hidden rounded-xl border border-light"
class:title
class:loaded
style={`--height:${height}px`}
>
{#if title}
<div class="flex items-center p-x-4 bg">
<h3>{title}</h3>
</div>
{/if}
<div class="images" bind:this={slot}>
<slot />
</div>
<div class="p-2 flex place-content-between bg">
<p>{index + 1}/{images?.length}</p>
{#if altText}
<p class="text-right">{altText}</p>
{/if}
<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
class="flex-1 i-tabler-arrow-right"
aria-label="next image"
on:click={() => setIndex(index + 1)}
/>
</div>
</div>
</div>
<style>
.wrapper {
grid-template-rows: 1fr 40px;
transition: height 0.3s;
}
.wrapper :global(.image-wrapper) {
margin: 0 !important;
}
.wrapper.title {
grid-template-rows: 40px 1fr 40px;
}
.loaded {
height: calc(var(--height) + 40px);
}
.wrapper.title.loaded {
height: calc(var(--height) + 80px);
}
.images :global(img) {
border-radius: 0px;
display: none;
}
.images :global(.active) {
position: relative;
display: block !important;
opacity: 1;
transition:
opacity 0.3s ease,
display 0.3s 0.3s;
}
</style>

View File

@@ -0,0 +1,33 @@
---
interface Props {
link: string;
title: string;
icon?: string;
}
---
<a
href={Astro.props.link}
class="rounded-diag-md border border-neutral p-4 overflow-hidden flex items-center justify-center pacity-50"
>
<h2
class="text-2xl flex gap-3 transition-gap items-center line-clamp text-ellipsis overflow-hidden"
>
{Astro.props.title}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-circle-arrow-right"
><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path
d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0 -18"></path><path d="M16 12l-4 -4"
></path><path d="M16 12h-8"></path><path d="M12 16l4 -4"></path></svg
>
</h2>
</a>

View File

@@ -16,6 +16,7 @@
}
</style>
</defs>
<title>home icon</title>
<path
id="l1"
d="M1 1H30C41.0457 1 50 9.95431 50 21V50H21C9.9543 50 1 41.0457 1 30V1Z"

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -10,7 +10,7 @@ const t = useTranslations(Astro);
---
<Card
classes="googley-eye-target relative rounded-diag-md border border-light gradient grid grid-cols-[250px_1fr] h-[180px] my-8xl"
classes="googley-eye-target relative rounded-diag-md border border-neutral gradient grid grid-cols-[250px_1fr] h-[180px] mt-8"
>
<div class="image">
<Image src={MaxImg} alt="its mee" maxWidth={700} />
@@ -39,6 +39,16 @@ const t = useTranslations(Astro);
align-items: flex-end;
}
.image::before {
content: "";
position: absolute;
height: 80%;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(-12deg, var(--background) 0%, transparent 50%);
}
.image > :global(img) {
height: 100%;
object-fit: contain;
@@ -50,7 +60,7 @@ const t = useTranslations(Astro);
}
.eye.left {
top: 29%;
right: 27%;
right: 28%;
}
.eye.right {
top: 31%;

View File

@@ -0,0 +1,45 @@
---
import markdownToText from "@helpers/markdownToText";
interface Props {
post: {
data: {
title: string;
description: string;
icon: string;
tags: string[];
};
collection: string;
body: string;
slug: string;
};
}
const { post } = Astro.props;
---
<a
href={`/${post.collection}/${post.slug.split("/").pop()}`}
class="rounded-diag-md border border-neutral p-4 overflow-hidden"
>
<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" />}
{post.data.title}
</h2>
<p class="text-ellipsis overflow-hidden line-clamp-2">
{post.data.description || markdownToText(post.body).slice(0, 200)}
</p>
{
post.data.tags && (
<div class="flex gap-2 mt-2">
{post.data.tags.map((tag) => (
<span class="text-xs border border-neutral p-2 rounded-md">
{tag}
</span>
))}
</div>
)
}
</a>

View File

@@ -0,0 +1,21 @@
---
import { useTranslations } from "@i18n/utils";
const t = useTranslations(Astro);
---
<div class="arrow flex items-center gap-2">
<img src="/images/arrow_a.svg" alt="Arrow" />
<p class="text-xl">{t("latest-projects")}</p>
</div>
<style>
.arrow {
position: absolute;
right: 0;
transform: rotate(10deg) translateX(110%) translateY(50px);
}
.arrow > p {
color: #414152;
transform: rotate(15deg) translateY(16px) translateX(4px);
}
</style>

View File

@@ -0,0 +1,21 @@
---
import { useTranslations } from "@i18n/utils";
const t = useTranslations(Astro);
---
<div class="arrow flex items-center gap-2">
<p class="text-xl">{t("latest-posts")}</p>
<img src="/images/arrow_b.svg" alt="Arrow" />
</div>
<style>
.arrow {
position: absolute;
left: 0;
transform: translateX(-110%) translateY(-160px);
}
.arrow > p {
color: #414152;
transform: rotate(-26deg) translateY(34px) translateX(-10px);
}
</style>

View File

@@ -2,6 +2,6 @@
export let classes = "";
</script>
<div class="flex flex-1 flex-col gap-4 {classes} w-[66%]">
<div class="flex flex-2 flex-col gap-4 {classes}">
<slot />
</div>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
export let alt: string;
export let src: string;
export let width: number;
export let height: number;
export let width: number | undefined;
export let height: number | undefined;
</script>
<img {src} {alt} {width} {height} />
<img {src} {alt} {width} {height} class="flex-1" />
<style>
img {

View File

@@ -1,11 +1,11 @@
<script lang="ts">
export let link: string;
export let text = "read more";
</script>
<a
href={link}
data-astro-prefetch
class="bg-light p-2 text-s rounded-md px-4 flex flex-0 items-center gap-2 w-fit"
>
read more <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" />
</a>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
export let classes = "";
</script>
<h2 class="text-2xl">
<h2 class="text-3xl {classes}">
<slot />
</h2>

View File

@@ -2,18 +2,8 @@
export let classes = "flex bg";
</script>
<div class="relative rounded-diag-md {classes} noise">
<div
class="relative rounded-diag-md {classes} noise card-wrapper items-stretch"
>
<slot />
</div>
<style>
.card-wrapper {
align-items: stretch;
min-height: 200px;
border: solid thin var(--outline);
max-width: 100%;
max-height: 300px;
overflow: hidden;
}
</style>

View File

@@ -5,7 +5,19 @@ headerImg: "images/Render.png"
comments: true
---
Die Inspiration diesen Render zu machen kam von einem Bild was 2013 auf dem Weg nach Italien entstanden ist.
<ImageGallery client:load/>
<Image
loading="eager"
src={ValleyView}
alt="ValleyView"
/>
import Image from "@components/Image.astro";
import ImageGallery from "@components/ImageGallery.svelte";
import ValleyView from "./images/Render.png";
import DSC_1601 from "./images/DSC_1601.jpg";
import Unbenannt1 from "./images/Unbenannt-1.jpg";
@@ -13,15 +25,8 @@ import Render4 from "./images/Render4.png";
import UntitledPng from "./images/untitled1-1.png"
import Render6 from "./images/Render6.png";
<Image
src={ValleyView}
alt="ValleyView"
/>
Die Inspiration diesen Render zu machen kam von einem Bild was 2013 auf dem Weg nach Italien entstanden ist.
<Image
loading="eager"
src={DSC_1601}
alt="DSC_1601"
/>

View File

@@ -1,9 +1,13 @@
---
title: "Random Renders NO°2"
date: 2023-05-11
headerImg: "images/render_05.png"
headerImg: "images/palma.png"
featured: true
comments: true
---
Last month I felt like creating some renderings again. I also found a few old renderings that haven't ended up on my blog yet.
import Image from "@components/Image.astro"
import Palma from "./images/palma.png"
import Render05 from "./images/render_05.png"
@@ -13,7 +17,6 @@ import Cabin_new from "./images/Cabin_new.png";
import Home from "./images/Home.png";
import Workroom from "./images/Workroom.png";
Last month I felt like creating some renderings again. I also found a few old renderings that haven't ended up on my blog yet.
## Palma
<Image src={Palma} alt="Duude" />

View File

@@ -1,38 +1,41 @@
---
title: "Random Renders NO°2"
date: 2023-05-11
headerImg: "images/render_05.png"
headerImg: "images/palma.png"
featured: true
comments: true
---
Im letzten Monat hatte ich wieder Lust, einige Renderings zu erstellen. Außerdem habe ich noch ein paar alte Renderings gefunden die noch nicht auf meinem Blog gelandet sind.
import ImageGallery from "@components/ImageGallery.svelte"
import Image from "@components/Image.astro"
import Palma from "./images/palma.png"
import Render05 from "./images/render_05.png"
import Poster_var2 from "./images/Poster_var2.jpg";
import Cabin_old from "./images/Cabin_old.jpg";
import Cabin_new from "./images/Cabin_new.png";
import Home from "./images/Home.png";
import Workroom from "./images/Workroom.png";
Im letzten Monat hatte ich wieder Lust, einige Renderings zu erstellen. Außerdem habe ich noch ein paar alte Renderings gefunden die noch nicht auf meinem Blog gelandet sind.
<ImageGallery client:load/>
## Palma
<Image
src={Palma}
alt="Duude"
/>
<Image src={Palma} alt="Duude" />
## [Sudoku.nvim](https://github.com/jim-fx/sudoku.nvim)
<Image
src={Render05}
alt="Duude"
/>
<Image src={Render05} alt="Duude" />
![](./images/Poster_var2.jpg)
<Image src={Poster_var2} alt="Poster_var2" />
## Super old renders:
![](./images/Cabin_old.jpg)
<Image src={Cabin_old} alt="Cabin_old" />
![](./images/Cabin_new.png)
<Image src={Cabin_new} alt="Cabin_new" />
![](./images/Home.png)
<Image src={Home} alt="Home" />
<Image src={Workroom} alt="Workroom" />
![](./images/Workroom.png)

View File

@@ -1,11 +1,13 @@
---
title: "Wie ich mein Server-Setup komplett neu aufbaue."
title: "Mein neues Server-Setup"
date: 2020-06-09
comments: true
---
import Image from "@components/Image.astro"
import ServerSetup from "./images/new-server.svg"
Dieses Thema beschäftigt mich also schon seit längerem. Und in den letzten zwei Wochen ist es mir endlich klar geworden. Dieser Blogbeitrag ist nicht als Leitfaden gedacht, sondern als Möglichkeit für mein zukünftiges Ich, die Entscheidungsprozesse meines jetzigen Ichs zu verstehen.
> **Mein Plan ist es, die Leitfäden später als separate Beiträge zu veröffentlichen:**
@@ -44,7 +46,7 @@ In meinem vorherigen Setup habe ich eine Mischung aus externen Diensten verwende
## My new Setup
<img src="./images/new-server.svg"/>
<Image src={ServerSetup} alt="Server Setup" />
Eine Frage, die man sich jetzt stellen könnte wäre: „Warum benutzt du Cloud und lokale Server?“ Beide haben einige Nachteile und einige Vorteile. Cloud-Server bieten schnelle Netzwerkgeschwindigkeiten und statische öffentliche IPs, aber die Speicherung großer Datenmengen ist recht teuer. Der Festplattenspeicher ist bei lokalen Servern vergleichsweise günstig. Ich habe etwa 60 € für meine 1-TB-Festplatte bezahlt, die speziell für NAS-Situationen entwickelt wurde, bei denen die Laufwerke rund um die Uhr laufen. Außerdem gefällt mir die Idee sehr gut, physischen Zugriff auf meine eigenen Daten zu haben und diese nicht einem Dritten anvertrauen zu müssen. Ein weiterer Vorteil des physischen Zugriffs auf meinen eigenen Server besteht darin, dass ich an Hardwareaspekten wie Netzwerk, Laufwerken und Kühlung herumbasteln und physische Messungen wie Raumtemperatur und Luftfeuchtigkeit vornehmen kann.

View File

@@ -2,6 +2,7 @@
import { onMount } from "svelte";
import createFishes from "./fishes/webgl-fishes";
import { Color } from "ogl";
import { rgbToHex } from "@helpers/colors";
let canvasBottom: HTMLCanvasElement;
@@ -26,20 +27,7 @@
fishCanvasBack.resize();
};
// function to turn css rgb() strings to hex
function rgbToHex(rgb: string) {
let hex = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
if (!hex) return rgb;
return (
"#" +
hex
.slice(1)
.map((x) => {
return ("0" + parseInt(x).toString(16)).slice(-2);
})
.join("")
);
}
let loaded = false;
onMount(async () => {
const background = window.getComputedStyle(document.body);
@@ -56,6 +44,7 @@
speed *= 0.99;
render && fishCanvasBack.update(t, timeOffset);
}
loaded = true;
});
</script>
@@ -69,7 +58,7 @@
}}
/>
<canvas id="bottom" bind:this={canvasBottom} />
<canvas id="bottom" bind:this={canvasBottom} class:loaded />
<!-- <canvas id="top" bind:this={canvasTop} /> -->
<style>
@@ -80,6 +69,11 @@
top: 0px;
left: 0px;
z-index: -1;
opacity: 0.5;
opacity: 0;
transition: opacity 0.5s;
}
canvas.loaded {
opacity: 0.2;
}
</style>

View File

@@ -1,7 +1,6 @@
---
title: "Zentralwerk_2051"
date: 2021-01-07
_layout: "transparent"
links: [["PDF", "/Zentralwerk_2051.pdf"]]
license: "CC-BY-SA:4.0"
comments: true

View File

@@ -5,8 +5,11 @@ const blogCollection = defineCollection({
title: z.string(),
date: z.date(),
headerImg: z.string().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(),
})
});

BIN
src/content/photos/bigge-changes/index.mdx (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/content/photos/portugal-2021/index.mdx (Stored with Git LFS)

Binary file not shown.

View File

@@ -1,7 +1,10 @@
---
title: "Argenti"
date: 2023-08-21
icon: "/projects/argenti.png"
draft: true
description: "A central database for things I enjoy stored in Markdown/Redis"
tags: ["deno", "fresh", "redis"]
links:
[
["live", "https://invoice.app.max-richter.dev"],

View File

@@ -2,6 +2,8 @@
title: "Invoice"
date: 2023-08-21
headerImg: "bg.jpg"
icon: "/projects/invoice.svg"
tags: ["sveltekit", "unocss", "prisma", "sqlite"]
draft: true
links:
[

View File

@@ -0,0 +1,10 @@
---
date: 2019-06-10
title: "Isyncrasy"
draft: false
icon: "/projects/isyncrasy/favicon.ico"
description: "A small fun virtual OS build with svelte"
tags: ["svelte", "web", "os"]
---
# Isyncrasy

View File

@@ -0,0 +1,23 @@
precision highp float;
precision highp int;
uniform vec3 uBackground;
varying vec3 vNormal;
varying vec2 vUv;
varying float fog;
void main(){
vec3 normal=normalize(vNormal);
float lighting=dot(normal,normalize(vec3(-.3,.8,.6)));
vec3 col = (vec3(.308*vUv.x,.712,.5)+lighting*.5)-.25;
/* gl_FragColor.rgb=vec3(vUv.x,vUv.y,vNormal.x); */
gl_FragColor.rgb=mix(col, uBackground,fog);
gl_FragColor.a=1.;
}

View File

@@ -0,0 +1,49 @@
attribute vec2 uv;
attribute vec3 position;
// Add instanced attributes just like any attribute
attribute vec3 offset;
attribute vec3 random;
attribute vec3 normal;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 normalMatrix;
uniform float uTime;
uniform float uRot;
varying vec2 vUv;
varying vec3 vNormal;
varying float fog;
void rotate2d(inout vec2 v, float a){
mat2 m = mat2(cos(a), -sin(a), sin(a), cos(a));
v = m * v;
}
void main() {
vUv = uv;
vNormal = normal;
/* vNormal = normalize(normalMatrix * normal); */
// copy position so that we can modify the instances
vec3 pos = position;
// scale first
fog = (offset.z+1.0)/2.0;
pos *= 0.4 + (1.0-fog) * 0.6;
// rotate around y axis
rotate2d(pos.xz, random.x * 6.28 + 1.0 * uTime * (random.y - 0.5));
rotate2d(pos.xy, random.y * 6.28 + 1.0 * uTime * (random.y - 0.5));
// rotate around x axis just to add some extra variation
rotate2d(pos.zy, random.z * 0.5 * sin(uTime * random.x + random.z * 3.14));
pos.y += sin(uRot/500.0+random.y*50.0)/5.0+(1.0-fog)*uRot;
pos += offset;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { onMount } from "svelte";
import { createLeaves, setRotation } from "./leaves";
let canvas: HTMLCanvasElement;
let loaded = false;
function handleScroll() {
setRotation(window.scrollY / window.innerHeight);
}
onMount(() => {
createLeaves({ canvas, num: 20, minZ: 0, maxZ: 1 });
setTimeout(() => {
loaded = true;
}, 100);
});
</script>
<svelte:window on:scroll={handleScroll} />
<canvas bind:this={canvas} class:loaded />
<style>
:global(body) {
z-index: 1;
}
canvas {
width: 100vw !important;
height: 100vh !important;
position: fixed;
top: 0px;
left: 0px;
z-index: -1;
pointer-events: none;
opacity: 0;
transition: opacity 2s;
}
.loaded {
opacity: 0.2;
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,88 @@
import { Camera, Color, Geometry, GLTFLoader, Mesh, Program, Renderer, Transform } from 'ogl';
import { rand, randMinMax } from "./random";
import fragment from "./Leaf.frag";
import vertex from "./Leaf.vert";
import { rgbToHex } from '@helpers/colors';
let rotation = 0;
export function setRotation(r: number) {
rotation = r;
}
export function createLeaves({ canvas, num = 20, alpha = false, minZ = -1, maxZ = 1 }: { canvas: HTMLCanvasElement, num?: number, alpha?: boolean, minZ?: number, maxZ?: number }) {
const renderer = new Renderer({ dpr: 1, canvas, alpha: true });
const gl = renderer.gl;
const background = window.getComputedStyle(document.body);
const d = new Color(rgbToHex(background.backgroundColor));
const camera = new Camera(gl, { fov: 15 });
camera.position.z = 5;
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
}
window.addEventListener('resize', resize, false);
resize();
const scene = new Transform();
const program = new Program(gl, {
vertex,
fragment,
cullFace: gl.NONE,
depthTest: true,
uniforms: {
uTime: { value: 0 },
uRot: { value: 0 },
uBackground: { value: d },
},
});
let mesh: Mesh;
loadModel();
async function loadModel() {
const model = await GLTFLoader.load(gl, "/models/leaf.glb");
const data = model.nodes[0].children[0].geometry.attributes;
let offset = new Float32Array(num * 3);
let random = new Float32Array(num * 3);
for (let i = 0; i < num; i++) {
offset.set([rand(8), rand(4), randMinMax(minZ, maxZ)], i * 3);
// unique random values are always handy for instances.
// Here they will be used for rotation, scale and movement.
random.set([Math.random(), Math.random(), Math.random()], i * 3);
}
const geometry = new Geometry(gl, {
...data,
// simply add the 'instanced' property to flag as an instanced attribute.
// set the value as the divisor number
offset: { instanced: 1, size: 3, data: offset },
random: { instanced: 1, size: 3, data: random },
});
mesh = new Mesh(gl, { geometry, program });
mesh.scale.set(0.2, 0.2, 0.2)
mesh.setParent(scene);
}
requestAnimationFrame(update);
function update(t: number) {
requestAnimationFrame(update);
program.uniforms.uTime.value = t * 0.001;
program.uniforms.uRot.value = rotation;
renderer.render({ scene, camera });
}
}

View File

@@ -0,0 +1,3 @@
export const rand = (r = 1) => (Math.random() * 2 - 1) * r;
export const randMinMax = (min = 0, max = 1) => min + Math.random() * (max - min)

BIN
src/content/projects/plantarium/images/plantarium.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -1,7 +1,7 @@
---
title: "Plantarium"
date: 2022-08-31
headerImg: "/projects/plantarium/plantarium.png"
headerImg: "images/plantarium.png"
featured: true
links: [["website", "https://plant.max-richter.com"], ["git", "https://github.com/jim-fx/plantarium"]]
draft: true

View File

@@ -1,7 +1,7 @@
---
title: "Plantarium"
date: 2022-08-31T20:46:26+02:00
headerImg: "/projects/plantarium/plantarium.png"
headerImg: "images/plantarium.png"
featured: true
links: [["website", "https://plant.max-richter.dev"], ["git", "https://github.com/jim-fx/plantarium"]]
tags: ["Web", "3D", "Svelte", "Node-Systeme"]
@@ -14,14 +14,31 @@ Plantarium ist wohl das Hobby Projekt, mit dem ich am meisten Zeit verbracht hab
Plantarium ist eine WebApp mit der Nutzer 3D Model von Pflanzen generieren können. Der erste Prototyp war innerhalb von zwei Wochen intensiver Arbeit fertig und sah ungefähr so aus:
<svelte component="image-slider" title="Prototype Design">
<img src="./images/page01-0.jpg" alt="Stem Page"/>
<img src="./images/page01-1.jpg" alt="Branches page"/>
<img src="./images/page01-2.jpg" alt="Leaves page"/>
<img src="./images/page01-3.jpg" alt="Import/Export page"/>
<img src="./images/page01-5.jpg" alt="Design of UI Components"/>
<img src="./images/page01-6.jpg" alt="Data flow inside app"/>
</svelte>
import ImageSlider from "@components/ImageSlider.svelte"
import Leaves from "./_components/Leaves.svelte"
import Image from "@components/Image.astro"
import page01_0 from "./images/page01-0.jpg"
import page01_1 from "./images/page01-1.jpg"
import page01_2 from "./images/page01-2.jpg"
import page01_3 from "./images/page01-3.jpg"
import page01_5 from "./images/page01-5.jpg"
import page01_6 from "./images/page01-6.jpg"
import screenshot_geometry_nodes from "./images/screenshot-geometry-nodes.jpg"
import screenshot_houdini from "./images/screenshot-houdini.jpg"
import screenshot_unreal from "./images/screenshot-unreal.jpg"
import screenshot_davinci from "./images/screenshot-davinci.jpg"
<Leaves client:load/>
<ImageSlider title="Erster Prototyp" client:load>
<Image src={page01_0} alt="Stem Page"/>
<Image src={page01_1} alt="Branches page"/>
<Image src={page01_2} alt="Leaves page"/>
<Image src={page01_3} alt="Import/Export page"/>
<Image src={page01_5} alt="Design of UI Components"/>
<Image src={page01_6} alt="Data flow inside app"/>
</ImageSlider>
Schon gar nicht schlecht, aber wie das mit Prototypen so ist gab es noch einiges zu verbessern. Also eine Kurze Historie der größten Änderungen bis heute:
@@ -56,13 +73,13 @@ In der Beispielgrafik haben wir zwei `input-color` nodes die jeweils eine Farbe
Das coole ist das man dieses System sehr generisch gestalten kann und zum beispiel eine `generate-stem`, `generate-branches` oder eine `add-leaves` node programmieren kann. Aufgrund der
<img src="/projects/plantarium/screenshot-plantarium.png" alt="Plantariums uses nodes to create plants"/>
<svelte component="image-slider" title="Beispiele von Node Systemen">
<img src="images/screenshot-geometry-nodes.jpg" alt="Blenders uses nodes to create geometry"/>
<img src="images/screenshot-houdini.jpg" alt="Houdini uses nodes for vfx/simulations"/>
<img src="images/screenshot-unreal.jpg" alt="Unreal uses nodes for game logic"/>
<img src="images/screenshot-davinci.jpg" alt="Davinvi uses nodes for vfx"/>
</svelte>
<ImageSlider title="Beispiele von Node Systemen" client:load>
<Image src={screenshot_geometry_nodes} alt="Blenders uses nodes to create geometry"/>
<Image src={screenshot_houdini} alt="Houdini uses nodes for vfx/simulations"/>
<Image src={screenshot_unreal} alt="Unreal uses nodes for game logic"/>
<Image src={screenshot_davinci} alt="Davinvi uses nodes for vfx"/>
</ImageSlider>
### Svelte-Kit Rewrite

14
src/helpers/colors.ts Normal file
View File

@@ -0,0 +1,14 @@
// function to turn css rgb() strings to hex
export function rgbToHex(rgb: string) {
let hex = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
if (!hex) return rgb;
return (
"#" +
hex
.slice(1)
.map((x) => {
return ("0" + parseInt(x).toString(16)).slice(-2);
})
.join("")
);
}

1033
src/helpers/exif.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,25 @@
import MarkdownIt from 'markdown-it';
const parser = new MarkdownIt();
export default function markdownToText(markdown: string): string {
return markdown.replace(/#|`|\*|_|~/g, '');
return parser
.render(markdown)
.split('\n')
.map((str) => str.trim())
.map((str) => {
return str.replace(/<\/?[^>]+(>|$)/g, '').split('\n');
})
.flat()
.filter((str) => !str.startsWith("import")
&& !str.startsWith("export")
&& !str.startsWith("#")
&& !str.startsWith("const")
&& !str.startsWith("function")
&& !str.startsWith("export")
&& !str.startsWith("import")
&& !str.startsWith("&lt;")
&& !str.startsWith("let")
&& str.length > 0
)
.join(' ');
}

View File

@@ -0,0 +1,49 @@
// Reasonable defaults
var PIXEL_STEP = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;
export default function normalizeWheel(/*object*/ event) /*object*/ {
var sX = 0, sY = 0, // spinX, spinY
pX = 0, pY = 0; // pixelX, pixelY
// Legacy
if ('detail' in event) { sY = event.detail; }
if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
// side scrolling on FF with DOMMouseScroll
if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
sX = sY;
sY = 0;
}
pX = sX * PIXEL_STEP;
pY = sY * PIXEL_STEP;
if ('deltaY' in event) { pY = event.deltaY; }
if ('deltaX' in event) { pX = event.deltaX; }
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) { // delta in LINE units
pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT;
} else { // delta in PAGE units
pX *= PAGE_HEIGHT;
pY *= PAGE_HEIGHT;
}
}
// Fall-back if spin cannot be determined
if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
return {
spinX: sX,
spinY: sY,
pixelX: pX,
pixelY: pY
};
}

View File

@@ -10,6 +10,11 @@ export const ui = {
en: {
"en": "English",
"de": "Deutsch",
"more-posts": "More Posts",
"more-projects": "More Projects",
"read-more": "Read More",
"latest-posts": "Latest Posts",
"latest-projects": "Latest Projects",
'home.title': 'Hi, Im Max :)',
'home.subtitle': 'Trained Media Designer, Blender Nerd, Developer and Hardware Tinkerer.',
'nav.blog': 'Blog',
@@ -20,6 +25,11 @@ export const ui = {
de: {
"en": "English",
"de": "Deutsch",
"more-posts": "Mehr Posts",
"more-projects": "Mehr Projekte",
"latest-posts": "Neueste Posts",
"latest-projects": "Neueste Projekte",
"read-more": "weiterlesen",
'home.title': 'Hi, ich bin Max :)',
'home.subtitle': 'Ausgebildeter Mediengestalter, Blender Nerd, Entwickler und Hardware Bastler.',
'nav.blog': 'Blog',

View File

@@ -29,5 +29,7 @@ export function filterCollection<T extends { id: string }>(collection: T[], loca
return collection.filter(post => {
const [_, lang] = parseSlug(post?.id);
return lang === locale;
}).sort((a, b) => {
return a.data.date > b.data.date ? -1 : 1;
});
}

View File

@@ -72,7 +72,7 @@ const { title, width = "compact" } = Astro.props;
<header>
<Nav />
</header>
<main class="flex flex-col gap-y-2xl">
<main class="flex flex-col mt-4xl gap-y-2xl">
<slot />
</main>
<LanguagePicker />

View File

@@ -2,7 +2,6 @@
import type { CollectionEntry } from "astro:content";
import Layout from "./Layout.astro";
import { useTranslatedPath } from "@i18n/utils";
import { getLocale } from "astro-i18n-aut";
type CustomProps = {
layout?: "normal" | "transparent";
@@ -10,52 +9,52 @@ type CustomProps = {
};
type Props = CollectionEntry<"blog">["data"] & CustomProps;
const { title, date, _layout = "normal", backlink = "/blog" } = Astro.props;
const { title, date, _layout, backlink = "/blog" } = Astro.props;
const path = useTranslatedPath(Astro);
---
<Layout title={title}>
<article class={`layout-${_layout}`}>
<div class="top-info">
<a href={path(backlink)}>← overview</a>
<div class="date">
{
date.toLocaleString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
</div>
<div
class="top-info flex items-center place-content-between opacity-50 m-y-4"
>
<a class="flex items-center gap-1" href={path(backlink)}
><span class="i-tabler-arrow-left"></span> overview</a
>
<div class="date">
{
date.toLocaleString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
</div>
<h1>{title}</h1>
</div>
<article class={`layout-${_layout} flex flex-col gap-2`}>
<h1 class="text-4xl my-4">{title}</h1>
<slot />
</article>
</Layout>
<style>
article {
margin-top: 50px;
display: flex;
flex-direction: column;
gap: 10px;
article :global(h2) {
@apply text-3xl;
}
article :global(h3) {
@apply text-2xl;
}
article :global(h4) {
@apply text-xl;
}
article :global(img) {
border-radius: 0.5rem;
}
article.layout-transparent {
padding: 20px;
background: var(--background);
}
.top-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1em;
color: var(--outline);
}
.top-info a {
text-decoration: none;
color: var(--outline);
}
</style>

View File

@@ -2,6 +2,8 @@
import { getCollection } from "astro:content";
const pages = await getCollection("blog");
import Layout from "@layouts/Layout.astro";
import HeroCard from "@components/HeroCard.astro";
import SmallCard from "@components/SmallCard.astro";
import { getLocale } from "astro-i18n-aut";
import { filterCollection } from "@i18n/utils";
@@ -9,17 +11,29 @@ import { filterCollection } from "@i18n/utils";
const locale = getLocale(Astro.url);
const posts = filterCollection(pages, locale);
const featuredPosts = await Promise.all(
posts.slice(0, 3).map(async (post) => {
if (!post.data.headerImg) {
return post;
}
const { default: image } = await import(
`../../content/blog/${post.slug.split("/")[0]}/${post.data.headerImg}`
);
return {
...post,
image,
};
}),
);
const otherPosts = posts.slice(3);
---
<Layout title="Dude">
<hr />
{
posts.map((post) => (
<>
<a href={"blog/" + post.slug.split("/")[0]}>{post.data.title}</a>
<br />
</>
))
}
<hr />
{featuredPosts.map((post) => <HeroCard post={post} />)}
<div class="grid grid-cols-2 gap-4 mt-4">
{otherPosts.map((post) => <SmallCard post={post} />)}
</div>
</Layout>

View File

@@ -1,54 +1,71 @@
---
import Layout from "@layouts/Layout.astro";
import Max from "@components/Max.astro";
import { Card } from "@components/card";
import { getCollection } from "astro:content";
import { filterCollection, useTranslatedPath } from "@i18n/utils";
import markdownToText from "@helpers/markdownToText";
import {
filterCollection,
useTranslatedPath,
useTranslations,
} from "@i18n/utils";
import { getLocale } from "astro-i18n-aut";
import HeroCard from "@components/HeroCard.astro";
import SmallCard from "@components/SmallCard.astro";
import LinkCard from "@components/LinkCard.astro";
import ArrowA from "@components/arrows/ArrowA.astro";
import ArrowB from "@components/arrows/ArrowB.astro";
const projects = filterCollection(
await getCollection("projects"),
getLocale(Astro.url),
);
const translatePath = useTranslatedPath(Astro);
const locale = getLocale(Astro.url);
const projectSlides = await Promise.all(
projects.map(async (project) => ({
title: project.data.title,
description: markdownToText(project.body),
image: project.data.headerImg,
link: translatePath(project.slug),
})),
);
const t = useTranslations(Astro);
const tp = useTranslatedPath(Astro);
const featuredProject = projects.find((project) => project.data?.featured);
const otherProjects = projects
.filter((project) => featuredProject !== project)
.sort((a) => (a?.data?.icon ? -1 : 1))
.slice(0, 3);
const posts = filterCollection(
await getCollection("blog"),
getLocale(Astro.url),
);
const featuredPost = posts.find((post) => post.data?.featured);
const otherPosts = posts.filter((post) => featuredPost !== post).slice(0, 3);
---
<Layout title="Max Richter">
<Max />
{
featuredProject && (
<Card classes="flex overflow-hidden gradient border-1 border-light">
<Card.Content classes="p-8">
<Card.Title>{featuredProject.data.title}</Card.Title>
<Card.Description>
{markdownToText(featuredProject.body).slice(0, 200)}
</Card.Description>
<Card.ReadMoreButton
link={translatePath(
`/projects/${featuredProject.slug.split("/")[0]}`,
)}
/>
</Card.Content>
<Card.Image
src={featuredProject.data.headerImg}
alt={featuredProject.data.title}
/>
</Card>
)
}
<section class="relative my-8">
<span class="i-tabler-circle-arrow-right-thin"></span>
<ArrowA />
{featuredProject && <HeroCard post={featuredProject} />}
<div class="grid grid-cols-2 gap-4 mt-4">
{
otherProjects.length > 0 &&
otherProjects.map((project) => <SmallCard post={project} />)
}
<LinkCard
link={tp("/projects")}
title={t("more-projects")}
icon="circle-arrow-right"
/>
</div>
</section>
<section class="relative my-8">
{featuredPost && <HeroCard post={featuredPost} />}
<ArrowB />
<div class="grid grid-cols-2 gap-4 mt-4">
{
otherPosts.length > 0 &&
otherPosts.map((post) => <SmallCard post={post} />)
}
<LinkCard link={tp("/blog")} title={t("more-posts")} />
</div>
</section>
</Layout>

View File

@@ -3,6 +3,7 @@ import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import { getLocale } from "astro-i18n-aut";
import { filterCollection } from "@i18n/utils";
import HeroCard from "@components/HeroCard.astro";
const locale = getLocale(Astro.url);
const pages = await getCollection("photos");
@@ -10,16 +11,5 @@ const posts = filterCollection(pages, locale);
---
<Layout title="Dude">
<hr />
{
posts.map((post) => (
<>
<>
<a href={"photos/" + post.slug.split("/")[0]}>{post.data.title}</a>
<br />
</>
</>
))
}
<hr />
{posts.map((post) => <HeroCard post={post} />)}
</Layout>

View File

@@ -3,6 +3,7 @@ import { getCollection } from "astro:content";
import Layout from "@layouts/Layout.astro";
import { getLocale } from "astro-i18n-aut";
import { filterCollection } from "@i18n/utils";
import HeroCard from "@components/HeroCard.astro";
const locale = getLocale(Astro.url);
const pages = await getCollection("projects");
@@ -10,16 +11,5 @@ const posts = filterCollection(pages, locale);
---
<Layout title="Dude">
<hr />
{
posts.map((post) => (
<>
<>
<a href={"projects/" + post.slug.split("/")[0]}>{post.data.title}</a>
<br />
</>
</>
))
}
<hr />
{posts.map((post) => <HeroCard post={post} />)}
</Layout>