feat: dark mode toggle

This commit is contained in:
Alejandro Laguna
2025-08-22 17:04:23 +02:00
parent cc8794a13f
commit b1929aa216
11 changed files with 186 additions and 52 deletions

View File

@@ -11,15 +11,15 @@ export interface Props {
const { title, company, period, location, achievements, technologies } = Astro.props;
---
<div class="p-4 bg-neutral-50 rounded-2xl border border-neutral-200 mx-14">
<h3 class="text-xl font-semibold text-neutral-900">{title}</h3>
<p class="text-sm text-neutral-600">{company}, {period}, {location}</p>
<ul class="list-disc list-inside text-sm text-neutral-700 mt-2 space-y-1">
<div class="p-4 bg-neutral-50 dark:bg-neutral-800 rounded-2xl border border-neutral-200 dark:border-neutral-700 mx-14">
<h3 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100">{title}</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400">{company}, {period}, {location}</p>
<ul class="list-disc list-inside text-sm text-neutral-700 dark:text-neutral-300 mt-2 space-y-1">
{achievements.map(achievement => (
<li>{achievement}</li>
))}
</ul>
<p class="text-xs text-neutral-500 mt-2">
<p class="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{technologies}
</p>
</div>

View File

@@ -11,13 +11,13 @@ const { href, icon: Icon, heading, content } = Astro.props;
<a
href={href}
class="nav-card group block p-6 rounded-2xl bg-white border border-neutral-200/80 shadow-sm hover:shadow-lg hover:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 focus:border-neutral-400"
class="nav-card group block p-6 rounded-2xl bg-white dark:bg-neutral-800 border border-neutral-200/80 dark:border-neutral-700 shadow-sm hover:shadow-lg hover:border-neutral-300 dark:hover:border-neutral-600 focus:outline-none focus:ring-2 focus:ring-neutral-900 dark:focus:ring-neutral-100 focus:ring-offset-2 dark:focus:ring-offset-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-500 transition-colors duration-200"
>
<div class="flex flex-col gap-4">
<Icon class="w-7 h-7 text-neutral-500 group-hover:text-neutral-900 transition-colors duration-200" />
<Icon class="w-7 h-7 text-neutral-500 dark:text-neutral-400 group-hover:text-neutral-900 dark:group-hover:text-neutral-100 transition-colors duration-200" />
<div>
<h3 class="text-2xl font-semibold text-neutral-900 mb-2">{heading}</h3>
<p class="text-sm text-neutral-600 leading-relaxed">{content}</p>
<h3 class="text-2xl font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{heading}</h3>
<p class="text-sm text-neutral-600 dark:text-neutral-400 leading-relaxed">{content}</p>
</div>
</div>
</a>

16
src/content.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const workshop = defineCollection({
loader: glob({ base: './src/content/workshop', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: image().optional(),
}),
});
export const collections = { workshop };

View File

@@ -0,0 +1,16 @@
---
title: 'First post'
description: 'Lorem ipsum dolor sit amet'
pubDate: '2025-08-03'
tags: []
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.

View File

@@ -1,6 +1,6 @@
---
import "../styles/global.css"
import { NotepadText, PencilRuler, LibraryBig, Fingerprint, House } from "lucide-astro";
import { House, Sun, Moon } from "lucide-astro";
---
<!doctype html>
<html lang="en" class="scroll-smooth">
@@ -15,28 +15,56 @@ import { NotepadText, PencilRuler, LibraryBig, Fingerprint, House } from "lucide
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
</head>
<body class="bg-neutral-50 text-neutral-900 min-h-screen antialiased">
<div class="fixed inset-0 bg-[linear-gradient(rgba(0,0,0,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(0,0,0,0.03)_1px,transparent_1px)] bg-[size:32px_32px] pointer-events-none" aria-hidden="true"></div>
<header class="relative z-10 w-full px-6 sm:px-8 py-8 border-b border-neutral-200/80 bg-white backdrop-blur-md">
<div class="max-w-6xl mx-auto flex items-center justify-between">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-neutral-900">Alejandro Laguna</h1>
<p class="text-sm text-neutral-600 mt-1 font-medium">My digital corner — science, code, thoughts.</p>
</div>
<a href="/" aria-label="Home" class="text-neutral-600 hover:text-neutral-900 transition-colors bg-neutral-100 p-2 rounded-lg">
<House class="w-7 h-7 text-neutral-500 group-hover:text-neutral-900 transition-colors duration-200" />
</a>
</div>
</header>
<main class="relative z-10 max-w-6xl mx-auto px-6 sm:px-8 py-16 sm:py-20 min-h-screen">
<slot />
</main>
<footer class="relative z-10 w-full px-6 sm:px-8 py-8 border-b backdrop-blur-md py-12 border-t border-neutral-200/80 bg-white">
<div class="text-center">
<p class="text-sm text-neutral-500">
© 2025 Alejandro Laguna. Made with a lot of love.
</p>
</div>
</footer>
<body class="bg-neutral-50 text-neutral-900 dark:bg-neutral-900 dark:text-neutral-100 min-h-screen antialiased transition-colors duration-200">
<div class="fixed inset-0 bg-[linear-gradient(rgba(0,0,0,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(0,0,0,0.03)_1px,transparent_1px)] dark:bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:32px_32px] pointer-events-none" aria-hidden="true"></div>
<header class="relative z-10 w-full px-6 sm:px-8 py-8 border-b border-neutral-200/80 dark:border-neutral-800 bg-white/80 dark:bg-neutral-950/80 backdrop-blur-md">
<div class="max-w-6xl mx-auto flex items-center justify-between">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-neutral-900 dark:text-neutral-100">Alejandro Laguna</h1>
<p class="text-sm text-neutral-600 dark:text-neutral-400 mt-1 font-medium">My digital corner — science, code, thoughts.</p>
</div>
<div class="flex items-center gap-2">
<a href="/" aria-label="Home" class="text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100 transition-colors bg-neutral-100 dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700 p-2 rounded-lg group">
<House class="w-6 h-6 text-neutral-500 dark:text-neutral-400 group-hover:text-neutral-900 dark:group-hover:text-neutral-100 transition-colors duration-200" />
</a>
<button id="theme-toggle" aria-label="Toggle theme" class="text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100 transition-colors bg-neutral-100 dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-700 p-2 rounded-lg group">
<Sun id="sun-icon" class="w-6 h-6 text-neutral-500 dark:text-neutral-400 group-hover:text-neutral-900 dark:group-hover:text-neutral-100 transition-colors duration-200 dark:hidden" />
<Moon id="moon-icon" class="w-6 h-6 text-neutral-500 dark:text-neutral-400 group-hover:text-neutral-900 dark:group-hover:text-neutral-100 transition-colors duration-200 hidden dark:block" />
</button>
</div>
</div>
</header>
<main class="relative z-10 max-w-6xl mx-auto px-6 sm:px-8 py-16 sm:py-20 min-h-screen">
<slot />
</main>
<footer class="relative z-10 w-full px-6 sm:px-8 py-12 border-t border-neutral-200/80 dark:border-neutral-800 bg-white/80 dark:bg-neutral-950/80 backdrop-blur-md">
<div class="text-center">
<p class="text-sm text-neutral-500 dark:text-neutral-400">
© 2025 Alejandro Laguna. Made with a lot of love.
</p>
</div>
</footer>
<script>
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const isDark = getThemePreference() === 'dark';
document.documentElement.classList.toggle('dark', isDark);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
document.getElementById('theme-toggle')?.addEventListener('click', () => {
const isDark = document.documentElement.classList.toggle('dark');
if (typeof localStorage !== 'undefined') {
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
---
import Layout from "./Layout.astro"
export interface Props {
title: string,
description: string,
pubDate: Date,
}
const {title, description, pubDate} = Astro.props;
---
<Layout>
<h1 class="text-4xl sm:text-5xl font-bold mb-6 text-neutral-900">{title}</h1>
<main class="text-lg text-neutral-600 leading-relaxed text-justify">
<slot />
</main>
</Layout>

View File

@@ -72,29 +72,29 @@ const experiences = [
<Layout>
<section class="max-w-3xl mb-10 sm:mb-14">
<h1 class="text-4xl sm:text-5xl font-bold mb-6 text-neutral-900">About</h1>
<p class="text-lg text-neutral-600 leading-relaxed text-justify">
<h1 class="text-4xl sm:text-5xl font-bold mb-6 text-neutral-900 dark:text-neutral-100">About</h1>
<p class="text-lg text-neutral-600 dark:text-neutral-400 leading-relaxed text-justify">
I'm a second year computer science student at the European Institute of Technology, still trying to figure out my place but enjoying the journey through systems programming, webdev and other fields. Outside of code, my favorite writer is Isaac Asimov, my favorite piece of music is Spiegel im Spiegel, and my favorite book is The Witches by Roald Dahl. My favorite star is Vega because it was once our north star and will be again in about 12,000 years - something poetic about that cycle. And that's pretty much everything you need to know about me, except maybe my tea preferences, but we'll save that for another time... </p>
</section>
<section class="mb-12 bg-neutral-50 p-6 rounded-2xl border border-neutral-200">
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 mb-4">
<GraduationCap class="w-6 h-6 text-neutral-500" /> Education
<section class="mb-12 bg-neutral-50 dark:bg-neutral-800 p-6 rounded-2xl border border-neutral-200 dark:border-neutral-700">
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
<GraduationCap class="w-6 h-6 text-neutral-500 dark:text-neutral-400" /> Education
</h2>
<ul class="space-y-4">
<li>
<p class="text-base text-neutral-900 font-medium">Bachelor + Masters Degree in Computer Science</p>
<p class="text-sm text-neutral-600">European Institute of Technology, class of 2029</p>
<p class="text-base text-neutral-900 dark:text-neutral-100 font-medium">Bachelor + Master's Degree in Computer Science</p>
<p class="text-sm text-neutral-600 dark:text-neutral-400">European Institute of Technology, class of 2029</p>
</li>
<li>
<p class="text-base text-neutral-900 font-medium">Vocational Studies in Networking, Microcomputers and Operative Systems</p>
<p class="text-sm text-neutral-600">INS Cendrassos, class of 2024</p>
<p class="text-base text-neutral-900 dark:text-neutral-100 font-medium">Vocational Studies in Networking, Microcomputers and Operative Systems</p>
<p class="text-sm text-neutral-600 dark:text-neutral-400">INS Cendrassos, class of 2024</p>
</li>
</ul>
</section>
<section class="mb-12">
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 mb-4">
<Briefcase class="w-6 h-6 text-neutral-500" /> Experience
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
<Briefcase class="w-6 h-6 text-neutral-500 dark:text-neutral-400" /> Experience
</h2>
<div class="grid sm:grid-cols-1 gap-6">
{experiences.map(exp => (
@@ -103,10 +103,10 @@ const experiences = [
</div>
</section>
<section class="mb-12">
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 mb-4">
<Wrench class="w-6 h-6 text-neutral-500" /> Tools I Use
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
<Wrench class="w-6 h-6 text-neutral-500 dark:text-neutral-400" /> Tools I Use
</h2>
<code class="block bg-neutral-50 p-4 rounded-xl text-neutral-800 overflow-x-auto border border-neutral-200">
<code class="block bg-neutral-50 dark:bg-neutral-800 p-4 rounded-xl text-neutral-800 dark:text-neutral-200 overflow-x-auto border border-neutral-200 dark:border-neutral-700">
<b>Languages:</b> C, Go, Python, Javascript/Typescript, PHP<br>
<b>Frameworks & Libraries:</b> Astro, React, Preact, NextJS, Vue, NuxtJS, Express, FastAPI, TailwindCSS <br>
<b>Databases:</b> MongoDB, PostgreSQL, MySQL/MariaDB, ClickHouse <br>
@@ -115,8 +115,8 @@ const experiences = [
</section>
<section class="mb-20 sm:mb-28" aria-labelledby="navigation-heading">
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 mb-4">
<Feather class="w-6 h-6 text-neutral-500" /> Projects, posts, thoughts...
<h2 class="flex items-center gap-2 text-2xl font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
<Feather class="w-6 h-6 text-neutral-500 dark:text-neutral-400" /> Projects, posts, thoughts...
</h2>
<div class="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-3 gap-5">

View File

@@ -33,10 +33,10 @@ const navItems = [
---
<Layout>
<section class="max-w-4xl mb-20 sm:mb-28">
<h2 class="text-4xl sm:text-5xl lg:text-6xl font-bold leading-[1.1] mb-6 text-neutral-900">
A <span class="text-neutral-600"> notebook </span> for <br>working things out.
<h2 class="text-4xl sm:text-5xl lg:text-6xl font-bold leading-[1.1] mb-6 text-neutral-900 dark:text-neutral-100">
A <span class="text-neutral-600 dark:text-neutral-400"> notebook </span> for <br>working things out.
</h2>
<p class="text-lg sm:text-xl text-neutral-600 leading-relaxed max-w-2xl">
<p class="text-lg sm:text-xl text-neutral-600 dark:text-neutral-400 leading-relaxed max-w-2xl">
Unpolished notes, working prototypes, honest questions. A space for building understanding from the ground up. Trying to understand the world by writing and building.
</p>
</section>

View File

@@ -0,0 +1,20 @@
---
import { type CollectionEntry, getCollection, render } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('workshop');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<'workshop'>;
const post = Astro.props;
const { Content } = await render(post);
---
<PostLayout {...post.data}>
<Content />
</PostLayout>

View File

@@ -0,0 +1,34 @@
---
import "../../styles/global.css"
import Layout from "../../layouts/Layout.astro"
import { getCollection } from 'astro:content';
const posts = (await getCollection('workshop')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<Layout>
<section class="max-w-3xl mb-10 sm:mb-14">
<h1 class="text-4xl sm:text-5xl font-bold mb-6 text-neutral-900 dark:text-neutral-100">Workshop</h1>
<p class="text-lg text-neutral-600 dark:text-neutral-400 leading-relaxed text-justify">
I'm a second year computer science student at the European Institute of Technology, still trying to figure out my place but enjoying the journey through systems programming, webdev and other fields. Outside of code, my favorite writer is Isaac Asimov, my favorite piece of music is Spiegel im Spiegel, and my favorite book is The Witches by Roald Dahl. My favorite star is Vega because it was once our north star and will be again in about 12,000 years - something poetic about that cycle. And that's pretty much everything you need to know about me, except maybe my tea preferences, but we'll save that for another time... </p>
</section>
<ul class="space-y-4">
{posts.map((post) => (
<li class="p-4 rounded-xl border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 hover:shadow-md transition-shadow">
<a href={`/workshop/${post.id}/`} class="block">
<h4 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100 hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors">
{post.data.title}
</h4>
{post.data.pubDate && (
<p class="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
{new Date(post.data.pubDate).toLocaleDateString()}
</p>
)}
</a>
</li>
))}
</ul>
</Layout>

View File

@@ -7,6 +7,8 @@
--font-mono: 'JetBrains Mono', monospace;
}
@custom-variant dark (&:where(.dark, .dark *));
body {
font-family: var(--font-sans);
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1;