Compare commits
6 commits
main
...
redo-landi
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fedf3ef1 | |||
| 3aabd391c1 | |||
| d154d2e937 | |||
| 5e09ae35f0 | |||
| 2943ace31b | |||
| 16e9bb1147 |
BIN
src/assets/projects/discretizeui/demo.png
Normal file
|
After Width: | Height: | Size: 373 KiB |
BIN
src/assets/projects/discretizeui/languages.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
src/assets/projects/discretizeui/tooltip.png
Normal file
|
After Width: | Height: | Size: 431 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/projects/videovault/dashboard.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
src/assets/projects/videovault/edit.png
Normal file
|
After Width: | Height: | Size: 835 KiB |
BIN
src/assets/projects/videovault/frontpage.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
src/assets/projects/videovault/player.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
|
@ -1,6 +1,4 @@
|
|||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import "../styles/global.css";
|
||||
import ubuntuRegularWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-400-normal.woff2?url";
|
||||
import ubuntuBoldWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-700-normal.woff2?url";
|
||||
|
|
|
|||
233
src/components/Carousel.astro
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
---
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
interface Props {
|
||||
images: {
|
||||
src: ImageMetadata;
|
||||
alt: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const { images } = Astro.props;
|
||||
|
||||
const carouselId = `carousel-${Math.random().toString(36).slice(2, 11)}`;
|
||||
const helpId = `${carouselId}-help`;
|
||||
---
|
||||
|
||||
<section
|
||||
id={carouselId}
|
||||
class="relative overflow-hidden rounded-lg"
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
aria-label="Image carousel"
|
||||
data-carousel-id={carouselId}
|
||||
tabindex="0"
|
||||
aria-describedby={helpId}
|
||||
>
|
||||
<!-- Slides -->
|
||||
<div class="relative h-0 pb-[56.25%]">
|
||||
{
|
||||
images.map((image, i) => (
|
||||
<img
|
||||
src={image.src.src}
|
||||
alt={image.alt}
|
||||
class={`
|
||||
absolute inset-0 w-full h-full object-cover object-top
|
||||
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"
|
||||
>
|
||||
<Icon name="mdi:chevron-left" class="h-5 w-5" />
|
||||
<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"
|
||||
>
|
||||
<Icon name="mdi:chevron-right" class="h-5 w-5" />
|
||||
<span class="sr-only">Next</span>
|
||||
</button>
|
||||
|
||||
<!-- Theater toggle button (bottom-right) -->
|
||||
<button
|
||||
type="button"
|
||||
class="absolute bottom-4 right-4 flex h-10 w-10 items-center justify-center
|
||||
rounded-full bg-white/30 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="Enter wide mode"
|
||||
aria-pressed="false"
|
||||
data-theater-toggle
|
||||
>
|
||||
<Icon name="mdi:arrow-expand-horizontal" class="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<!-- Live region for screen‑reader announcements -->
|
||||
<div
|
||||
class="sr-only"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
data-live-announcer
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Hidden helper text for screen readers -->
|
||||
<p id={helpId} class="sr-only">
|
||||
Carousel focused. Use Left and Right Arrow keys to change slides. Press T to
|
||||
toggle wide mode. Press Escape to exit wide mode.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<script define:vars={{ carouselId }}>
|
||||
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]");
|
||||
const theaterBtn = carousel.querySelector("[data-theater-toggle]");
|
||||
|
||||
let current = 0;
|
||||
const total = slides.length;
|
||||
let autoRotateTimer;
|
||||
let theater = false;
|
||||
|
||||
const updateTheaterButton = () => {
|
||||
if (!theaterBtn) return;
|
||||
theaterBtn.setAttribute("aria-pressed", theater ? "true" : "false");
|
||||
theaterBtn.setAttribute(
|
||||
"aria-label",
|
||||
theater ? "Exit wide mode" : "Enter wide mode",
|
||||
);
|
||||
const icon = theaterBtn.querySelector("svg");
|
||||
if (icon) {
|
||||
icon.setAttribute("data-icon-state", theater ? "collapse" : "expand");
|
||||
}
|
||||
};
|
||||
|
||||
const applyTheaterState = () => {
|
||||
const section = carousel.closest("[data-threecol]");
|
||||
if (!section) return;
|
||||
section.classList.toggle("theater-mode", theater);
|
||||
updateTheaterButton();
|
||||
};
|
||||
|
||||
const toggleTheater = () => {
|
||||
theater = !theater;
|
||||
applyTheaterState();
|
||||
};
|
||||
|
||||
const exitTheater = () => {
|
||||
if (!theater) return;
|
||||
theater = false;
|
||||
applyTheaterState();
|
||||
};
|
||||
|
||||
theaterBtn?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
toggleTheater();
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
if (liveAnnouncer) {
|
||||
liveAnnouncer.textContent = `Slide ${current + 1} of ${total}`;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Key handling (now works when the section itself is focused)
|
||||
carousel.addEventListener("keydown", (e) => {
|
||||
const target = e.target;
|
||||
const isTyping =
|
||||
target instanceof HTMLElement &&
|
||||
(target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable);
|
||||
if (isTyping) return;
|
||||
|
||||
if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
goPrev();
|
||||
} else if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
goNext();
|
||||
} else if (
|
||||
(e.key === "t" || e.key === "T") &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
!e.ctrlKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
toggleTheater();
|
||||
} else if (e.key === "Escape") {
|
||||
exitTheater();
|
||||
}
|
||||
});
|
||||
|
||||
const startAutoRotate = () => {
|
||||
stopAutoRotate();
|
||||
autoRotateTimer = window.setInterval(goNext, 5000);
|
||||
};
|
||||
const stopAutoRotate = () => {
|
||||
if (autoRotateTimer) {
|
||||
clearInterval(autoRotateTimer);
|
||||
autoRotateTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
startAutoRotate();
|
||||
|
||||
carousel.addEventListener("mouseenter", stopAutoRotate);
|
||||
carousel.addEventListener("mouseleave", startAutoRotate);
|
||||
carousel.addEventListener("focusin", stopAutoRotate);
|
||||
carousel.addEventListener("focusout", startAutoRotate);
|
||||
});
|
||||
</script>
|
||||
99
src/components/ProjectSection.astro
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { type Project } from "../consts";
|
||||
import Carousel from "./Carousel.astro";
|
||||
import ThreeColumnSection from "./ThreeColumnSection.astro";
|
||||
|
||||
interface Props {
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
const { projects } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
projects.map((project) => (
|
||||
<ThreeColumnSection maxWidth="8xl" py="py-8">
|
||||
{/* ---------- LEFT: Project details ---------- */}
|
||||
<div slot="left" 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}
|
||||
|
||||
{/* Duration */}
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 flex items-center mt-1">
|
||||
<Icon name="mdi:calendar" class="mr-2" />
|
||||
{project.duration}
|
||||
</p>
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p class="text-base text-slate-800 dark:text-slate-200">
|
||||
{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>
|
||||
|
||||
{/* ---------- 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>
|
||||
))
|
||||
}
|
||||
139
src/components/ThreeColumnSection.astro
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
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}`} data-threecol>
|
||||
<div
|
||||
class={`threecol-row ${maxWidthClasses[maxWidth]} mx-auto flex flex-col lg:flex-row gap-8 px-4 ${containerClass}`}
|
||||
>
|
||||
<!-- Left Column -->
|
||||
<div
|
||||
class={`${reverseOnMobile ? "order-3 lg:order-1" : "order-1"} flex-col`}
|
||||
data-left
|
||||
>
|
||||
<slot name="left" />
|
||||
</div>
|
||||
|
||||
<!-- Center Column -->
|
||||
<div class={`order-1 lg:order-2 flex-col`} data-center>
|
||||
<slot name="center" />
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div
|
||||
class={`${reverseOnMobile ? "order-1 lg:order-3" : "order-3"} flex-col`}
|
||||
data-right
|
||||
>
|
||||
<slot name="right" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Base layout variables */
|
||||
[data-threecol] .threecol-row {
|
||||
--center-initial: 640px; /* initial center width at lg+ */
|
||||
--transition-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Make sure columns can shrink properly */
|
||||
[data-threecol] [data-left],
|
||||
[data-threecol] [data-center],
|
||||
[data-threecol] [data-right] {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Large-screen flex behavior (mobile stays stacked naturally) */
|
||||
@media (min-width: 1024px) {
|
||||
[data-threecol] .threecol-row {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
[data-threecol] [data-left] {
|
||||
max-width: 33.33%;
|
||||
flex: 1 1 33.33%;
|
||||
/* optional: transition flex changes if you expect subtle movement */
|
||||
transition: flex 0.55s var(--transition-ease);
|
||||
}
|
||||
|
||||
[data-threecol] [data-center] {
|
||||
flex: 0 0 var(--center-initial);
|
||||
transition:
|
||||
flex-basis 0.55s var(--transition-ease),
|
||||
flex-grow 0.55s var(--transition-ease),
|
||||
max-width 0.55s var(--transition-ease);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-threecol] [data-right] {
|
||||
flex: 1 1 0;
|
||||
transition:
|
||||
flex-basis 0.55s var(--transition-ease),
|
||||
flex-grow 0.55s var(--transition-ease),
|
||||
opacity 0.35s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* THEATER MODE (expanded center) */
|
||||
[data-threecol].theater-mode [data-center] {
|
||||
flex: 1 1 0; /* allow it to fill remaining space */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[data-threecol].theater-mode [data-right] {
|
||||
flex: 0 0 0;
|
||||
flex-grow: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Optional: subtle emphasis on the center content while expanding */
|
||||
[data-threecol] .threecol-row :is([data-center]) .carousel-theater-target,
|
||||
[data-threecol] .threecol-row :is([data-center]) > * {
|
||||
transition:
|
||||
box-shadow 0.55s var(--transition-ease),
|
||||
transform 0.55s var(--transition-ease);
|
||||
}
|
||||
[data-threecol].theater-mode [data-center] .carousel-theater-target,
|
||||
[data-threecol].theater-mode [data-center] > * {
|
||||
/* Example visual polish (comment out if not desired) */
|
||||
/* box-shadow: 0 6px 24px -4px rgba(0,0,0,.25); */
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-threecol] [data-left],
|
||||
[data-threecol] [data-center],
|
||||
[data-threecol] [data-right] {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
194
src/consts.ts
|
|
@ -1,6 +1,196 @@
|
|||
// Place any global data in this file.
|
||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||
import type { ImageMetadata } from "astro";
|
||||
import discretizeui_demo from "./assets/projects/discretizeui/demo.png";
|
||||
import discretizeui_languages from "./assets/projects/discretizeui/languages.png";
|
||||
import discretizeui_tooltip from "./assets/projects/discretizeui/tooltip.png";
|
||||
import optimizer1 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_52_AM.png";
|
||||
import optimizer2 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_53_AM.png";
|
||||
import optimizer3 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_54_AM.png";
|
||||
import optimizer4 from "./assets/projects/optimizer/Discretize-Gear-Optimizer-08-05-2025_11_55_AM.png";
|
||||
import videovault_dashboard from "./assets/projects/videovault/dashboard.png";
|
||||
import videovault_edit from "./assets/projects/videovault/edit.png";
|
||||
import videovault_frontpage from "./assets/projects/videovault/frontpage.png";
|
||||
import videovault_player from "./assets/projects/videovault/player.png";
|
||||
|
||||
export const SITE_TITLE = "Alex Daichendt";
|
||||
export const SITE_DESCRIPTION =
|
||||
"Alex Daichendt's personal website, blog, and portfolio.";
|
||||
|
||||
export interface Project {
|
||||
title: string;
|
||||
featured: boolean;
|
||||
live_url?: string;
|
||||
repo_url?: string;
|
||||
description: string;
|
||||
tech_stack: string[];
|
||||
duration: string;
|
||||
deliverables: string[];
|
||||
company?: string;
|
||||
images: {
|
||||
src: ImageMetadata;
|
||||
alt: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const projects: Project[] = [
|
||||
{
|
||||
title: "VideoVault",
|
||||
featured: true,
|
||||
live_url: "https://videovault.shiverpeak.xyz/",
|
||||
repo_url: "https://github.com/AlexDaichendt/VideoVault",
|
||||
description:
|
||||
"A private, self-hosted video vault for your personal use. Supports multiple video transcodes, search, and is blazingly fast thanks to a backend written in Rust and a zero-js (vanilla) frontend.",
|
||||
tech_stack: [
|
||||
"Rust",
|
||||
"Axum",
|
||||
"sqlx",
|
||||
"Tera Templates",
|
||||
"Tailwind CSS",
|
||||
"Node.js",
|
||||
"pnpm",
|
||||
"cargo",
|
||||
"ffmpeg",
|
||||
],
|
||||
duration: "2025 - Present",
|
||||
deliverables: [
|
||||
"HLS Streaming of videos",
|
||||
"Video Scrubbing",
|
||||
"Aspect Ratio Awareness",
|
||||
"Multiple Video Transcodes",
|
||||
"External Transcoding Workers",
|
||||
"Responsive, lightweight design",
|
||||
"User Accounts via local, LDAP or OIDC",
|
||||
"Sharing with timestamps",
|
||||
"Quick and simple deployment (Docker)",
|
||||
],
|
||||
images: [
|
||||
{
|
||||
alt: "Frontpage",
|
||||
src: videovault_frontpage,
|
||||
},
|
||||
{
|
||||
alt: "VideoVault Dashboard",
|
||||
src: videovault_dashboard,
|
||||
},
|
||||
{
|
||||
alt: "VideoVault Video Player",
|
||||
src: videovault_player,
|
||||
},
|
||||
{
|
||||
alt: "VideoVault Edit",
|
||||
src: videovault_edit,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: "Discretize: Gear Optimizer",
|
||||
featured: true,
|
||||
live_url: "https://optimizer.discretize.eu/",
|
||||
repo_url: "https://github.com/discretize/discretize-gear-optimizer",
|
||||
description:
|
||||
"A gear optimizer for the popular MMORPG Guild Wars 2. The optimizer is used by thousands of players daily to find the best gear combinations for their characters.",
|
||||
tech_stack: ["React", "Redux", "Rust", "Vite", "MaterialUI"],
|
||||
duration: "2021 - Present",
|
||||
deliverables: [
|
||||
"User-friendly interface",
|
||||
"Rust/WASM calculation core",
|
||||
"Internationalization",
|
||||
"Keyboard Navigation",
|
||||
],
|
||||
images: [
|
||||
{
|
||||
src: optimizer1,
|
||||
alt: "Discretize Gear Optimizer Screenshot",
|
||||
},
|
||||
{
|
||||
src: optimizer2,
|
||||
alt: "Discretize Gear Optimizer Screenshot",
|
||||
},
|
||||
{
|
||||
src: optimizer3,
|
||||
alt: "Discretize Gear Optimizer Screenshot",
|
||||
},
|
||||
{
|
||||
src: optimizer4,
|
||||
alt: "Discretize Gear Optimizer Screenshot",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "@discretize/gw2-ui-new",
|
||||
featured: false,
|
||||
live_url: "https://discretize.github.io/discretize-ui/gw2-ui",
|
||||
repo_url: "https://github.com/discretize/discretize-ui",
|
||||
description: `A modern, lightweight React component library for Guild Wars 2 UI elements. Used by all Discretize applications.`,
|
||||
tech_stack: ["React", "TypeScript", "CSS Modules", "Storybook"],
|
||||
duration: "2023 – Present",
|
||||
deliverables: [
|
||||
"Refactored all components to TypeScript",
|
||||
"Replaced CSS-in-JS with CSS Modules",
|
||||
"Better performance by caching, batching",
|
||||
],
|
||||
images: [
|
||||
{
|
||||
src: discretizeui_demo,
|
||||
alt: "Production Demo of the component library",
|
||||
},
|
||||
|
||||
{
|
||||
src: discretizeui_tooltip,
|
||||
alt: "Tooltip component",
|
||||
},
|
||||
{
|
||||
src: discretizeui_languages,
|
||||
alt: "Supports multiple languages",
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// title: "Discretize -- Rewritten Website",
|
||||
// description:
|
||||
// "Rewritten website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Awaiting last few changes and content updates by players before deployment.",
|
||||
// live_url: "https://next.discretize.eu/",
|
||||
// repo_url: "https://github.com/discretize/discretize.eu-rewrite",
|
||||
// tech_stack: ["Astro", "React", "TypeScript", "Tailwind CSS"],
|
||||
// duration: "2022 - Present",
|
||||
// complexity: 5,
|
||||
// },
|
||||
// {
|
||||
// title: "Discretize -- CC Tool",
|
||||
// description:
|
||||
// "Allows players to create skill schedules with drag and drop. Used by high-end players to optimize and coordinate their gameplay.",
|
||||
// live_url: "https://cc-tool.pages.dev/",
|
||||
// repo_url: "https://github.com/discretize/cc-tool",
|
||||
// tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
|
||||
// duration: "2024 - Present",
|
||||
// complexity: 3,
|
||||
// },
|
||||
// {
|
||||
// title: "Discretize -- Random Builds",
|
||||
// description:
|
||||
// "Generates random builds for the popular MMORPG Guild Wars 2. Meant as a way to force players out of their comfort zone and try new things.",
|
||||
// live_url: "https://random-builds.discretize.eu/",
|
||||
// tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
|
||||
// duration: "2022",
|
||||
// complexity: 3,
|
||||
// },
|
||||
// {
|
||||
// title: "Discretize -- Old Website",
|
||||
// description:
|
||||
// "Currently deployed website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Inherited project from previous maintainer. Several hundred thousand monthly users.",
|
||||
// live_url: "https://discretize.eu/",
|
||||
// tech_stack: ["React", "Gatsby", "Material UI"],
|
||||
// duration: "2019 - Present",
|
||||
// complexity: 3,
|
||||
// },
|
||||
// {
|
||||
// title: "Minecraft LandLord Spigot Plugin",
|
||||
// live_url: "https://www.spigotmc.org/resources/landlord-2.44398/",
|
||||
// repo_url: "https://github.com/LandlordPlugin/LandLord",
|
||||
// description:
|
||||
// "Landlord aims to keep the Minecraft experience simple and fluid for players while also protecting their land. The idea for this plugin is to protect player builds with minimal game-play interference, while also allowing them to tweak the protection details in a simple and user-friendly way. Handed over the project to a new group of maintainers in 2019.",
|
||||
// tech_stack: ["Java"],
|
||||
// duration: "2017 - 2019",
|
||||
// complexity: 2,
|
||||
// },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ description: A guide on what is needed to get Linux running on the Huawei MateBo
|
|||
heroImage: ./images/matebook.jpg
|
||||
---
|
||||
|
||||
**UPDATE 24/08/2024**:
|
||||
Having used this machine for a year soon, I can not recommend it. None of my issues have been resolved, some even gotten worse. The laptop's battery life is poor, often barely reaching 3 hours while having a text editor open. I pretty much depend on plugging the laptop into the wall, all the time to not degrade the battery more than necessary. The webcam, fingerprint, S3 and Xe drivers are still all pretty much broken. Bluetooth also behaves a bit strangely where it sometimes requires multiple attempts to connect.
|
||||
|
||||
---
|
||||
|
||||
I recently bought a Huawei MateBook X Pro 2024. It is a beautiful laptop with a 3:2 aspect ratio display and a touchscreen. The laptop comes with Windows 11 preinstalled. However, I wanted to run Linux on it. Here is a guide on what is needed to get Linux running on the Huawei MateBook X Pro 2024.
|
||||
|
||||
Overall, the experience was okay, but not something I would recommend to an average user. There are a fair bit of quirks that need to be ironed out. Especially distros running older kernels will have a hard time. I am running CachyOS with the latest 6.13-rc1 kernel, more on that later.
|
||||
|
|
@ -59,10 +64,10 @@ The webcam is an ipu6-based camera. Support has been trickling in over the years
|
|||
|
||||
The fingerprint sensor is not supported at the moment. It does not even show up anywhere. One of those ingenious Goodix sensors that are not supported by the fprintd library.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Sources:
|
||||
|
||||
[^1]: https://www.kernelconfig.io/search?q=CONFIG_BOOTPARAM_HOTPLUG_CPU0&kernelversion=6.12.4&arch=x86
|
||||
|
||||
[^2]: https://github.com/intel/intel-lpmd
|
||||
|
|
|
|||
|
|
@ -1,4 +1,23 @@
|
|||
export const publications = [
|
||||
{
|
||||
authors: [
|
||||
"Alexander Daichendt",
|
||||
"Florian Wiedner",
|
||||
"Jonas Andre",
|
||||
"Georg Carle",
|
||||
],
|
||||
title:
|
||||
"Applicability of Hardware-Supported Containers in Low-Latency Networking",
|
||||
conference:
|
||||
"20th International Conference on Network and Service Management (CNSM 2024)",
|
||||
location: "Prague, Czech Republic",
|
||||
date: "Oct. 2024",
|
||||
links: {
|
||||
pdf: "http://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_2024_cnsm.pdf",
|
||||
homepage: "https://tumi8.github.io/applicability-hwsupported-containers",
|
||||
bibtex: "https://www.net.in.tum.de/publications/bibtex/Wied24CNSM.bib",
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
"Florian Wiedner",
|
||||
|
|
@ -20,25 +39,6 @@ export const publications = [
|
|||
bibtex: "/publications/bibtex/WiedHelm24Container.bib",
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
"Alexander Daichendt",
|
||||
"Florian Wiedner",
|
||||
"Jonas Andre",
|
||||
"Georg Carle",
|
||||
],
|
||||
title:
|
||||
"Applicability of Hardware-Supported Containers in Low-Latency Networking",
|
||||
conference:
|
||||
"20th International Conference on Network and Service Management (CNSM 2024)",
|
||||
location: "Prague, Czech Republic",
|
||||
date: "Oct. 2024",
|
||||
links: {
|
||||
pdf: "http://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_2024_cnsm.pdf",
|
||||
homepage: "https://tumi8.github.io/applicability-hwsupported-containers",
|
||||
bibtex: "https://www.net.in.tum.de/publications/bibtex/Wied24CNSM.bib",
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
"Florian Wiedner",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import me from "../assets/me.jpg";
|
||||
import { Image } from "astro:assets";
|
||||
import Picture from "../components/Picture.astro";
|
||||
import Link from "../components/Link.astro";
|
||||
|
||||
import Picture from "../components/Picture.astro";
|
||||
import ProjectSection from "../components/ProjectSection.astro";
|
||||
import ThreeColumnSection from "../components/ThreeColumnSection.astro";
|
||||
import { projects } from "../consts";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
const love = [
|
||||
{
|
||||
emoji: "🦀",
|
||||
|
|
@ -57,18 +58,10 @@ const skills = {
|
|||
subtitle="Software Engineer, Linux Enthusiast, Lightweight Systems Advocate"
|
||||
className="w-full py-16 flex flex-col"
|
||||
>
|
||||
<section
|
||||
class="max-w-8xl mx-auto
|
||||
grid grid-cols-1 lg:grid-cols-[auto,1fr,auto]
|
||||
items-center md:items-start
|
||||
gap-8
|
||||
py-8 md:py-12"
|
||||
>
|
||||
<!-- Left Spacer: Already responsive, no changes needed -->
|
||||
<div class="w-72 hidden md:block"></div>
|
||||
<ThreeColumnSection reverseOnMobile={true}>
|
||||
<div slot="left"></div>
|
||||
|
||||
<!-- Main Content: Add order to place it below the image on mobile -->
|
||||
<div class="max-w-2xl px-4 order-2 md:order-none">
|
||||
<div slot="center" class="max-w-2xl">
|
||||
<p class="mb-4">
|
||||
I am a privacy-first software engineer passionate about building web
|
||||
applications that are efficient, user-friendly, and respectful of your
|
||||
|
|
@ -86,14 +79,18 @@ const skills = {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Picture: Place it first on mobile using order -->
|
||||
<div class="px-4 order-1 md:order-none">
|
||||
<Picture src={me} alt="me" class="w-48 md:w-64 h-auto mx-auto" />
|
||||
<div slot="right" class="flex justify-center md:justify-start items-start">
|
||||
<Picture
|
||||
src={me}
|
||||
alt="My profile picture"
|
||||
class="w-48 md:w-64 h-auto rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</ThreeColumnSection>
|
||||
|
||||
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
|
||||
<div class="max-w-2xl mx-auto px-4">
|
||||
<ThreeColumnSection bgClass="bg-mytheme-200/70 dark:bg-mytheme-700/50">
|
||||
<div slot="left"></div>
|
||||
<div slot="center" class="max-w-2xl">
|
||||
<h2
|
||||
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
|
|
@ -112,10 +109,12 @@ const skills = {
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div slot="right"></div>
|
||||
</ThreeColumnSection>
|
||||
|
||||
<section class="py-16">
|
||||
<div class="max-w-2xl mx-auto px-4">
|
||||
<ThreeColumnSection>
|
||||
<div slot="left"></div>
|
||||
<div slot="center">
|
||||
<h2
|
||||
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
|
|
@ -140,5 +139,20 @@ const skills = {
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="right"></div>
|
||||
</ThreeColumnSection>
|
||||
|
||||
<section class="py-16 bg-mytheme-200/70 dark:bg-mytheme-700/50">
|
||||
<h2
|
||||
class="text-3xl md:text-4xl font-bold mb-12 text-center text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
Software I developed
|
||||
</h2>
|
||||
|
||||
<div class="space-y-16">
|
||||
<ProjectSection
|
||||
projects={projects.filter((project) => project.featured)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
|
|
|||
|
|
@ -1,238 +1,13 @@
|
|||
---
|
||||
import ProjectSection from "../../components/ProjectSection.astro";
|
||||
import { projects } from "../../consts";
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
const projects = [
|
||||
{
|
||||
title: "daichendt.one (this site)",
|
||||
live_url: "https://www.youtube.com/watch?v=XfELJU1mRMg",
|
||||
repo_url: "https://github.com/AlexDaichendt/site",
|
||||
description: "Personal website and blog.",
|
||||
tech_stack: ["Astro", "Tailwind CSS"],
|
||||
duration: "2022 - Present",
|
||||
complexity: 4,
|
||||
},
|
||||
// {
|
||||
// title: "Radio Player",
|
||||
// live_url: "https://video.taxi/en/functions/simultaneous-interpreting/",
|
||||
// description: "A radio player for the VIDEO.TAXI website. Allows users to listen to radio stations with dubbed audio with the voice of the original speaker.",
|
||||
// tech_stack: ["React", "TypeScript", "Vite", "Go", "Docker"],
|
||||
// duration: "2024 - Present",
|
||||
// complexity: 4,
|
||||
// company: "TV1 GmbH",
|
||||
// },
|
||||
{
|
||||
title: "VIDEO.TAXI Meetings",
|
||||
live_url: "https://meetings.video.taxi/",
|
||||
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.",
|
||||
tech_stack: [
|
||||
"Svelte",
|
||||
"TypeScript",
|
||||
"Tailwind CSS",
|
||||
"Express",
|
||||
"GraphQL",
|
||||
"PostgreSQL",
|
||||
"Docker",
|
||||
"OpenAPI",
|
||||
],
|
||||
complexity: 5,
|
||||
duration: "2024 - Present",
|
||||
company: "TV1 GmbH",
|
||||
},
|
||||
{
|
||||
title: "Netbox PDU Plugin",
|
||||
repo_url: "https://github.com/AlexDaichendt/axians-netbox-plugin-pdu",
|
||||
description:
|
||||
"Netbox plugin to read out power usage of PDUs. Forked and maintained from the original plugin by Axians. Used in production by multiple companies.",
|
||||
tech_stack: ["Python", "Django", "Netbox"],
|
||||
duration: "2023 - Present",
|
||||
complexity: 4,
|
||||
company: "TV1 GmbH",
|
||||
},
|
||||
{
|
||||
title: "Latency IRQ Analyzer",
|
||||
repo_url: "https://github.com/AlexDaichendt/latency-irq-analyzer",
|
||||
description:
|
||||
"Quick uni project for overlaying latency files with IRQ data.",
|
||||
tech_stack: ["NodeJS", "Highcharts"],
|
||||
duration: "2024",
|
||||
complexity: 2,
|
||||
},
|
||||
{
|
||||
title: "Netbox Agent",
|
||||
description:
|
||||
"Reads out lshw, dmidecode and other data of a server and creates Netbox devices. Forked from the original project.",
|
||||
tech_stack: ["Python"],
|
||||
duration: "2023 - Present",
|
||||
complexity: 2,
|
||||
company: "TV1 GmbH",
|
||||
},
|
||||
{
|
||||
title: "magewell-exporter",
|
||||
repo_url: "https://github.com/TV1-EU/magewell-exporter",
|
||||
description:
|
||||
"Prometheus exporter for Magewell AiO encoders. Allows monitoring of the capture card status and video signal. Used in production by TV1.",
|
||||
tech_stack: ["NodeJs", "Typescript"],
|
||||
duration: "2023",
|
||||
complexity: 4,
|
||||
company: "TV1 GmbH",
|
||||
},
|
||||
{
|
||||
title: "Discretize -- Gear Optimizer",
|
||||
live_url: "https://optimizer.discretize.eu/",
|
||||
repo_url: "https://github.com/discretize/discretize-gear-optimizer",
|
||||
description:
|
||||
"A gear optimizer for the popular MMORPG Guild Wars 2. The optimizer is used by thousands of players daily to find the best gear combinations for their characters.",
|
||||
tech_stack: ["React", "Redux", "Rust", "Vite", "MaterialUI"],
|
||||
duration: "2021 - Present",
|
||||
complexity: 5,
|
||||
},
|
||||
{
|
||||
title: "Discretize -- UI Library",
|
||||
repo_url: "https://github.com/discretize/discretize-ui",
|
||||
description:
|
||||
"A beautiful component library with tooltips for the popular MMORPG Guild Wars 2. Allows websites to look and feel like the game. Integral part of the Discretize ecosystem.",
|
||||
live_url:
|
||||
"https://discretize.github.io/discretize-ui/gw2-ui/?path=/story/components-attribute--boon-duration",
|
||||
tech_stack: ["React", "TypeScript", "Storybook"],
|
||||
duration: "2021 - Present",
|
||||
complexity: 5,
|
||||
},
|
||||
{
|
||||
title: "Discretize -- Rewritten Website",
|
||||
description:
|
||||
"Rewritten website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Awaiting last few changes and content updates by players before deployment.",
|
||||
live_url: "https://next.discretize.eu/",
|
||||
repo_url: "https://github.com/discretize/discretize.eu-rewrite",
|
||||
tech_stack: ["Astro", "React", "TypeScript", "Tailwind CSS"],
|
||||
duration: "2022 - Present",
|
||||
complexity: 5,
|
||||
},
|
||||
{
|
||||
title: "Discretize -- CC Tool",
|
||||
description:
|
||||
"Allows players to create skill schedules with drag and drop. Used by high-end players to optimize and coordinate their gameplay.",
|
||||
live_url: "https://cc-tool.pages.dev/",
|
||||
repo_url: "https://github.com/discretize/cc-tool",
|
||||
tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
|
||||
duration: "2024 - Present",
|
||||
complexity: 3,
|
||||
},
|
||||
{
|
||||
title: "Discretize -- Random Builds",
|
||||
description:
|
||||
"Generates random builds for the popular MMORPG Guild Wars 2. Meant as a way to force players out of their comfort zone and try new things.",
|
||||
live_url: "https://random-builds.discretize.eu/",
|
||||
tech_stack: ["Vite", "React", "TypeScript", "Tailwind CSS"],
|
||||
duration: "2022",
|
||||
complexity: 3,
|
||||
},
|
||||
{
|
||||
title: "Discretize -- Old Website",
|
||||
description:
|
||||
"Currently deployed website for the Discretize community. Contains guides, builds, and other useful information for the popular MMORPG Guild Wars 2. Inherited project from previous maintainer. Several hundred thousand monthly users.",
|
||||
live_url: "https://discretize.eu/",
|
||||
tech_stack: ["React", "Gatsby", "Material UI"],
|
||||
duration: "2019 - Present",
|
||||
complexity: 3,
|
||||
},
|
||||
{
|
||||
title: "Minecraft LandLord Spigot Plugin",
|
||||
live_url: "https://www.spigotmc.org/resources/landlord-2.44398/",
|
||||
repo_url: "https://github.com/LandlordPlugin/LandLord",
|
||||
description:
|
||||
"Landlord aims to keep the Minecraft experience simple and fluid for players while also protecting their land. The idea for this plugin is to protect player builds with minimal game-play interference, while also allowing them to tweak the protection details in a simple and user-friendly way. Handed over the project to a new group of maintainers in 2019.",
|
||||
tech_stack: ["Java"],
|
||||
duration: "2017 - 2019",
|
||||
complexity: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const getCardStyle = (company?: string) => {
|
||||
const baseStyles =
|
||||
"rounded-lg shadow-lg p-6 transition-colors duration-300 mb-8";
|
||||
|
||||
if (!company) {
|
||||
return `${baseStyles} bg-white dark:bg-gray-800`;
|
||||
}
|
||||
|
||||
const companyColors: Record<string, string> = {
|
||||
Discretize: "bg-blue-50 dark:bg-blue-900/30",
|
||||
"TV1 GmbH": "border-2 border-orange-500 dark:border-orange-500",
|
||||
};
|
||||
|
||||
return `${baseStyles} ${companyColors[company] || "bg-white dark:bg-gray-800"}`;
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout title="Projects" subtitle="Selected Passion Projects I am proud of">
|
||||
<p>
|
||||
Here are some of the projects I have worked on in the past. They are sorted
|
||||
by my personal rating of relevancy. Projects done for a company are marked
|
||||
with the company name and have a special border color.
|
||||
</p>
|
||||
{
|
||||
projects
|
||||
.sort((a, b) => b.complexity - a.complexity)
|
||||
.map((project) => (
|
||||
<article class={getCardStyle(project.company)}>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{project.title}
|
||||
</h2>
|
||||
{project?.company && (
|
||||
<div class="flex items-center mt-1">
|
||||
<Icon name="mdi:office-building" class="w-5 h-5 mr-1" />
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300 font-medium">
|
||||
{project?.company}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{project.duration}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{project.tech_stack.map((tech) => (
|
||||
<span class="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-full">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
{project.live_url && (
|
||||
<a
|
||||
href={project.live_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 dark:text-blue-400 hover:underline flex items-center"
|
||||
>
|
||||
<Icon name="mdi:web" class="w-5 h-5 mr-1" />
|
||||
Live Demo
|
||||
</a>
|
||||
)}
|
||||
{project.repo_url && (
|
||||
<a
|
||||
href={project.repo_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-600 dark:text-gray-400 hover:underline flex items-center"
|
||||
>
|
||||
<Icon name="mdi:github" class="w-5 h-5 mr-1" />
|
||||
Repository
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
<BaseLayout
|
||||
title="Projects"
|
||||
subtitle="Selected Passion Projects I am proud of"
|
||||
className="w-full py-16 flex flex-col"
|
||||
>
|
||||
<ProjectSection projects={projects} />
|
||||
</BaseLayout>
|
||||
|
|
|
|||
|
|
@ -100,8 +100,7 @@ blockquote {
|
|||
font-size: 1.333em;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgb(var(--gray-light));
|
||||
@apply mb-8 border-t-2 border-t-mytheme-500 dark:border-t-mytheme-300 rounded-sm;
|
||||
}
|
||||
ul:not(ul ul, ol ul),
|
||||
ol:not(ul ol, ol ol) {
|
||||
|
|
|
|||