more project sectioning

This commit is contained in:
Alexander Daichendt 2025-08-14 22:20:44 +02:00
parent 16e9bb1147
commit 2943ace31b
5 changed files with 401 additions and 70 deletions

View file

@ -0,0 +1,214 @@
---
// ──────────────────────────────────────────────────────────────
// Types & Props
// ──────────────────────────────────────────────────────────────
import type { ImageMetadata } from "astro";
interface Props {
/** Array of images for the carousel */
images: {
/** Astro image metadata we only need the `src` field */
src: ImageMetadata;
/** Alt text for the image (required for accessibility) */
alt: string;
}[];
}
const { images } = Astro.props;
// ──────────────────────────────────────────────────────────────
// Unique ID needed so multiple carousels on the same page dont
// clash when we query the DOM from the <script> block.
// ──────────────────────────────────────────────────────────────
const carouselId = `carousel-${Math.random().toString(36).slice(2, 11)}`;
---
<!-- ──────────────────────────────────────────────────────────────
Carousel markup all styling is done with Tailwind classes.
The outer <section> gets the proper ARIA roles/labels.
────────────────────────────────────────────────────────────── -->
<section
id={carouselId}
class="relative overflow-hidden rounded-lg"
role="region"
aria-roledescription="carousel"
aria-label="Image carousel"
data-carousel-id={carouselId}
>
<!-- Slides -->
<div class="relative h-0 pb-[56.25%]">
<!-- 16:9 ratio placeholder -->
{
images.map((image, i) => (
<img
src={image.src.src}
alt={image.alt}
class={`
absolute inset-0 w-full h-full object-cover
transition-opacity duration-500 ease-in-out
${i === 0 ? "opacity-100" : "opacity-0"}
carousel-slide
`}
data-index={i}
/>
))
}
</div>
<!-- Navigation Buttons -->
<button
type="button"
class="absolute left-4 top-1/2 -translate-y-1/2
flex h-10 w-10 items-center justify-center
rounded-full bg-white/70 backdrop-blur-sm
text-gray-800 hover:bg-white focus:outline-none
focus-visible:ring-2 focus-visible:ring-indigo-600
transition-colors"
aria-label="Previous slide"
data-dir="prev"
>
<!-- Heroicon: chevronleft -->
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"
></path>
</svg>
<span class="sr-only">Previous</span>
</button>
<button
type="button"
class="absolute right-4 top-1/2 -translate-y-1/2
flex h-10 w-10 items-center justify-center
rounded-full bg-white/70 backdrop-blur-sm
text-gray-800 hover:bg-white focus:outline-none
focus-visible:ring-2 focus-visible:ring-indigo-600
transition-colors"
aria-label="Next slide"
data-dir="next"
>
<!-- Heroicon: chevronright -->
<svg
class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"
></path>
</svg>
<span class="sr-only">Next</span>
</button>
<!-- Live region for screenreader announcements -->
<div
class="sr-only"
aria-live="polite"
aria-atomic="true"
data-live-announcer
>
</div>
</section>
<!-- ──────────────────────────────────────────────────────────────
Tailwind utilities are already available in the project.
No extra CSS is required everything lives in the classes above.
────────────────────────────────────────────────────────────── -->
<script define:vars={{ carouselId }}>
// -----------------------------------------------------------------
// Carousel logic runs once the component is in the DOM.
// -----------------------------------------------------------------
document.addEventListener("DOMContentLoaded", () => {
const carousel = document.getElementById(carouselId);
if (!carousel) return;
const slides = carousel.querySelectorAll(".carousel-slide");
const prevBtn = carousel.querySelector('[data-dir="prev"]');
const nextBtn = carousel.querySelector('[data-dir="next"]');
const liveAnnouncer = carousel.querySelector("[data-live-announcer]");
let current = 0;
const total = slides.length;
let autoRotateTimer;
// -----------------------------------------------------------------
// Helper show a slide by index (with fade transition)
// -----------------------------------------------------------------
const showSlide = (index) => {
slides[current].classList.remove("opacity-100");
slides[current].classList.add("opacity-0");
current = index;
slides[current].classList.remove("opacity-0");
slides[current].classList.add("opacity-100");
// Update screenreader announcement
if (liveAnnouncer) {
liveAnnouncerContent = `Slide ${current + 1} of ${total}`;
}
};
// -----------------------------------------------------------------
// Navigation button handlers
// -----------------------------------------------------------------
const goPrev = () => {
const prev = (current - 1 + total) % total;
showSlide(prev);
};
const goNext = () => {
const next = (current + 1) % total;
showSlide(next);
};
prevBtn?.addEventListener("click", goPrev);
nextBtn?.addEventListener("click", goNext);
// -----------------------------------------------------------------
// Keyboard navigation (left / right arrows)
// -----------------------------------------------------------------
carousel.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") {
e.preventDefault();
goPrev();
} else if (e.key === "ArrowRight") {
e.preventDefault();
goNext();
}
});
// -----------------------------------------------------------------
// Autorotate (pause on hover / focus)
// -----------------------------------------------------------------
const startAutoRotate = () => {
stopAutoRotate(); // safety
autoRotateTimer = window.setInterval(goNext, 5000);
};
const stopAutoRotate = () => {
if (autoRotateTimer) {
clearInterval(autoRotateTimer);
autoRotateTimer = undefined;
}
};
// Start rotating when the component mounts
startAutoRotate();
// Pause when the user hovers or focuses inside the carousel
carousel.addEventListener("mouseenter", stopAutoRotate);
carousel.addEventListener("mouseleave", startAutoRotate);
carousel.addEventListener("focusin", stopAutoRotate);
carousel.addEventListener("focusout", startAutoRotate);
});
</script>

View file

@ -1,6 +1,8 @@
--- ---
import { Picture } from "astro:assets"; import { Icon } from "astro-icon/components";
import { projects } from "../consts"; import { projects } from "../consts";
import Carousel from "./Carousel.astro";
import ThreeColumnSection from "./ThreeColumnSection.astro";
--- ---
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50"> <section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
@ -10,31 +12,95 @@ import { projects } from "../consts";
Software I developed Software I developed
</h2> </h2>
<div <div class="space-y-16">
class="max-w-7xl mx-auto {
grid grid-cols-1 lg:grid-cols-[auto,1fr,auto] projects.map((project) => (
items-center md:items-start <ThreeColumnSection maxWidth="8xl" py="py-8">
gap-8 {/* ---------- LEFT: Project details ---------- */}
py-8 md:py-12" <div
> slot="left"
<div class="w-72 md:block">project details</div> class="max-w-xs justify-self-center lg:justify-self-end"
>
<article class="space-y-4">
{/* Title + optional company */}
<h3 class="text-2xl font-semibold text-slate-900 dark:text-slate-100">
{project.title}
<div class="max-w-2xl px-4 order-2 md:order-none"> {/* Duration */}
{ <p class="text-sm text-slate-500 dark:text-slate-400 flex items-center mt-1">
projects.map((project, index) => ( <Icon name="mdi:calendar" class="mr-2" />
<div class=""> {project.duration}
-- TODO: make it rotate through images, create new component for </p>
this </h3>
<Picture
src={project.images[0].src} {/* Description */}
alt={project.images[0].alt} <p class="text-base text-slate-800 dark:text-slate-200">
class="w-full" {project.description}
/> </p>
{/* Tech stack */}
<div class="flex flex-wrap gap-2 mt-2">
{project.tech_stack.map((tech) => (
<span class="px-2 py-0.5 text-xs font-medium bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-slate-100 rounded">
{tech}
</span>
))}
</div>
{/* Deliverables (optional) */}
{project.deliverables?.length && (
<ul class="list-disc list-inside text-sm text-slate-700 dark:text-slate-300 mt-3">
{project.deliverables.map((item) => (
<li class="mb-1">{item}</li>
))}
</ul>
)}
{/* Links */}
<div class="flex space-x-4 mt-4">
{project.live_url && (
<a
href={project.live_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium
transition-colors duration-150
bg-slate-100 hover:bg-slate-200 focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-slate-500
dark:bg-slate-800 dark:hover:bg-slate-700 dark:focus-visible:outline-slate-400"
>
<Icon name="mdi:aspect-ratio" /> Live
</a>
)}
{project.repo_url && (
<a
href={project.repo_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded px-3 py-1.5 text-sm font-medium
transition-colors duration-150
bg-slate-100 hover:bg-slate-200 focus-visible:outline focus-visible:outline-2
focus-visible:outline-offset-2 focus-visible:outline-slate-500
dark:bg-slate-800 dark:hover:bg-slate-700 dark:focus-visible:outline-slate-400"
>
<Icon name="mdi:github" /> Repo
</a>
)}
</div>
</article>
</div> </div>
))
}
</div>
<div class="w-48 md-w-64 md:block hidden"></div> {/* ---------- CENTER: Carousel ---------- */}
<div slot="center" class="w-full">
<div class="rounded-lg overflow-hidden shadow-lg">
<Carousel images={project.images} />
</div>
</div>
{/* ---------- RIGHT: Empty for now ---------- */}
<div slot="right" class="hidden lg:block" />
</ThreeColumnSection>
))
}
</div> </div>
</section> </section>

View file

@ -0,0 +1,49 @@
---
export interface Props {
bgClass?: string;
containerClass?: string;
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "4xl" | "6xl" | "8xl";
py?: string;
reverseOnMobile?: boolean;
}
const {
bgClass = "",
containerClass = "",
maxWidth = "8xl",
py = "py-8 md:py-12",
reverseOnMobile = false,
} = Astro.props;
const maxWidthClasses = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
"2xl": "max-w-2xl",
"4xl": "max-w-4xl",
"6xl": "max-w-6xl",
"8xl": "max-w-8xl",
};
---
<section class={`${bgClass} ${py}`}>
<div
class={`${maxWidthClasses[maxWidth]} mx-auto grid grid-cols-1 lg:grid-cols-[1fr_640px_1fr] items-center md:items-start gap-8 px-4 ${containerClass}`}
>
<!-- Left Column (Hidden on mobile, visible on desktop) -->
<div class={`${reverseOnMobile ? "order-3 lg:order-1" : "order-1"}`}>
<slot name="left" />
</div>
<!-- Center Column (Main content) -->
<div class={`order-1 lg:order-2`}>
<slot name="center" />
</div>
<!-- Right Column -->
<div class={`${reverseOnMobile ? "order-1 lg:order-3" : "order-3"}`}>
<slot name="right" />
</div>
</div>
</section>

View file

@ -45,29 +45,30 @@ export const projects: Project[] = [
// complexity: 4, // complexity: 4,
// company: "TV1 GmbH", // company: "TV1 GmbH",
// }, // },
// { {
// title: "VIDEO.TAXI Meetings", title: "VIDEO.TAXI Meetings",
// live_url: "https://meetings.video.taxi/", live_url: "https://meetings.video.taxi/",
// description: description:
// "Records, transcribes, and translates meetings (Webex, Teams, Zoom). Interfaces with the GraphQL API of VIDEO.TAXI. Architecure, development, and deployment all done by me. Greenfield project.", "Records, transcribes, and translates meetings (Webex, Teams, Zoom). Interfaces with the GraphQL API of VIDEO.TAXI. Architecure, development, and deployment all done by me. Greenfield project.",
// tech_stack: [ tech_stack: [
// "Svelte", "Svelte",
// "TypeScript", "TypeScript",
// "Tailwind CSS", "Tailwind CSS",
// "Express", "Express",
// "GraphQL", "GraphQL",
// "PostgreSQL", "PostgreSQL",
// "Docker", "Docker",
// "OpenAPI", "OpenAPI",
// ], ],
// duration: "2024 - Present", duration: "2024 - Present",
// company: "TV1 GmbH", company: "TV1 GmbH",
// deliverables: [ deliverables: [
// "Live Updating Dashboard", "Live Updating Dashboard",
// "Meeting Bots for Teams, Zoom and Webex", "Meeting Bots for Teams, Zoom and Webex",
// "Keycloak integration", "Keycloak integration",
// ], ],
// }, images: [],
},
// { // {
// title: "Netbox PDU Plugin", // title: "Netbox PDU Plugin",
// repo_url: "https://github.com/AlexDaichendt/axians-netbox-plugin-pdu", // repo_url: "https://github.com/AlexDaichendt/axians-netbox-plugin-pdu",
@ -107,7 +108,7 @@ export const projects: Project[] = [
// company: "TV1 GmbH", // company: "TV1 GmbH",
// }, // },
{ {
title: "Discretize -- Gear Optimizer", title: "Discretize: Gear Optimizer",
live_url: "https://optimizer.discretize.eu/", live_url: "https://optimizer.discretize.eu/",
repo_url: "https://github.com/discretize/discretize-gear-optimizer", repo_url: "https://github.com/discretize/discretize-gear-optimizer",
description: description:

View file

@ -1,11 +1,10 @@
--- ---
import BaseLayout from "../layouts/BaseLayout.astro";
import me from "../assets/me.jpg"; import me from "../assets/me.jpg";
import { Image } from "astro:assets";
import Picture from "../components/Picture.astro";
import Link from "../components/Link.astro"; import Link from "../components/Link.astro";
import { projects } from "../consts"; import Picture from "../components/Picture.astro";
import ProjectSection from "../components/ProjectSection.astro"; import ProjectSection from "../components/ProjectSection.astro";
import ThreeColumnSection from "../components/ThreeColumnSection.astro";
import BaseLayout from "../layouts/BaseLayout.astro";
const love = [ const love = [
{ {
@ -59,16 +58,10 @@ const skills = {
subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate" subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate"
className="w-full py-16 flex flex-col" className="w-full py-16 flex flex-col"
> >
<section <ThreeColumnSection reverseOnMobile={true}>
class="max-w-8xl mx-auto <div slot="left"></div>
grid grid-cols-1 lg:grid-cols-[auto,1fr,auto]
items-center md:items-start
gap-8
py-8 md:py-12"
>
<div class="w-72 hidden md:block"></div>
<div class="max-w-2xl px-4 order-2 md:order-none"> <div slot="center" class="max-w-2xl">
<p class="mb-4"> <p class="mb-4">
I am a privacy-first software engineer passionate about building web I am a privacy-first software engineer passionate about building web
applications that are efficient, user-friendly, and respectful of your applications that are efficient, user-friendly, and respectful of your
@ -86,13 +79,18 @@ const skills = {
</p> </p>
</div> </div>
<div class="px-4 order-1 md:order-none"> <div slot="right" class="flex justify-center md:justify-start items-start">
<Picture src={me} alt="me" class="w-48 md:w-64 h-auto mx-auto" /> <Picture
src={me}
alt="My profile picture"
class="w-48 md:w-64 h-auto rounded-lg"
/>
</div> </div>
</section> </ThreeColumnSection>
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50"> <ThreeColumnSection bgClass="bg-mytheme-200/70 dark:bg-mytheme-700/50">
<div class="max-w-2xl mx-auto px-4"> <div slot="left"></div>
<div slot="center" class="max-w-2xl">
<h2 <h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100" class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
> >
@ -111,10 +109,12 @@ const skills = {
} }
</div> </div>
</div> </div>
</section> <div slot="right"></div>
</ThreeColumnSection>
<section class="py-16"> <ThreeColumnSection>
<div class="max-w-2xl mx-auto px-4"> <div slot="left"></div>
<div slot="center">
<h2 <h2
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100" class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
> >
@ -139,7 +139,8 @@ const skills = {
} }
</div> </div>
</div> </div>
</section> <div slot="right"></div>
</ThreeColumnSection>
<ProjectSection /> <ProjectSection />
</BaseLayout> </BaseLayout>