refined design
Some checks failed
Next.js App CI / docker (push) Failing after 3m19s

This commit is contained in:
2025-09-14 22:49:23 +02:00
parent 78de337446
commit 0444067c2d
89 changed files with 1117 additions and 594 deletions

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
# Git
.git
.gitignore
# Docker
Dockerfile
.dockerignore
# Dependencies
node_modules
# Build output
.next
# Local environment variables
.env.local
# PNPM specific
# pnpm-lock.yaml

View File

@@ -4,7 +4,7 @@ This document provides context for AI agents working on this project.
## Project Overview
This is a personal website and portfolio built with Next.js and Tailwind CSS. The site is statically generated, with content sourced from local `.mdx` files. The primary purpose is to showcase research, projects, and blog posts.
This is a personal website and portfolio built with Next.js and Tailwind CSS. The site is statically generated, with content sourced from local `.mdx` files. The primary purpose is to showcase experience, and publications posts.
## Tech Stack
@@ -24,7 +24,7 @@ This is a personal website and portfolio built with Next.js and Tailwind CSS. Th
## Project Structure
- `content/`: Root directory for all MDX content, organized into subdirectories by category (e.g., `research`, `projects`, `blog`).
- `content/`: Root directory for all MDX content, organized into subdirectories by category (e.g., `research`, `experience`).
- `src/app/`: Next.js App Router structure. Pages are dynamically generated based on the content in the `content/` directory.
- `src/lib/mdx.ts`: Core logic for finding, parsing, and serializing MDX files. It reads the file contents, separates frontmatter using `gray-matter`, and prepares it for rendering.
- `src/components/`: Contains all reusable React components.

View File

@@ -1,3 +1,39 @@
FROM nginx:latest
COPY ./_site/. /usr/share/nginx/html/.
COPY ./nginx_default.conf /etc/nginx/conf.d/default.conf
# Stage 1: Builder
FROM node:20-alpine AS builder
# Enable pnpm
RUN corepack enable
WORKDIR /app
# Copy dependency files and install dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy the rest of the application source code
COPY . .
# Run the build command
RUN pnpm run build
# ---
# Stage 2: Runner
FROM node:20-alpine AS runner
WORKDIR /app
# Create a non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
# Copy the standalone output from the builder stage
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
EXPOSE 3000
ENV PORT 3000
# The standalone output creates a server.js file that is the entrypoint
CMD ["node", "server.js"]

View File

@@ -9,8 +9,7 @@ Built with next.js, [shadcn/ui](https://ui.shadcn.com/), and [magic ui](https://
# Features
- Setup only takes a few minutes by editing the [single config file](./src/data/resume.tsx)
- Built using Next.js 14, React, Typescript, Shadcn/UI, TailwindCSS, Framer Motion, Magic UI
- Includes a blog
- Built using Next.js 15, React, Typescript, Shadcn/UI, TailwindCSS, React/Motion, Magic UI
- Responsive for different devices
- Optimized for Next.js and Vercel

View File

@@ -1,80 +0,0 @@
import { getPostBySlug, getPostSlugs } from '@/lib/mdx';
import { CitationProvider } from '@/components/context-citation';
import { ReferencesContainer } from '@/components/container-references';
import { notFound } from 'next/navigation';
import Image from 'next/image';
import { DATA } from '@/app/resume';
import { getPublicationsData } from '@/lib/publications';
import { CustomMDX } from '@/components/mdx-custom';
import Link from 'next/link';
export async function generateStaticParams() {
const slugs = getPostSlugs('blog');
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({ params: { slug } }: { params: { slug: string } }) {
const post = await getPostBySlug('blog', slug);
if (!post) {
return {};
}
return {
title: post.frontmatter.title,
description: post.frontmatter.description || DATA.description,
};
}
export default async function BlogPage({ params: { slug } }: { params: { slug: string } }) {
const post = await getPostBySlug('blog', slug);
const publications = getPublicationsData();
if (!post) {
return notFound();
}
return (
<CitationProvider publications={publications}>
<main className="flex flex-col min-h-[100dvh]">
<article className="prose prose-stone dark:prose-invert max-w-none">
<header className="mb-8">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl">
{post.frontmatter.title}
</h1>
{post.frontmatter.excerpt && (
<p className="text-lg text-muted-foreground mt-2">{post.frontmatter.excerpt}</p>
)}
{post.frontmatter.icon && (
<div className="mt-4">
<Image
src={post.frontmatter.icon}
alt={`${post.frontmatter.title} icon`}
width={64}
height={64}
className="rounded-full"
/>
</div>
)}
</header>
<CustomMDX {...post.source} />
{post.frontmatter.tags && (
<div className="flex flex-wrap gap-2 mt-8">
{post.frontmatter.tags.map((tag: string) => (
<Link
key={tag}
href={`/tags/${tag}`}
className="px-3 py-1 text-sm font-medium bg-secondary text-secondary-foreground rounded-full hover:bg-primary hover:text-primary-foreground transition-colors"
>
{tag}
</Link>
))}
</div>
)}
<ReferencesContainer />
</article>
</main>
</CitationProvider>
);
}

View File

@@ -1,49 +0,0 @@
import { getSortedPostsData } from "@/lib/posts";
import { ProjectCard } from "@/components/project-card";
import { BlurFade } from "@/components/magicui/blur-fade";
const BLUR_FADE_DELAY = 0.04;
export default function BlogPage() {
const posts = getSortedPostsData("blog");
return (
<main className="flex flex-col min-h-[100dvh] space-y-10">
<section id="blog">
<div className="mx-auto w-full max-w-6xl space-y-8">
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none">
Blog
</h1>
<p className="text-muted-foreground">
Musings on technology, research, and life.
</p>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{posts
.filter((post) => post.title)
.map((post, id) => (
<BlurFade
key={post.title}
delay={BLUR_FADE_DELAY * 2 + id * 0.05}
>
<ProjectCard
href={post.href}
key={post.title}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
tags={post.tags}
image={post.image || ""}
video={post.video}
links={[]}
/>
</BlurFade>
))}
</div>
</div>
</section>
</main>
);
}

View File

@@ -1,13 +1,11 @@
import { DATA } from "@/app/resume";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { CenteredImage } from "@/components/centered-image";
import { BlurFade } from "@/components/magicui/blur-fade";
import { TrackedLink } from "@/components/util-tracked-link";
const BLUR_FADE_DELAY = 0.05;
const BLUR_FADE_DELAY = 0.01;
export default function ConnectPage() {
const featuredSocials = ["Email", "LinkedIn", "GoogleScholar", "arXiv", "ResearchGate", "Gitea"];
@@ -16,10 +14,9 @@ export default function ConnectPage() {
return (
<main
className="fixed inset-0 flex flex-col items-center justify-center bg-background"
className="inset-0 flex flex-col items-center justify-center bg-background"
>
<div className="flex flex-col items-center space-y-8 text-center max-w-sm w-full p-6">
<BlurFade delay={BLUR_FADE_DELAY * 1}>
<Image
src="/images/newshot_2.jpg"
@@ -35,11 +32,10 @@ export default function ConnectPage() {
Dr. Steffen Illium
</h1>
</BlurFade>
<BlurFade delay={BLUR_FADE_DELAY * 3}>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-2 w-full">
{socialLinks.map(([name, social]) => (
<TrackedLink href={social.url} key={name} eventName={`${name}-social`} target="_blank">
<TrackedLink className="truncate" href={social.url} key={name} eventName={`${name}-social`} target="_blank">
<Button variant="outline" className="w-full">
<social.icon className="size-4 mr-2" />
{name}
@@ -49,26 +45,20 @@ export default function ConnectPage() {
</div>
</BlurFade>
<div className="flex w-full flex-col items-center space-y-4 pb-8">
<div className="w-full px-8">
<BlurFade delay={BLUR_FADE_DELAY * 4}>
<hr className="w-full" />
</BlurFade>
</div>
<div className="flex w-full flex-col items-center space-y-4 pb-4">
<BlurFade delay={BLUR_FADE_DELAY * 5}>
<a href="/images/qr.png" download="SteffenIllium-QRCode.png">
<Image
src="/images/qr.png"
alt="QR Code to connect"
width={256}
height={256}
width={240}
height={240}
className="rounded-xl shadow-lg hover:opacity-80 transition-opacity"
/>
</a>
</BlurFade>
</div>
</div>
</main>
);

View File

@@ -1,30 +1,43 @@
import { getPostBySlug, getPostSlugs } from '@/lib/mdx';
import { getPostBySlug, getPostSlugs, Post } from '@/lib/mdx';
import { notFound } from 'next/navigation';
import { DATA } from '@/app/resume';
import { getPublicationsData } from '@/lib/publications';
import { Article } from '@/components/page-article';
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateStaticParams() {
const slugs = getPostSlugs('experience');
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
// FIX: Await params to get slug for Next.js 15
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
if (!slug) {
return {};
}
const post = await getPostBySlug('experience', slug);
if (!post) { return {}; }
if (!post) {
return {};
}
return {
title: post.frontmatter.title,
description: post.frontmatter.teaser || DATA.description,
};
}
export default async function ExperiencePage({ params }: { params: { slug: string } }) {
// FIX: Await params to get slug for Next.js 15
export default async function ExperiencePage({ params }: Props) {
const { slug } = await params;
if (!slug) {
notFound();
}
const post = await getPostBySlug('experience', slug);
const publications = getPublicationsData();
@@ -37,11 +50,13 @@ export default async function ExperiencePage({ params }: { params: { slug: strin
const currentIndex = allSlugs.findIndex((s) => s === slug);
const prevSlug = currentIndex > 0 ? allSlugs[currentIndex - 1] : null;
const nextSlug = currentIndex < allSlugs.length - 1 ? allSlugs[currentIndex + 1] : null;
const prevPost = prevSlug ? await getPostBySlug('experience', prevSlug) : null;
const nextPost = nextSlug ? await getPostBySlug('experience', nextSlug) : null;
const prevPost: Post | null = prevSlug ? await getPostBySlug('experience', prevSlug) : null;
const nextPost: Post | null = nextSlug ? await getPostBySlug('experience', nextSlug) : null;
const navigation = {
prev: prevPost ? { slug: prevSlug, title: prevPost.frontmatter.title } : null,
next: nextPost ? { slug: nextSlug, title: nextPost.frontmatter.title } : null,
prev: prevPost ? { slug: prevSlug!, title: prevPost.frontmatter.title } : null,
next: nextPost ? { slug: nextSlug!, title: nextPost.frontmatter.title } : null,
};
return <Article post={post} publications={publications} navigation={navigation} basePath="experience" />;

View File

@@ -1,18 +1,13 @@
// src/pages/experience.tsx
import { getSortedPostsData } from "@/lib/posts";
import { ProjectCard } from "@/components/project-card";
import { ExperienceCard } from "@/components/list-item"; // Import the new component
import { BlurFade } from "@/components/magicui/blur-fade";
const BLUR_FADE_DELAY = 0.04;
import { getAllTags, getSortedPostsData } from "@/lib/posts";
import { FilterableExperienceGrid } from "@/components/filterable-experience-list";
export default function ExperiencePage() {
const posts = getSortedPostsData("experience");
// Filter out posts that might not be suitable for a list item if needed,
// or ensure your getSortedPostsData provides necessary fields for both.
const experiencePosts = posts.filter((post) => post.title);
const allPosts = posts.filter((post) => post.title);
const allTags = getAllTags(5, "experience").filter((tag) => (tag.name === "project" || tag.name === "teaching"));
return (
<main className="flex flex-col min-h-[100dvh] space-y-10">
@@ -23,30 +18,12 @@ export default function ExperiencePage() {
Experience
</h1>
<p className="text-muted-foreground">
My professional experience encompasses both hands-on systems engineering and academic instruction. I've worked at the intersection of machine learning and complex systems, with projects ranging from exploring emergent behavior in AI to managing cluster infrastructure. In my role at <b><a href="https://www.mobile.ifi.lmu.de/team/steffen-illium/">LMU Munich</a></b>, I further developed this experience by mentoring students, contributing to lectures, and leading practical seminars.<br/>
My professional experience encompasses both hands-on systems engineering and academic instruction. I&apos;ve worked at the intersection of machine learning and complex systems, with projects ranging from exploring emergent behavior in AI to managing cluster infrastructure. In my role at <b><a href="https://www.mobile.ifi.lmu.de/team/steffen-illium/">LMU Munich</a></b>, I further developed this experience by mentoring students, contributing to lectures, and leading practical seminars.<br/>
</p>
</div>
<hr />
<div className="flex flex-col space-y-4">
{experiencePosts.map((post, id) => (
<BlurFade
key={post.title + "-list"}
delay={BLUR_FADE_DELAY * 2 + id * 0.005}
>
<ExperienceCard
href={post.href}
key={post.title}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
tags={post.tags}
image={post.image || ""}
video={post.video}
links={[]}
/>
</BlurFade>
))}
</div>
<FilterableExperienceGrid posts={allPosts} tags={allTags} />
</div>
</section>
</main>

View File

@@ -6,7 +6,7 @@ import type { Metadata } from "next";
import { Inter as FontSans } from "next/font/google";
import "./globals.css";
import { Providers } from "@/components/providers";
import { Footer } from "@/components/footer";
import { Footer } from "@/components/container-footer";
const fontSans = FontSans({
subsets: ["latin"],

55
app/not-found.js Normal file
View File

@@ -0,0 +1,55 @@
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { BlurFade } from "@/components/magicui/blur-fade";
const BLUR_FADE_DELAY = 0.01;
export default function NotFoundPage() {
return (
<main
className="fixed inset-0 flex flex-col items-center justify-center bg-background"
>
<div className="flex flex-col items-center space-y-6 text-center max-w-lg w-full p-6">
<BlurFade delay={BLUR_FADE_DELAY * 1}>
<Image
src="/images/404.jpg"
alt="A lost robot in a desert, indicating a 404 page not found error."
width={400}
height={300}
className="rounded-lg shadow-lg"
/>
</BlurFade>
<BlurFade delay={BLUR_FADE_DELAY * 2}>
<h1 className="text-4xl font-bold tracking-tight">
Lost in the Digital Sands
</h1>
</BlurFade>
<BlurFade delay={BLUR_FADE_DELAY * 3}>
<p className="text-muted-foreground">
It seems the page you are looking for does not exist, has been moved,
or is currently unavailable.
</p>
</BlurFade>
<BlurFade delay={BLUR_FADE_DELAY * 4}>
<div className="flex flex-wrap items-center justify-center gap-2">
<Link href="/">
<Button>Go to Homepage</Button>
</Link>
<Link href="/publications">
<Button variant="outline">View Publications</Button>
</Link>
<Link href="/#contact">
<Button variant="outline">Contact Me</Button>
</Link>
</div>
</BlurFade>
</div>
</main>
);
}

View File

@@ -5,13 +5,11 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { DATA } from "@/app/resume";
import Markdown from "react-markdown";
//import {ContactCard} from "@/components/contact-card";
import Link from "next/link";
import { TextAnimate } from "@/components/magicui/text-animate";
import { BlurFade } from "@/components/magicui/blur-fade";
import { TrackedLink } from "@/components/util-tracked-link";
const BLUR_FADE_DELAY = 0.04;
const BLUR_FADE_DELAY = 0.01;
export default function Page() {
const posts = getSortedPostsData().slice(0, 6);
@@ -68,8 +66,7 @@ export default function Page() {
Check out my latest work
</h2>
<p className="text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
I&apos;ve worked on a variety of projects, from scientific research to managing projects. Here are a few of my
latest.
I&apos;ve worked on a variety of projects, from scientific research to managing projects. Here are a few of my latest.
</p>
</div>
</div>

View File

@@ -1,49 +0,0 @@
import { getPostBySlug, getPostSlugs } from '@/lib/mdx';
import { notFound } from 'next/navigation';
import { getPublicationsData } from '@/lib/publications';
import { Article } from '@/components/page-article';
import { DATA } from '@/app/resume';
export async function generateStaticParams() {
const slugs = getPostSlugs('projects');
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
// FIX: Await params to get slug for Next.js 15
const { slug } = await params;
const post = await getPostBySlug('projects', slug);
if (!post) { return {}; }
return {
title: post.frontmatter.title,
description: post.frontmatter.teaser || DATA.description,
};
}
export default async function ProjectPage({ params }: { params: { slug: string } }) {
// FIX: Await params to get slug for Next.js 15
const { slug } = await params;
const post = await getPostBySlug('projects', slug);
const publications = getPublicationsData();
if (!post) {
notFound();
}
// --- Navigation Logic ---
const allSlugs = getPostSlugs('projects');
const currentIndex = allSlugs.findIndex((s) => s === slug);
const prevSlug = currentIndex > 0 ? allSlugs[currentIndex - 1] : null;
const nextSlug = currentIndex < allSlugs.length - 1 ? allSlugs[currentIndex + 1] : null;
const prevPost = prevSlug ? await getPostBySlug('projects', prevSlug) : null;
const nextPost = nextSlug ? await getPostBySlug('projects', nextSlug) : null;
const navigation = {
prev: prevPost ? { slug: prevSlug, title: prevPost.frontmatter.title } : null,
next: nextPost ? { slug: nextSlug, title: nextPost.frontmatter.title } : null,
};
return <Article post={post} publications={publications} navigation={navigation} basePath="projects" />;
}

View File

@@ -1,47 +0,0 @@
import { getSortedPostsData } from "@/lib/posts";
import { BlurFade } from "@/components/magicui/blur-fade";
import { ExperienceCard } from "@/components/list-item";
const BLUR_FADE_DELAY = 0.04;
export default function ProjectsPage() {
const posts = getSortedPostsData("projects");
return (
<main className="flex flex-col min-h-[100dvh] space-y-10">
<section id="projects">
<div className="mx-auto w-full max-w-6xl space-y-8">
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none mt-12">
Projects
</h1>
<p className="text-muted-foreground">
My work sits at the intersection of machine learning and systems engineering. I have experience in everything from exploring emergent behavior in AI agents to managing robust, automated infrastructure with Kubernetes. Here are some highlights.
</p>
</div>
<hr />
<div className="flex flex-col space-y-4"> {/* Use flex-col for list items */}
{posts.map((post, id) => (
<BlurFade
key={post.title + "-list"} // Use a different key to avoid collisions if both sections are rendered
delay={BLUR_FADE_DELAY * 2 + id * 0.05} // You might want to adjust delay for list items
>
<ExperienceCard
href={post.href}
key={post.title}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
tags={post.tags}
image={post.image || ""}
video={post.video}
links={[]}
/>
</BlurFade>
))}
</div>
</div>
</section>
</main>
);
}

View File

@@ -1,7 +1,6 @@
import { getPublicationsData } from "@/lib/publications";
import { PublicationCard } from "@/components/publication-card";
import { DATA } from "@/app/resume";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import fs from "fs";
import path from "path";
@@ -42,9 +41,10 @@ export default function PublicationsPage() {
<div className="flex flex-wrap gap-3 my-10 justify-center">
{Object.entries(DATA.contact.social)
.filter(([_, social]) => social.pub)
//ts-
.filter(([, social]) => social.pub)
.map(([name, social]) => (
<TrackedLink href={social.url} key={name} eventName={`${name}-social`} target="_blank">
<TrackedLink href={social.url} key={name} eventName={`${name}-social`}>
<Badge className="flex gap-2 px-3 py-1 text-sm">
<social.icon className="size-4" />
{name}

View File

@@ -5,13 +5,14 @@ import { notFound } from 'next/navigation';
import { getPublicationsData } from '@/lib/publications';
import { Article } from '@/components/page-article';
import { DATA } from '@/app/resume';
import { Props } from '@/components/types';
export async function generateStaticParams() {
const slugs = getPostSlugs('research');
return slugs.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
export async function generateMetadata({ params }: Props ) {
const { slug } = await params;
const post = await getPostBySlug('research', slug);
@@ -22,7 +23,7 @@ export async function generateMetadata({ params }: { params: { slug: string } })
};
}
export default async function ResearchPage({ params }: { params: { slug: string } }) {
export default async function ResearchPage({ params }: Props ) {
const { slug } = await params;
const post = await getPostBySlug('research', slug);
@@ -42,8 +43,8 @@ export default async function ResearchPage({ params }: { params: { slug: string
const nextPost = nextSlug ? await getPostBySlug('research', nextSlug) : null;
const navigation = {
prev: prevPost ? { slug: prevSlug, title: prevPost.frontmatter.title } : null,
next: nextPost ? { slug: nextSlug, title: nextPost.frontmatter.title } : null,
prev: prevPost ? { slug: prevSlug!, title: prevPost.frontmatter.title } : null,
next: nextPost ? { slug: nextSlug!, title: nextPost.frontmatter.title } : null,
};
return <Article post={post} publications={publications} navigation={navigation} basePath="research" />;

View File

@@ -1,51 +1,33 @@
import { getSortedPostsData } from "@/lib/posts";
import { ProjectCard } from "@/components/project-card";
import { BlurFade } from "@/components/magicui/blur-fade";
// NO "use client" here. This is a Server Component.
const BLUR_FADE_DELAY = 0.04;
import { getSortedPostsData, getAllTags } from "@/lib/posts";
import { FilterableResearchGrid } from "@/components/filterable-research-list"; // Import the new client component
export default function ResearchPage() {
const posts = getSortedPostsData("research");
// These functions run safely on the server because this is a Server Component.
const allPosts = getSortedPostsData("research");
const allTags = getAllTags(5, "research");
return (
<main className="flex flex-col min-h-[100dvh] space-y-10">
<section id="research">
<div className="mx-auto w-full max-w-6xl space-y-8">
<div className="mx-auto w-full max-w-6xl space-y-8 px-4">
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none mt-12">
Research
</h1>
<p className="text-muted-foreground">
<p className="max-w-3xl text-muted-foreground">
This section details my scientific publications, primarily focused
on advancing machine learning and deep neural networks.
My involvement has spanned, from conceptualizing the ideas
and developing machine learning models, to providing support
to my colleagues.
on advancing machine learning and deep neural networks. My
involvement has spanned, from conceptualizing the ideas and
developing machine learning models, to providing support to my
colleagues.
</p>
</div>
<hr />
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-2">
{posts
.filter((post) => post.title)
.map((post, id) => (
<BlurFade
key={post.title}
delay={BLUR_FADE_DELAY * 2 + id * 0.005}
>
<ProjectCard
href={post.href}
key={post.title}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
tags={post.tags}
image={post.image || ""}
video={post.video}
links={[]}
/>
</BlurFade>
))}
</div>
<FilterableResearchGrid posts={allPosts} tags={allTags} />
</div>
</section>
</main>

View File

@@ -1,15 +1,8 @@
import { Icons } from "@/components/icons";
import { url } from "inspector";
import {
HomeIcon,
NotebookIcon,
FlaskConicalIcon,
PaperclipIcon,
BookUserIcon,
BriefcaseIcon,
GraduationCapIcon,
LayersIcon,
GridIcon,
ClipboardListIcon,
} from "lucide-react";
@@ -73,7 +66,7 @@ export const DATA = {
],
contact:
{
email: "contact@steffenillium.de",
email: "steffen.illium@ifi.lmu.de",
tel: "",
social:
{

61
app/sitemap.ts Normal file
View File

@@ -0,0 +1,61 @@
// app/sitemap.ts
import { getAllTags, getSortedPostsData } from '@/lib/posts';
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://steffenillium.de';
// Improvement 1: Fetch all posts with a single, more efficient call
const allPosts = getSortedPostsData();
const postUrls: MetadataRoute.Sitemap = allPosts.map((post) => ({
url: `${baseUrl}/${post.href}`,
lastModified: new Date(post.date),
changeFrequency: 'yearly',
priority: 0.7,
}));
const allTags = getAllTags(2);
const tagUrls: MetadataRoute.Sitemap = allTags.map((tag) => ({
url: `${baseUrl}/tags/${tag.name}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
}));
// The types for changeFrequency and lastModified will now be correctly inferred
const staticRoutes: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
priority: 1,
changeFrequency: 'monthly',
},
{
url: `${baseUrl}/experience`,
lastModified: new Date(),
priority: 0.8,
changeFrequency: 'monthly',
},
{
url: `${baseUrl}/research`,
lastModified: new Date(),
priority: 0.9,
changeFrequency: 'yearly',
},
{
url: `${baseUrl}/publications`,
lastModified: new Date(),
priority: 0.9,
changeFrequency: 'yearly',
},
{
url: `${baseUrl}/tags`,
lastModified: new Date(),
priority: 0.7,
changeFrequency: 'monthly',
},
];
return [...staticRoutes, ...postUrls, ...tagUrls];
}

33
app/status/page.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { BlurFade } from "@/components/magicui/blur-fade";
const BLUR_FADE_DELAY = 0.01;
export default function StatusPage() {
return (
<main className="flex flex-col min-h-[100dvh] space-y-10">
<section id="research">
<div className="mx-auto w-full max-w-6xl space-y-8">
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tighter sm:text-5xl xl:text-6xl/none mt-12">
Status
</h1>
</div>
<hr />
<div className="w-full">
<BlurFade delay={BLUR_FADE_DELAY * 5}>
<iframe
src="https://uptime.steffenillium.de/status/system"
width="100%"
height="800px"
style={{ border: 'none', display: 'block', margin: '0 auto' }}
></iframe>
</BlurFade>
</div>
</div>
</section>
</main>
);
}

View File

@@ -3,16 +3,17 @@ import { ProjectCard } from "@/components/project-card";
import { BlurFade } from "@/components/magicui/blur-fade";
import { notFound } from "next/navigation";
import { Breadcrumbs } from "@/components/element-breadcrumbs";
import { TagProps } from "@/components/types";
const BLUR_FADE_DELAY = 0.04;
const BLUR_FADE_DELAY = 0.01;
export async function generateStaticParams() {
const tags = getAllTags();
return tags.map((tag) => ({ tag: tag.name }));
}
export async function generateMetadata({ params }: { params: { tag: string } }) {
export async function generateMetadata({ params }: TagProps ) {
const { tag } = await params;
const tagData = getTagData(tag);
@@ -22,7 +23,7 @@ export async function generateMetadata({ params }: { params: { tag: string } })
};
}
export default async function TagPage({ params }: { params: { tag: string } }) {
export default async function TagPage({ params }: TagProps) {
const { tag } = await params;
const posts = getPostsByTag(tag);
const tagData = getTagData(tag);

View File

@@ -2,7 +2,7 @@ import { getAllTags } from "@/lib/posts";
import Link from "next/link";
import { BlurFade } from "@/components/magicui/blur-fade";
const BLUR_FADE_DELAY = 0.04;
const BLUR_FADE_DELAY = 0.01;
export default function TagsPage() {
const tags = getAllTags(2);

View File

@@ -0,0 +1,80 @@
"use client";
import {
Card,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { ChevronRightIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import Markdown from "react-markdown";
// Interface is identical to ProjectCard but adds the 'venue'
interface Props {
title: string;
href: string; // Made href non-optional as research should always link somewhere
description: string;
dates: string;
venue?: string; // The new field for publication venue
tags: readonly string[];
image?: string;
video?: string;
className?: string;
}
export function ResearchCard({
title,
href,
description,
dates,
venue,
image,
video,
className,
}: Props) {
return (
<Link href={href} className={cn("block rounded-xl h-full cards", className)}>
<Card className="py-2 px-0 group flex h-full flex-col overflow-hidden border transition-shadow duration-300 ease-out hover:shadow-lg">
{video && (
<video
src={video}
autoPlay
loop
muted
playsInline
className="pointer-events-none w-full object-cover object-top"
style={{ height: "160px" }}
/>
)}
{image && (
<div className="relative w-full" style={{ height: "160px" }}>
<Image
src={image}
alt={title}
fill
className="object-cover object-center"
/>
</div>
)}
<CardHeader className="flex-1 px-4 pb-1">
<div className="space-y-1">
<CardTitle className="text-base flex items-start justify-between">
<span className="flex-1">{title}</span>
<ChevronRightIcon className="size-4 shrink-0 translate-x-0 transform opacity-0 transition-all duration-300 ease-out group-hover:translate-x-1 group-hover:opacity-100" />
</CardTitle>
<div className="flex items-center gap-x-2 text-xs text-muted-foreground mt-1">
{venue && <span className="font-semibold text-primary">{venue}</span>}
{venue && <span></span>}
<time>{dates}</time>
</div>
<div className="prose max-w-full text-pretty font-normal text-xs text-foreground dark:prose-invert">
<Markdown>{description}</Markdown>
</div>
</div>
</CardHeader>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,38 @@
// ./components/footer.tsx
"use client";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
const StatusBadge = () => (
<Image
src="https://uptime.steffenillium.de/api/badge/2/status"
alt="System Status"
width={88} // Add the width of the badge image
height={20} // Add the height of the badge image
className="rounded-md shadow-sm" // Optional: for styling consistency
unoptimized
/>
);
export function Footer() {
const pathname = usePathname();
if (pathname !== "/connect") return;
return (
<div className="hidden md:block fixed bottom-4 right-4 z-50">
<Link
href="/status"
target="_blank"
rel="noopener noreferrer"
className="transition-opacity hover:opacity-80"
aria-label="View system status page"
>
<StatusBadge />
</Link>
</div>
);
}

View File

@@ -8,10 +8,11 @@ import { usePathname } from "next/navigation";
export function Header() {
const [isVisible, setIsVisible] = useState(false);
const pathname = usePathname();
if (pathname === "/connect") return null;
const isMainPage = pathname === "/";
useEffect(() => {
if (pathname === "/connect") return;
const handleScroll = () => {
if (window.scrollY > 100) {
setIsVisible(true);
@@ -32,9 +33,9 @@ export function Header() {
window.removeEventListener("scroll", handleScroll);
}
};
}, [isMainPage]);
}, [isMainPage, pathname]);
if (!isVisible && isMainPage) {
if (pathname === "/connect" || (!isVisible && isMainPage)) {
return null;
}

View File

@@ -40,7 +40,7 @@ export function ReferencesContainer() {
pdfUrl={pub.pdfUrl}
bibtex={pub.bibtex}
pdfAvailable={pub.pdfAvailable}
className="cards"
className="cards cursor-pointer"
/>
);
})}

View File

@@ -0,0 +1,132 @@
"use client";
import { useState } from "react";
import { BlurFade } from "@/components/magicui/blur-fade";
import { Badge } from "@/components/ui/badge";
import { ExperienceCard } from "./list-item";
const BLUR_FADE_DELAY = 0.01;
// Define the shape of your post data for type safety
type Post = {
id: string;
href: string;
title?: string;
excerpt?: string;
date: string;
venue?: string;
tags: readonly string[];
image?: string;
video?: string;
};
type Tag = {
name: string;
count: number;
};
interface Props {
posts: Post[];
tags: Tag[];
}
// IDs of your most important papers to feature.
const FEATURED_Experience_IDS = [
"FIKS",
"water-networks"
];
export function FilterableExperienceGrid({ posts, tags }: Props) {
const [activeTag, setActiveTag] = useState<string | null>(null);
const featuredPosts = posts.filter((post) =>
FEATURED_Experience_IDS.includes(post.id)
);
const regularPosts = posts.filter(
(post) => !FEATURED_Experience_IDS.includes(post.id)
);
const displayedPosts = activeTag
? regularPosts.filter((post) => post.tags.includes(activeTag))
: regularPosts;
const handleTagClick = (tag: string) => {
setActiveTag(activeTag === tag ? null : tag);
};
return (
<>
{/* SECTION 1: Featured Experience */}
<div>
<h2 className="text-2xl font-bold tracking-tight mb-4">Featured Projects</h2>
<div className="grid grid-cols-1 gap-2 lg:grid-cols-1">
{featuredPosts.map((post, id) => (
<BlurFade key={post.id} delay={BLUR_FADE_DELAY + id * 0.05}>
<ExperienceCard
href={post.href}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
tags={post.tags}
image={post.image || ""}
video={post.video}
/>
</BlurFade>
))}
</div>
</div>
<hr />
{/* SECTION 2: All Experience with Filtering */}
<div>
<h2 className="text-2xl font-bold tracking-tight mb-4">Other</h2>
<div className="flex flex-wrap items-center gap-2 mb-6">
<p className="text-sm font-semibold mr-2">Filter by Topic:</p>
{tags.map((tag) => (
<Badge
key={tag.name}
variant={activeTag === tag.name ? "default" : "secondary"}
onClick={() => handleTagClick(tag.name)}
className="cursor-pointer transition-transform hover:scale-105"
>
{tag.name} ({tag.count})
</Badge>
))}
{activeTag && (
<Badge
variant="destructive"
onClick={() => setActiveTag(null)}
className="cursor-pointer"
>
Clear Filter &times;
</Badge>
)}
</div>
<div className="flex flex-col gap-4">
{displayedPosts.map((post, id) => (
<BlurFade key={post.id} delay={0.1 + id * 0.05}>
<ExperienceCard
href={post.href}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
tags={post.tags}
image={post.image || ""}
video={post.video}
/>
</BlurFade>
))}
</div>
{displayedPosts.length === 0 && activeTag && (
<p className="text-muted-foreground col-span-full text-center py-8">
No publications found with the tag &quot;{activeTag}&quot;.
</p>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,135 @@
"use client";
import { useState } from "react";
import { ResearchCard } from "@/components/card-research";
import { BlurFade } from "@/components/magicui/blur-fade";
import { Badge } from "@/components/ui/badge";
import { ResearchListItem } from "./list-item-research";
const BLUR_FADE_DELAY = 0.01;
// Define the shape of your post data for type safety
type Post = {
id: string;
href: string;
title?: string;
excerpt?: string;
date: string;
venue?: string;
tags: readonly string[];
image?: string;
video?: string;
};
type Tag = {
name: string;
count: number;
};
interface Props {
posts: Post[];
tags: Tag[];
}
// IDs of your most important papers to feature.
const FEATURED_RESEARCH_IDS = [
"mas-emergence-safety",
"audio-vision-transformer",
"voronoi-data-augmentation",
"rnn-memory-limits"
];
export function FilterableResearchGrid({ posts, tags }: Props) {
const [activeTag, setActiveTag] = useState<string | null>(null);
const featuredPosts = posts.filter((post) =>
FEATURED_RESEARCH_IDS.includes(post.id)
);
const regularPosts = posts.filter(
(post) => !FEATURED_RESEARCH_IDS.includes(post.id)
);
const displayedPosts = activeTag
? regularPosts.filter((post) => post.tags.includes(activeTag))
: regularPosts;
const handleTagClick = (tag: string) => {
setActiveTag(activeTag === tag ? null : tag);
};
return (
<>
{/* SECTION 1: Featured Research */}
<div>
<h2 className="text-2xl font-bold tracking-tight mb-4">Featured Publications</h2>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{featuredPosts.map((post, id) => (
<BlurFade key={post.id} delay={BLUR_FADE_DELAY + id * 0.05}>
<ResearchCard
href={post.href}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
venue={post.venue}
tags={post.tags}
image={post.image || ""}
video={post.video}
/>
</BlurFade>
))}
</div>
</div>
<hr />
{/* SECTION 2: All Research with Filtering */}
<div>
<h2 className="text-2xl font-bold tracking-tight mb-4">Other</h2>
<div className="flex flex-wrap items-center gap-2 mb-6">
<p className="text-sm font-semibold mr-2">Filter by Topic:</p>
{tags.map((tag) => (
<Badge
key={tag.name}
variant={activeTag === tag.name ? "default" : "secondary"}
onClick={() => handleTagClick(tag.name)}
className="cursor-pointer transition-transform hover:scale-105"
>
{tag.name} ({tag.count})
</Badge>
))}
{activeTag && (
<Badge
variant="destructive"
onClick={() => setActiveTag(null)}
className="cursor-pointer"
>
Clear Filter &times;
</Badge>
)}
</div>
<div className="flex flex-col gap-4">
{displayedPosts.map((post, id) => (
<BlurFade key={post.id} delay={0.1 + id * 0.05}>
<ResearchListItem
href={post.href}
title={post.title!}
description={post.excerpt || ""}
dates={post.date}
venue={post.venue}
tags={post.tags}
image={post.image || ""}
video={post.video}
/>
</BlurFade>
))}
</div>
{displayedPosts.length === 0 && activeTag && (
<p className="text-muted-foreground col-span-full text-center py-8">
No publications found with the tag &quot;{activeTag}&quot;.
</p>
)}
</div>
</>
);
}

View File

@@ -1,9 +0,0 @@
"use client";
export function Footer() {
return (
<div className="h-10">
</div>
);
}

View File

@@ -0,0 +1,82 @@
"use client";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import Markdown from "react-markdown";
// Using the same interface as ResearchCard for data consistency
interface Props {
title: string;
href: string;
description: string;
dates: string;
venue?: string;
tags: readonly string[];
image?: string;
video?: string;
className?: string;
}
export function ResearchListItem({
title,
href,
description,
dates,
venue,
image,
video, // Video is less common in this layout but supported
className,
}: Props) {
return (
<Link href={href} className={cn("cards rounded-xl block no-underline", className)}>
<Card className="group flex flex-col md:flex-row overflow-hidden">
{/* Image Container (Fixed Width) */}
{(image || video) && (
<div className="relative w-full md:w-48 flex-shrink-0 bg-muted/30">
{video ? (
<video
src={video}
autoPlay loop muted playsInline
className="h-full w-full object-cover"
/>
) : (
<Image
src={image!}
alt={title}
width={200}
height={150}
className="max-h-30 h-full w-full object-contain p-2"
/>
)}
</div>
)}
{/* Text Content Container (Flexible Width) */}
<div className="flex flex-1 flex-col p-4">
<h3 className="font-bold text-lg">{title}</h3>
<div className="flex items-center gap-x-2 text-xs text-muted-foreground mt-1">
{venue && <span className="font-semibold text-primary">{venue}</span>}
{venue && <span></span>}
<time>{dates}</time>
</div>
<div className="prose max-w-full text-pretty text-xs font-normal dark:prose-invert mt-2">
<Markdown>{description}</Markdown>
</div>
{/*
<CardContent className="mt-auto flex flex-col px-0 pb-0 pt-4">
<div className="flex flex-row flex-wrap gap-1">
{tags.map((tag) => (
<Badge key={tag} variant="secondary" className="px-2 py-0.5 text-[10px] font-normal">
{tag}
</Badge>
))}
</div>
</CardContent> */}
</div>
</Card>
</Link>
);
}

View File

@@ -2,7 +2,6 @@
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import Markdown from "react-markdown";
import React from "react";
@@ -32,17 +31,17 @@ export function ExperienceCard({
href,
description,
dates,
tags,
link,
image,
video,
links,
className,
}: Props) {
return (
<Link
href={href || "#"}
className="cards group block rounded-xl overflow-hidden font-normal no-underline cursor-pointer"
className={cn(
"cards group block rounded-xl overflow-hidden font-normal no-underline cursor-pointer"
, className
)}
>
{/* 3. The Card component now has its conflicting shadow removed. */}
<Card className="flex flex-row items-center p-4 shadow-none">

View File

@@ -1,72 +0,0 @@
"use client";
import { cn } from "@/lib/utils";
import { motion, MotionProps, type AnimationProps } from "motion/react";
import React from "react";
const animationProps = {
initial: { "--x": "100%", scale: 0.8 },
animate: { "--x": "-100%", scale: 1 },
whileTap: { scale: 0.95 },
transition: {
repeat: Infinity,
repeatType: "loop",
repeatDelay: 1,
type: "spring",
stiffness: 20,
damping: 15,
mass: 2,
scale: {
type: "spring",
stiffness: 200,
damping: 5,
mass: 0.5,
},
},
} as AnimationProps;
interface ShinyButtonProps
extends Omit<React.HTMLAttributes<HTMLElement>, keyof MotionProps>,
MotionProps {
children: React.ReactNode;
className?: string;
}
export const ShinyButton = React.forwardRef<
HTMLButtonElement,
ShinyButtonProps
>(({ children, className, ...props }, ref) => {
return (
<motion.button
ref={ref}
className={cn(
"relative cursor-pointer rounded-lg px-6 py-2 font-medium backdrop-blur-xl border transition-shadow duration-300 ease-in-out hover:shadow dark:bg-[radial-gradient(circle_at_50%_0%,var(--primary)/10%_0%,transparent_60%)] dark:hover:shadow-[0_0_20px_var(--primary)/10%]",
className,
)}
{...animationProps}
{...props}
>
<span
className="relative block size-full text-sm uppercase tracking-wide text-[rgb(0,0,0,65%)] dark:font-light dark:text-[rgb(255,255,255,90%)]"
style={{
maskImage:
"linear-gradient(-75deg,var(--primary) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),var(--primary) calc(var(--x) + 100%))",
}}
>
{children}
</span>
<span
style={{
mask: "linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box exclude,linear-gradient(rgb(0,0,0), rgb(0,0,0))",
WebkitMask:
"linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box exclude,linear-gradient(rgb(0,0,0), rgb(0,0,0))",
backgroundImage:
"linear-gradient(-75deg,var(--primary)/10% calc(var(--x)+20%),var(--primary)/50% calc(var(--x)+25%),var(--primary)/10% calc(var(--x)+100%))",
}}
className="absolute inset-0 z-10 block rounded-[inherit] p-px"
/>
</motion.button>
);
});
ShinyButton.displayName = "ShinyButton";

View File

@@ -1,8 +1,8 @@
"use client";
import Image from "next/image";
import Image, { ImageProps } from "next/image";
import Link from "next/link";
import React, { Children, isValidElement } from "react";
import React, { AnchorHTMLAttributes, Children, isValidElement } from "react";
import { InfoBox } from "./infobox";
import { Cite } from "./cite";
import { FloatingImage } from "./floating-image";
@@ -10,26 +10,39 @@ import { CenteredImage } from "./centered-image";
import { FaGithub, FaPython, FaBook, FaFileAlt } from 'react-icons/fa';
function CustomLink(props: any) {
let href = props.href;
type CustomLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
href: string; // We ensure href is a required string
};
function CustomLink({ href, children, ...rest }: CustomLinkProps) {
if (href.startsWith("/")) {
return (
<Link href={href} {...props}>
{props.children}
<Link href={href} {...rest}>
{children}
</Link>
);
}
if (href.startsWith("#")) {
return <a {...props} />;
// The same pattern applies to regular <a> tags.
return <a href={href} {...rest}>{children}</a>;
}
return <a target="_blank" rel="noopener noreferrer" {...props} />;
// We explicitly add href and children, and spread the rest.
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...rest}>
{children}
</a>
);
}
function RoundedImage(props: any) {
return <Image alt={props.alt} className="rounded-lg" {...props} />;
function RoundedImage(props: ImageProps) {
<Image
{...props}
alt={props.alt}
className={`rounded-lg ${props.className || ''}`}
/>
}
// Helper to extract plain text content from React children for slugification.
@@ -39,10 +52,6 @@ function getPlainTextFromChildren(nodes: React.ReactNode): string {
if (typeof node === 'string') {
return node;
}
// Recursively extract text from valid React elements
if (isValidElement(node) && node.props && node.props.children) {
return getPlainTextFromChildren(node.props.children);
}
return ''; // Ignore other types of nodes (null, undefined, numbers etc.)
})
.join('');
@@ -61,10 +70,10 @@ function slugify(str: string) {
}
function createHeading(level: number) {
const Heading = ({ children, ...props }: { children?: React.ReactNode, [key: string]: any }) => {
const Heading = ({ children, ...props }: { children?: React.ReactNode, [key: string]: unknown }) => {
// Extract plain text from children for reliable slug generation
const plainText = getPlainTextFromChildren(children);
let slug = slugify(plainText);
const slug = slugify(plainText);
// Process children to handle HTML entities for text nodes and ensure unique keys
const processedChildren = Children.map(children, (child, index) => {
@@ -88,7 +97,7 @@ function createHeading(level: number) {
href: `#${slug}`,
key: `link-${slug}`,
className: "anchor",
}), ...processedChildren
}), ...(processedChildren || [])
]
);
};

View File

@@ -8,7 +8,6 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";
import { DATA } from "@/app/resume";
import { cn } from "@/lib/utils";

View File

@@ -36,7 +36,6 @@ export function ProjectCard({
href,
description,
dates,
tags,
link,
image,
video,

View File

@@ -136,8 +136,8 @@ export function PublicationCard({
return (
<div
className={cn(
"flex items-start space-x-4 rounded-lg border p-4 shadow-sm transition-all duration-300 ease-out",
url && "cursor-pointer hover:shadow-lg",
"cards flex items-start space-x-4 rounded-lg border p-4",
url,
className
)}
id={bibtexKey}

View File

@@ -1,7 +1,6 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
import { ThemeProvider as NextThemesProvider, ThemeProviderProps } from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

9
components/types.tsx Normal file
View File

@@ -0,0 +1,9 @@
export type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export type TagProps = {
params: Promise<{ tag: string }>;
};

View File

@@ -1,9 +0,0 @@
---
layout: single
title: "Welcome to Jekyll!"
categories: blog
excerpt: "A unique line of text to describe this post that will display in an archive listing and meta description with SEO benefits."
---
W. I. P.

View File

@@ -1,6 +1,6 @@
---
title: "InnoMi Project"
tags: [mobile-internet, technology-transfer, bavaria, industry]
tags: [project, mobile-internet, technology-transfer, bavaria, industry]
excerpt: "Early-stage mobile/distributed tech transfer between academia and industry."
teaser: /images/projects/innomi.png
icon: /images/projects/innomi.png

View File

@@ -1,6 +1,6 @@
---
title: "Computer Architecture TA"
tags: [computer architecture, coordination]
tags: [teaching, computer architecture, coordination]
excerpt: "Served as a Teaching Assistant and Tutorial Coordinator for the LMU Computer Architecture course, managing tutors and curriculum for over 600 students."
teaser: "/images/teaching/computer_gear.png"
icon: "/images/teaching/computer_gear.png"

View File

@@ -1,6 +1,6 @@
---
title: "IoT Practical Exercise"
tags: [iot, mqtt, python, influxdb, distributed-systems, practical-course]
tags: [teaching, iot, mqtt, python, influxdb, distributed-systems, practical-course]
excerpt: "Designed and taught an IoT practical exercise using MQTT and Python for approximately 200 students."
teaser: "/images/teaching/server.png"
icon: "/images/teaching/server.png"

View File

@@ -1,6 +1,6 @@
---
title: "Python 101 Course"
tags: [python, programming, introductory-course, curriculum-development]
tags: [teaching, python, programming, introductory-course, curriculum-development]
excerpt: "Co-developed/taught intensive introductory Python course for 200 students."
teaser: /images/teaching/py.png
icon: /images/teaching/py.png

View File

@@ -1,6 +1,6 @@
---
title: "DW Editorial Lead"
tags: [editorial, content management, digital strategy, magazine, workflow optimization, website relaunch]
tags: [project, editorial, content management, digital strategy, magazine, workflow optimization, website relaunch]
excerpt: "Led online editorial team for DIGITALE WELT Magazin (2018-2023)."
teaser: "/images/projects/dw.png"
icon: "/images/projects/dw.png"

View File

@@ -1,7 +1,7 @@
---
title: "ErLoWa Leak Detection"
excerpt: "Deep learning detects acoustic water leaks with SWM."
tags: [acoustic, anomaly-detection, deep-learning, real-world-data, signal-processing, water-management, sensors]
tags: [project, acoustic, anomaly-detection, deep-learning, real-world-data, signal-processing, water-management, sensors]
teaser: "/images/projects/pipe_leak.png"
icon: "/images/projects/pipe_leak.png"
---

View File

@@ -1,6 +1,6 @@
---
title: "TIMS Seminar Supervision"
tags: [supervision, mentoring, academic writing]
tags: [teaching, supervision, mentoring, academic writing]
excerpt: "Supervised student research, writing, and presentation skills in Mobile/Distributed Systems, ML, and Quantum Computing."
teaser: "/images/teaching/thesis.png"
icon: "/images/teaching/thesis.png"

View File

@@ -1,6 +1,6 @@
---
title: "VTIMS Advanced Seminar"
tags: [machine learning, distributed systems, quantum computing, research mentoring, scientific writing, presentation coaching, critical analysis, academic assessment]
tags: [teaching, machine learning, distributed systems, quantum computing, research mentoring, scientific writing, presentation coaching, critical analysis, academic assessment]
excerpt: "Supervised Master's advanced research/analysis in Mobile/Distributed Systems, ML, QC."
teaser: "/images/teaching/thesis_master.png"
icon: "/images/teaching/thesis_master.png"

View File

@@ -1,6 +1,6 @@
---
title: "OpenMunich Conference Organization"
tags: [community-engagement, event-management, conference, open-source, industry, technology]
tags: [project, community-engagement, event-management, conference, open-source, industry, technology]
excerpt: "Led the OpenMunich conference series (2018-19), connecting academia, industry, and students on open-source topics."
teaser: "/images/projects/openmunich.png"
icon: "/images/projects/openmunich.png"

View File

@@ -1,6 +1,6 @@
---
title: "Operating Systems TA"
tags: [system programming, java, lmu munich]
tags: [teaching, system programming, java, lmu munich]
excerpt: "TA & Coordinator for the Operating Systems lecture, focusing on system programming concepts and concurrent programming in Java for over 350 students."
teaser: /images/teaching/computer_os.png
icon: /images/teaching/computer_os.png

View File

@@ -1,6 +1,6 @@
---
title: "iOS App Development"
tags: [mobile-development, app-development, agile, teamwork]
tags: [teaching, mobile-development, app-development, agile, teamwork]
excerpt: "Supervised iOS Praktikum: student teams built Swift apps using agile."
teaser: /images/teaching/ios.png
icon: /images/teaching/ios.png

View File

@@ -1,6 +1,6 @@
---
title: "AI-Fusion Safety"
tags: [MARL, reinforcement-learning, AI-safety, emergence, simulation, python, unity, software-engineering, industry-collaboration]
tags: [teaching, MARL, reinforcement-learning, AI-safety, emergence, simulation, python, unity, software-engineering, industry-collaboration]
excerpt: "Studied MARL emergence and safety, built simulations with Fraunhofer."
teaser: /images/projects/robot.png
icon: /images/projects/robot.png

View File

@@ -1,6 +1,6 @@
---
title: "MSP Android Course"
tags: [java, kotlin, mobile-development, app-development, agile, teamwork]
tags: [teaching, java, kotlin, mobile-development, app-development, agile, teamwork]
excerpt: "Supervised MSP: teams built Android apps (Java/Kotlin) using agile."
teaser: "/images/teaching/android.png"
icon: "/images/teaching/android.png"

View File

@@ -1,6 +1,6 @@
---
title: "Chair DevOps Admin"
tags: [devops, kubernetes, server-administration, infrastructure]
tags: [project, devops, kubernetes, server-administration, infrastructure]
excerpt: "Managed LMU chair IT: Kubernetes, CI/CD, and infrastructure automation from 2018 to 2023."
teaser: "/images/projects/arch.png"
icon: "/images/projects/arch.png"

View File

@@ -2,6 +2,7 @@
title: "Learned Trajectory Annotation"
tags: [geoinformatics, machine-learning, unsupervised-learning, human-robot-interaction, autoencoder, clustering, trajectory, perception, spatial-context, representation-learning]
excerpt: "Unsupervised autoencoder learns spatial context from trajectory data for annotation."
venue: "26th ACM SIGSPATIAL"
teaser: "/figures/0_trajectory_reconstruction_teaser.png"
---

View File

@@ -3,6 +3,7 @@ title: "Neural Self-Replication"
tags: [neural-networks, artificial-life, complex-systems, self-organization, machine-learning, evolution, replication]
excerpt: "Neural networks replicating weights, inspired by biology and artificial life."
teaser: "/figures/1_self_replication_pca_space.jpg"
venue: "ALIFE 2019"
---
Drawing inspiration from the fundamental process of self-replication in biological systems, this research explores the potential for implementing analogous mechanisms within neural networks. The objective is to develop computational models capable of autonomously reproducing their own structure (specifically, their connection weights), potentially leading to the emergence of complex, adaptive behaviors <Cite bibtexKey="gabor2019self" />.

View File

@@ -11,6 +11,7 @@ tags: [
end-to-end-learning,
cnn
]
venue: "Interspeech 2019"
excerpt: "Deep learning audio baseline for Interspeech 2019 ComParE challenge."
teaser: "/figures/3_deep_neural_baselines_teaser.jpg"
---

View File

@@ -2,6 +2,7 @@
title: "Soccer Team Vectors"
tags: [machine-learning, representation-learning, sports-analytics, similarity-search, soccer, embeddings, team-performance, prediction]
excerpt: "STEVE learns soccer team embeddings from match data for analysis."
venue: "ECML PKDD 2019"
teaser: "/figures/2_steve_algo.jpg"
---

View File

@@ -3,6 +3,7 @@ title: "3D Primitive Segmentation"
tags: [computer-vision, 3d-processing, point-clouds, segmentation, deep-learning, genetic-algorithms]
excerpt: "Hybrid method segments/fits primitives in large 3D point clouds."
teaser: "/figures/4_point_cloud_segmentation_teaser.jpg"
venue: "VISIGRAPP 2020"
---

View File

@@ -3,6 +3,7 @@ title: "PEOC OOD Detection"
tags: [deep-reinforcement-learning, out-of-distribution-detection, safety, anomaly-detection, policy-entropy, machine-learning-safety]
excerpt: "PEOC uses policy entropy for OOD detection in deep RL."
teaser: "/figures/6_ood_pipeline.jpg"
venue: "ICANN 2020"
---
Ensuring the safety and reliability of deep reinforcement learning (RL) agents deployed in real-world environments necessitates the ability to detect when the agent encounters states significantly different from those seen during training (i.e., out-of-distribution or OOD states). This research introduces **PEOC (Policy Entropy-based OOD Classifier)**, a novel and computationally efficient method designed for this purpose.

View File

@@ -3,6 +3,7 @@ title: "AV Meantime Coverage"
tags: [autonomous-vehicles, shared-mobility, transportation-systems, urban-computing, geoinformatics, mobility-as-a-service, resource-optimization]
excerpt: "Analyzing service coverage of parked AVs during downtime ('meantime')."
teaser: "/figures/5_meantime_coverage.jpg"
venue: "AGILE-GISS 2020"
---

View File

@@ -3,6 +3,7 @@ title: "Surgical-Mask Detection"
tags: [audio-classification, deep-learning, data-augmentation, computer-vision, paralinguistics, speech-processing, audio-analysis, signal-processing, spectrograms]
excerpt: "CNN mask detection in speech using augmented spectrograms."
teaser: "/figures/7_mask_models.jpg"
venue: "Interspeech 2020"
---

View File

@@ -3,6 +3,7 @@ title: "Anomalous Sound Features"
tags: [anomaly-detection, audio-classification, deep-learning, transfer-learning, feature-extraction, machine-learning, predictive-maintenance, dcase, sound-analysis, unsupervised-learning]
excerpt: "Pretrained networks extract features for anomalous industrial sound detection."
teaser: "/figures/8_anomalous_sound_teaser.jpg"
venue: "IJCNN 2021"
---
Detecting anomalous sounds, particularly in industrial settings, is crucial for predictive maintenance and safety. This often involves unsupervised or semi-supervised approaches where models learn a representation of 'normal' sounds. This research explores the effectiveness of leveraging **transfer learning** for this task by using **pretrained deep neural networks** as fixed feature extractors.

View File

@@ -3,7 +3,7 @@ title: "Sound Anomaly Transfer"
tags: [anomaly-detection, audio-classification, deep-learning, transfer-learning, feature-extraction, computer-vision, industrial-monitoring, machine-learning]
excerpt: "Image nets detect acoustic anomalies in machinery via spectrograms."
teaser: "/figures/9_image_transfer_sound_teaser.jpg"
icon: "/figures/9_image_transfer_sound_workflow.jpg"
venue: "ICAART 2021"
---
<FloatingImage src="/figures/9_image_transfer_sound_workflow.jpg" alt="Workflow of sound anomaly detection using image transfer learning" width={800} height={400} float="right" caption="Overall workflow for acoustic anomaly detection using transfer learning from image classification models." />

View File

@@ -3,6 +3,7 @@ title: "Acoustic Leak Detection"
tags: [anomaly-detection, audio-processing, deep-learning, signal-processing, real-world-application, water-networks, infrastructure-monitoring]
excerpt: "Anomaly detection models for acoustic leak detection in water networks."
teaser: "/figures/10_water_networks_teaser.jpg"
venue: "ICAART 2021"
---
Detecting leaks in vast municipal water distribution networks is critical for resource conservation and infrastructure maintenance. This study introduces and evaluates an **anomaly detection approach for acoustic leak identification**, specifically designed with **energy efficiency** and **ease of deployment** as key considerations.

View File

@@ -3,6 +3,7 @@ title: "Primate Vocalization Classification"
tags: [deep-learning, audio-classification, bioacoustics, conservation-technology, recurrent-neural-networks, machine-learning, wildlife-monitoring, pytorch, animal-conservation, bayesian-optimization]
excerpt: "Deep BiLSTM classifies primate vocalizations for acoustic wildlife monitoring."
teaser: /figures/11_recurrent_primate_workflow.jpg
venue: "Interspeech 2021"
---
# Primate Vocalization Classification

View File

@@ -3,7 +3,7 @@ title: "Audio Vision Transformer"
tags: [deep-learning, audio-classification, computer-vision, attention-mechanisms, transformers, mel-spectrograms, ComParE-2021]
excerpt: "Vision Transformer on spectrograms for audio classification, with data augmentation."
teaser: /figures/12_vision_transformer_teaser.jpg
venue: "Interspeech 2021"
---
This research explores the application of the **Vision Transformer (ViT)** architecture, originally designed for image processing, to the domain of audio classification by operating on **mel-spectrogram representations**.

View File

@@ -3,6 +3,7 @@ title: "Tasked Self-Replication"
tags: [artificial-life, complex-systems, neural-networks, self-organization, multi-task-learning, self-replication, artificial-chemistry, evolution, computational-systems, guided-evolution, artificial-intelligence]
excerpt: "Self-replicating networks perform tasks, exploring stabilization in artificial chemistry."
teaser: "/figures/13_sr_teaser.jpg"
venue: "ALIFE 2021"
---

View File

@@ -3,7 +3,7 @@ title: "RNN Memory Limits"
tags: [deep-learning, recurrent-neural-networks, sequence-modeling, theoretical-ml, machine-learning, memory-systems]
excerpt: "Investigated memory limits of RNNs in recalling uncorrelated sequences."
teaser: "/figures/22_rnn_limits.png"
venue: "ICAART 2022"
---
Recurrent Neural Networks (RNNs), including variants like Long Short-Term Memory (LSTM) and Gated Recurrent Units (GRU), are designed with the intent to capture temporal dependencies within sequential data. Their internal mechanisms allow information from previous time steps to influence current processing.

View File

@@ -3,7 +3,7 @@ title: "RL Anomaly Detection"
tags: [reinforcement-learning, anomaly-detection, safety, lifelong-learning, generalization]
excerpt: "Perspective on anomaly detection challenges and future in reinforcement learning."
teaser: "/figures/14_ad_rl_teaser.jpg"
venue: "AAMAS 22"
---
Anomaly Detection (AD) is crucial for the safe deployment of Reinforcement Learning (RL) agents, especially in safety-critical applications where encountering unexpected or out-of-distribution situations can lead to catastrophic failures. This work provides a perspective on the state and future directions of AD research specifically tailored for the complexities inherent in RL.

View File

@@ -3,6 +3,7 @@ title: "Extended Self-Replication"
tags: [artificial-life, complex-systems, neural-networks, self-organization, dynamical-systems, self-replication, emergent-behavior, robustness, replication-fidelity]
excerpt: "Journal extension: self-replication, noise robustness, emergence, dynamical system analysis."
teaser: "/figures/15_sr_journal_teaser.jpg"
venue: "Artificial Life 28 (2022)"
---
<FloatingImage src="/figures/15_sr_journal_teaser.jpg" alt="Scatter plot showing the relationship between relative parent distance and replication outcome or child distance" width={600} height={480} float="right" caption="An analysis of replication fidelity, showing how the distance between parent and child networks relates to different parent distances." />

View File

@@ -3,6 +3,7 @@ title: "Organism Network Emergence"
tags: [artificial-life, complex-systems, neural-networks, self-organization, emergent-computation, artificial-intelligence, collective-intelligence, evolutionary-computation]
excerpt: "Self-replicating networks collaborate forming higher-level Organism Networks with emergent functionalities."
teaser: "/figures/16_on_teaser.jpg"
venue: "SSCI 2022"
---
This research investigates the transition from simple self-replication <Cite bibtexKey="gabor2019self" /> to higher levels of organization by exploring how populations of basic, self-replicating neural network units can form **"Organism Networks" (ONs)** through **collaboration and emergent differentiation**. Moving beyond the replication of individual networks, the focus shifts to the collective dynamics and functional capabilities that arise when these units interact within a shared environment (akin to an "artificial chemistry").

View File

@@ -3,6 +3,7 @@ title: "Voronoi Data Augmentation"
tags: [data-augmentation, computer-vision, deep-learning, convolutional-neural-networks, voronoi, machine-learning, image-processing]
excerpt: "VoronoiPatches improves CNN robustness via non-linear recombination augmentation."
teaser: "/figures/17_vp_teaser.jpg"
venue: "ICAART 2023"
---
Data augmentation is essential for improving the performance and generalization of Convolutional Neural Networks (CNNs), especially when training data is limited. This research introduces **VoronoiPatches (VP)**, a novel data augmentation algorithm based on the principle of **non-linear recombination** of image information.

View File

@@ -3,6 +3,7 @@ title: "Autoencoder Trajectory Compression"
tags: [deep-learning, recurrent-neural-networks, trajectory-analysis, data-compression, geoinformatics, autoencoders, LSTM, GPS]
excerpt: "LSTM autoencoder better DP for trajectory compression (Fréchet/DTW)."
teaser: /figures/23_trajectory_model.png
venue: "ICAART 2023"
---
The proliferation of location-aware mobile devices generates vast amounts of GPS trajectory data, necessitating efficient storage solutions. While various compression techniques aim to reduce data volume, preserving essential spatio-temporal information remains crucial.

View File

@@ -3,6 +3,7 @@ title: "Emergent Social Dynamics"
tags: [artificial-life, complex-systems, neural-networks, self-organization, emergent-behavior, predictive-coding, artificial-chemistry, social-interaction]
excerpt: "Artificial chemistry networks develop predictive models via surprise minimization."
teaser: "/figures/18_surprised_soup_teaser.jpg"
venue: "ALIFE 2023"
---
This research extends the study of **artificial chemistry** systems populated by neural network "particles" <Cite bibtexKey="gabor2019self" />, focusing on the emergence of complex behaviors driven by **social interaction** rather than explicit programming. Building on systems where particles may exhibit self-replication, we introduce interactions based on principles of **predictive processing and surprise minimization** (akin to the Free Energy Principle).

View File

@@ -3,6 +3,7 @@ title: "Primate Subsegment Sorting"
tags: [bioacoustics, audio-classification, deep-learning, data-labeling, signal-processing, primate-vocalizations, wildlife-monitoring, machine-learning, spectrograms, cnn]
excerpt: "Binary subsegment presorting improves noisy primate sound classification."
teaser: /figures/19_binary_primates_teaser.jpg
venue: "ICAART 2023"
---
<FloatingImage

View File

@@ -3,6 +3,7 @@ title: "Aquarium MARL Environment"
tags: [MARL, simulation, emergence, complex-systems, environment, predator-prey, reinforcement-learning, multi-agent]
excerpt: "Aquarium: Open-source MARL environment for predator-prey studies."
teaser: /figures/20_aquarium.png
venue: "ICAART 2024"
---
<FloatingImage

View File

@@ -3,7 +3,7 @@ title: "MAS Emergence Safety"
tags: [multi-agent-systems, MARL, safety, emergence, system-specification, decentralized-AI, AI-safety, system-design]
excerpt: "Formalized MAS emergence misalignment; proposed safety mitigation strategies."
teaser: "/figures/21_coins_teaser.png"
venue: "ISoLA 2024"
---
<FloatingImage

View File

@@ -29,7 +29,7 @@ Headings (`#`, `##`, `###`, etc.) automatically generate IDs and anchor links.
You can use **bold**, *italics*, `inline code`, and [links](https://example.com).
Internal links: [Link to another page](/blog/my-other-post)
Internal links: [Link to another page](/category/my-other-post)
Anchor links: [Link to a section on this page](#custom-components)
### Lists

View File

@@ -40,7 +40,7 @@ export async function markdownToHTML(markdown: string) {
export async function getPost(slug: string) {
const filePath = path.join("content", `${slug}.mdx`);
let source = fs.readFileSync(filePath, "utf-8");
const source = fs.readFileSync(filePath, "utf-8");
const { content: rawContent, data: metadata } = matter(source);
const content = await markdownToHTML(rawContent);
return {
@@ -51,14 +51,14 @@ export async function getPost(slug: string) {
}
async function getAllPosts(dir: string, category?: string) {
let mdxFiles = getMDXFiles(dir);
const mdxFiles = getMDXFiles(dir);
console.log(dir);
console.log(category);
console.log(mdxFiles);
const posts = await Promise.all(
mdxFiles.map(async (file) => {
let slug = path.basename(file, path.extname(file));
let { metadata, source } = await getPost(slug);
const slug = path.basename(file, path.extname(file));
const { metadata, source } = await getPost(slug);
console.log(file);
console.log(slug);
console.log(metadata);
@@ -79,8 +79,8 @@ async function getAllPosts(dir: string, category?: string) {
return posts;
}
export async function getBlogPosts(category?: string) {
let postPath = path.join(process.cwd(), "content");
export async function getPosts(category?: string) {
const postPath = path.join(process.cwd(), "content");
console.log(postPath);
return getAllPosts(postPath, category);
}

View File

@@ -1,11 +1,9 @@
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { bundleMDX } from 'mdx-bundler';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
export interface Frontmatter {
title: string;
date: string;
@@ -17,10 +15,16 @@ export interface Frontmatter {
video?: string;
}
export interface Post {
slug: string;
frontmatter: Frontmatter;
code: string;
}
const contentDirectory = path.join(process.cwd(), 'content');
const datePrefixRegex = /^\d{4}-\d{2}-\d{2}-/;
export function getPostSlugs(type: 'projects' | 'research' | 'teaching' | 'experience' ) {
export function getPostSlugs(type: 'projects' | 'research' | 'teaching' | 'experience') {
const typeDirectory = path.join(contentDirectory, type);
if (!fs.existsSync(typeDirectory)) return [];
return fs.readdirSync(typeDirectory).map(file =>
@@ -28,8 +32,9 @@ export function getPostSlugs(type: 'projects' | 'research' | 'teaching' | 'exper
);
}
export async function getPostBySlug(type: 'projects' | 'research' | 'teaching' | 'experience', slug: string) {
export async function getPostBySlug(type: 'projects' | 'research' | 'teaching' | 'experience', slug: string): Promise<Post | null> {
const typeDirectory = path.join(contentDirectory, type);
const allFiles = fs.readdirSync(typeDirectory);
const fileName = allFiles.find(file =>
file.replace(datePrefixRegex, '').replace(/\.mdx$/, '') === slug
@@ -61,18 +66,3 @@ export async function getPostBySlug(type: 'projects' | 'research' | 'teaching' |
code,
};
}
export function getSortedPostsData(type: 'projects' | 'research' | 'teaching' | 'experience') {
const postsDirectory = path.join(contentDirectory, type);
if (!fs.existsSync(postsDirectory)) return [];
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
const slug = fileName.replace(datePrefixRegex, "").replace(/\.mdx$/, "");
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data } = matter(fileContents);
return { slug, href: `/${type}/${slug}`, ...data, };
});
// @ts-ignore
return allPostsData.sort((a, b) => new Date(b.date) - new Date(a.date));
}

View File

@@ -1,4 +1,3 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
@@ -9,13 +8,14 @@ const tagsFilePath = path.join(postsDirectory, "tags.json");
type PostData = {
id: string;
href: string;
venue: string;
date: string;
tags: string[];
image: string | null;
image: string | undefined;
title?: string;
excerpt?: string;
video?: string;
[key: string]: any;
[key: string]: string | string[] | null | undefined;
};
function getPostFilePaths(dir: string, fileList: string[] = []) {
@@ -55,12 +55,14 @@ export function getSortedPostsData(category?: string): PostData[] {
? matterResult.data.tags
: matterResult.data.tags.split(/, ?/)
: [];
const venue = matterResult.data.venue ? matterResult.data.venue: "arxive";
let image = matterResult.data.teaser || matterResult.data.image || null;
const image = matterResult.data.teaser || matterResult.data.image || null;
return {
id,
href,
venue,
date: matterResult.data.date ?? dateFromFileName,
...matterResult.data,
tags,
@@ -77,8 +79,8 @@ export function getSortedPostsData(category?: string): PostData[] {
});
}
export function getAllTags(minCount: number = 1) {
const allPosts = getSortedPostsData();
export function getAllTags(minCount: number = 1, category?: "experience" | "research") {
const allPosts = getSortedPostsData(category);
const tags: { [tag: string]: number } = {};
allPosts.forEach((post) => {

View File

@@ -2,10 +2,8 @@ import { parse } from "@retorquere/bibtex-parser";
import fs from "fs";
import path from "path";
// Your existing path is correct.
const bibliographyPath = path.join(process.cwd(), "content", "_bibliography.bib");
// --- FIX: Add `pdfAvailable` to the Publication interface ---
export interface Publication {
key: string;
title: string;
@@ -14,8 +12,8 @@ export interface Publication {
year: string;
bibtex: string;
pdfUrl: string;
url?: string; // Also make url optional to be safe
pdfAvailable?: boolean; // This will hold the result of our check
url?: string;
pdfAvailable?: boolean;
}
export function getPublicationsData(): Publication[] {
@@ -59,7 +57,7 @@ export function getPublicationsData(): Publication[] {
url: Array.isArray(entry.fields.url) ? entry.fields.url.join(" ") : entry.fields.url,
bibtex: bibtexEntryString,
pdfUrl: `/publications/${entry.key}.pdf`,
pdfAvailable: pdfExists, // <-- Add the result here
pdfAvailable: pdfExists,
};
});
}

View File

@@ -2,7 +2,7 @@
declare global {
interface Window {
umami?: {
track: (eventName: string, eventData?: Record<string, any>) => void;
track: (eventName: string, eventData?: Record<string, string>) => void;
};
}
}

View File

@@ -6,15 +6,15 @@ export function cn(...inputs: ClassValue[]) {
}
export function formatDate(date: string) {
let currentDate = new Date().getTime();
const currentDate = new Date().getTime();
if (!date.includes("T")) {
date = `${date}T00:00:00`;
}
let targetDate = new Date(date).getTime();
let timeDifference = Math.abs(currentDate - targetDate);
let daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
const targetDate = new Date(date).getTime();
const timeDifference = Math.abs(currentDate - targetDate);
const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
let fullDate = new Date(date).toLocaleString("en-us", {
const fullDate = new Date(date).toLocaleString("en-us", {
month: "long",
day: "numeric",
year: "numeric",

View File

@@ -1,12 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
serverComponentsExternalPackages: [
'next-mdx-remote',
'@retorquere/bibtex-parser',
],
},
};
export default nextConfig;

View File

@@ -2,6 +2,15 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* your other config options here */
output: 'standalone',
images: {
remotePatterns: [
{
protocol: "https",
hostname: "*.steffenillium.de",
},
],
},
};
export default nextConfig;

View File

@@ -15,6 +15,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@retorquere/bibtex-parser": "^9.0.21",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
@@ -27,13 +28,18 @@
"react-dom": "19.1.1",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
"remark-html": "^16.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"rough-notation": "^0.5.1",
"shiki": "^3.12.2",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",

185
pnpm-lock.yaml generated
View File

@@ -26,6 +26,9 @@ importers:
'@retorquere/bibtex-parser':
specifier: ^9.0.21
version: 9.0.21
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@4.1.13)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -62,9 +65,15 @@ importers:
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.1.12)(react@19.1.1)
rehype-pretty-code:
specifier: ^0.14.1
version: 0.14.1(shiki@3.12.2)
rehype-slug:
specifier: ^6.0.0
version: 6.0.0
rehype-stringify:
specifier: ^10.0.1
version: 10.0.1
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
@@ -77,12 +86,21 @@ importers:
remark-rehype:
specifier: ^11.1.2
version: 11.1.2
rough-notation:
specifier: ^0.5.1
version: 0.5.1
shiki:
specifier: ^3.12.2
version: 3.12.2
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.13)
unified:
specifier: ^11.0.5
version: 11.0.5
devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.1
@@ -960,6 +978,11 @@ packages:
'@tailwindcss/postcss@4.1.13':
resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==}
'@tailwindcss/typography@0.5.16':
resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tybys/wasm-util@0.10.0':
resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
@@ -1371,6 +1394,11 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -1449,6 +1477,10 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
es-abstract@1.24.0:
resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
engines: {node: '>= 0.4'}
@@ -1810,9 +1842,18 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-from-html@2.0.3:
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-heading-rank@3.0.0:
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
hast-util-sanitize@5.0.2:
resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
@@ -1831,6 +1872,9 @@ packages:
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@@ -2122,6 +2166,12 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@@ -2478,6 +2528,12 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
parse-numeric-range@1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -2504,6 +2560,10 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -2590,12 +2650,24 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
rehype-parse@9.0.1:
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
rehype-pretty-code@0.14.1:
resolution: {integrity: sha512-IpG4OL0iYlbx78muVldsK86hdfNoht0z63AP7sekQNW2QOTmjxB7RbTO+rhIYNGRljgHxgVZoPwUl6bIC9SbjA==}
engines: {node: '>=18'}
peerDependencies:
shiki: ^1.0.0 || ^2.0.0 || ^3.0.0
rehype-recma@1.0.0:
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
rehype-stringify@10.0.1:
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
remark-frontmatter@5.0.0:
resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==}
@@ -2644,6 +2716,9 @@ packages:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rough-notation@0.5.1:
resolution: {integrity: sha512-ITHofTzm13cWFVfoGsh/4c/k2Mg8geKgBCwex71UZLnNuw403tCRjYPQ68jSAd37DMbZIePXPjDgY0XdZi9HPw==}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -2810,6 +2885,11 @@ packages:
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
tailwindcss-animate@1.0.7:
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
tailwindcss@4.1.13:
resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==}
@@ -2926,10 +3006,16 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
vfile-message@3.1.4:
resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==}
@@ -2942,6 +3028,9 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -3711,6 +3800,14 @@ snapshots:
postcss: 8.5.6
tailwindcss: 4.1.13
'@tailwindcss/typography@0.5.16(tailwindcss@4.1.13)':
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.13
'@tybys/wasm-util@0.10.0':
dependencies:
tslib: 2.8.1
@@ -4157,6 +4254,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
cssesc@3.0.0: {}
csstype@3.1.3: {}
damerau-levenshtein@1.0.8: {}
@@ -4232,6 +4331,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.3
entities@6.0.1: {}
es-abstract@1.24.0:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -4784,10 +4885,34 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-from-html@2.0.3:
dependencies:
'@types/hast': 3.0.4
devlop: 1.1.0
hast-util-from-parse5: 8.0.3
parse5: 7.3.0
vfile: 6.0.3
vfile-message: 4.0.3
hast-util-from-parse5@8.0.3:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
devlop: 1.1.0
hastscript: 9.0.1
property-information: 7.1.0
vfile: 6.0.3
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-heading-rank@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-parse-selector@4.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-sanitize@5.0.2:
dependencies:
'@types/hast': 3.0.4
@@ -4857,6 +4982,14 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
hastscript@9.0.1:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 7.1.0
space-separated-tokens: 2.0.2
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
@@ -5122,6 +5255,10 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash.castarray@4.4.0: {}
lodash.isplainobject@4.0.6: {}
lodash.merge@4.6.2: {}
longest-streak@3.1.0: {}
@@ -5775,6 +5912,12 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
parse-numeric-range@1.3.0: {}
parse5@7.3.0:
dependencies:
entities: 6.0.1
path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -5789,6 +5932,11 @@ snapshots:
possible-typed-array-names@1.1.0: {}
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.4.31:
dependencies:
nanoid: 3.3.11
@@ -5912,6 +6060,22 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
rehype-parse@9.0.1:
dependencies:
'@types/hast': 3.0.4
hast-util-from-html: 2.0.3
unified: 11.0.5
rehype-pretty-code@0.14.1(shiki@3.12.2):
dependencies:
'@types/hast': 3.0.4
hast-util-to-string: 3.0.1
parse-numeric-range: 1.3.0
rehype-parse: 9.0.1
shiki: 3.12.2
unified: 11.0.5
unist-util-visit: 5.0.0
rehype-recma@1.0.0:
dependencies:
'@types/estree': 1.0.8
@@ -5928,6 +6092,12 @@ snapshots:
hast-util-to-string: 3.0.1
unist-util-visit: 5.0.0
rehype-stringify@10.0.1:
dependencies:
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
unified: 11.0.5
remark-frontmatter@5.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -6015,6 +6185,8 @@ snapshots:
reusify@1.1.0: {}
rough-notation@0.5.1: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -6248,6 +6420,10 @@ snapshots:
tailwind-merge@3.3.1: {}
tailwindcss-animate@1.0.7(tailwindcss@4.1.13):
dependencies:
tailwindcss: 4.1.13
tailwindcss@4.1.13: {}
tapable@2.2.3: {}
@@ -6424,8 +6600,15 @@ snapshots:
dependencies:
react: 19.1.1
util-deprecate@1.0.2: {}
uuid@9.0.1: {}
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
vfile: 6.0.3
vfile-message@3.1.4:
dependencies:
'@types/unist': 2.0.11
@@ -6448,6 +6631,8 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
web-namespaces@2.0.1: {}
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0

View File

@@ -1,8 +1,8 @@
import type { Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
import defaultTheme from "tailwindcss/defaultTheme";
const config = {
darkMode: ["class"],
darkMode: "class",
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
@@ -23,7 +23,7 @@ const config = {
'18': '4.5rem',
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans],
},
colors: {
border: "hsl(var(--border))",