Skip to content
51 changes: 51 additions & 0 deletions public/banner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import "./globals.css";
import { ThemeProvider } from "./theme-provider";

export const metadata: Metadata = {
title: "ManyBodyLab - Quantum Many-Body Physics Software",
Expand All @@ -14,7 +15,9 @@ export default function RootLayout({
return (
<html lang="en">
<body className="antialiased">
{children}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
Expand Down
94 changes: 32 additions & 62 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,11 @@
"use client";

import Link from "next/link";
import { useState, useEffect } from "react";
import Image from "next/image";
import { categories, getPackagesByCategory, type Package } from "@/data/packages";

const bannerSlides = [
{
title: "Open-Source Quantum Many-Body Physics",
subtitle: "High-quality software tools for researchers and students",
gradient: "from-blue-900 to-blue-600"
},
{
title: "Exact Diagonalization Solutions",
subtitle: "Precise numerical methods for quantum systems",
gradient: "from-blue-700 to-blue-500"
},
{
title: "Tensor Network Methods",
subtitle: "Efficient representations of quantum states",
gradient: "from-blue-800 to-blue-400"
}
];
import { ThemeToggle } from "./theme-toggle";

export default function Home() {
const [currentSlide, setCurrentSlide] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCurrentSlide((prev) => (prev + 1) % bannerSlides.length);
}, 5000);
return () => clearInterval(timer);
}, []);

return (
<div className="min-h-screen bg-white dark:bg-gray-950">
Expand All @@ -46,7 +21,7 @@ export default function Home() {
Open-source software for quantum many-body physics
</p>
</div>
<nav className="flex gap-4 sm:gap-6">
<nav className="flex gap-4 sm:gap-6 items-center">
<Link
href="/"
className="text-sm sm:text-base text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-400 font-medium transition-colors"
Expand All @@ -59,47 +34,42 @@ export default function Home() {
>
People
</Link>
<ThemeToggle />
</nav>
</div>
</div>
</header>

{/* Rotating Banner */}
<section className="relative h-[300px] sm:h-[400px] md:h-[500px] overflow-hidden">
{bannerSlides.map((slide, index) => (
<div
key={index}
className={`absolute inset-0 transition-opacity duration-1000 ${
index === currentSlide ? "opacity-100" : "opacity-0"
}`}
>
<div className={`w-full h-full bg-gradient-to-r ${slide.gradient} flex items-center justify-center`}>
<div className="text-center px-4 sm:px-6">
<h2 className="text-3xl sm:text-4xl md:text-6xl font-bold text-white mb-4 sm:mb-6">
{slide.title}
</h2>
<p className="text-lg sm:text-xl md:text-2xl text-white/90">
{slide.subtitle}
</p>
</div>
{/* Hero Banner Section with Content Overlay */}
<section className="relative h-[400px] sm:h-[500px] md:h-[600px] overflow-hidden">
{/* Banner Background */}
<div className="absolute inset-0">
<Image
src="/banner.svg"
alt="ManyBodyLab - Open-Source Quantum Many-Body Physics"
fill
className="object-cover"
priority
/>
{/* Overlay for better text readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-black/20 to-black/40" />
</div>

{/* Hero Content */}
<div className="relative h-full flex items-center px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto w-full">
<div className="max-w-2xl">
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4 sm:mb-6 drop-shadow-lg">
Open-Source Quantum Many-Body Physics
</h2>
<p className="text-lg sm:text-xl md:text-2xl text-white/95 mb-6 sm:mb-8 drop-shadow-md">
High-quality software tools for researchers and students
</p>
<p className="text-base sm:text-lg text-white/90 drop-shadow-md">
From first principles to scalable computation
</p>
</div>
</div>
))}

{/* Banner Navigation Dots */}
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 flex gap-2">
{bannerSlides.map((_, index) => (
<button
key={index}
onClick={() => setCurrentSlide(index)}
className={`w-2 h-2 sm:w-3 sm:h-3 rounded-full transition-all ${
index === currentSlide
? "bg-white w-6 sm:w-8"
: "bg-white/50 hover:bg-white/75"
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</section>

Expand Down
55 changes: 55 additions & 0 deletions src/app/theme-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { createContext, useContext, useEffect, useState } from "react";

type Theme = "light" | "dark";

interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window === "undefined") {
return "light";
}

const savedTheme = window.localStorage.getItem("theme");
if (savedTheme === "light" || savedTheme === "dark") {
return savedTheme;
}

const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
return isDark ? "dark" : "light";
});

useEffect(() => {
if (typeof window === "undefined") return;
document.documentElement.classList.toggle("dark", theme === "dark");
}, [theme]);

const toggleTheme = () => {
if (typeof window === "undefined") return;

const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
window.localStorage.setItem("theme", newTheme);
};
Comment on lines +34 to +40
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toggleTheme function should check if window is defined before accessing localStorage and document, similar to the useEffect. This prevents potential SSR errors if the function is somehow called during server-side rendering.

Copilot uses AI. Check for mistakes.

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
61 changes: 61 additions & 0 deletions src/app/theme-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useTheme } from "./theme-provider";
import { useState, useEffect } from "react";

export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

// Prevent hydration mismatch by not rendering until mounted
if (!mounted) {
return (
<div className="p-2 rounded-lg bg-gray-100 w-9 h-9" />
);
}

return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-pressed={theme === "dark"}
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
>
{theme === "light" ? (
<svg
className="w-5 h-5 text-gray-800 dark:text-gray-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
) : (
<svg
className="w-5 h-5 text-gray-800 dark:text-gray-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
)}
</button>
);
}
4 changes: 2 additions & 2 deletions src/data/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const packages: Package[] = [
language: "Julia",
githubUrl: "https://github.com/ManyBodyLab/PeriodicArrays.jl",
docsUrl: "https://manybodylab.github.io/PeriodicArrays.jl",
category: "Generic Julia Packages"
category: "Utility Libraries"
},
{
name: "TensorKitAdapters.jl",
Expand Down Expand Up @@ -68,7 +68,7 @@ export const categories: Category[] = [
icon: ""
},
{
name: "Generic Julia Packages",
name: "Utility Libraries",
description: "General-purpose Julia utilities and tools. These packages provide fundamental building blocks and utilities that can be used across different computational physics applications.",
icon: ""
}
Expand Down
2 changes: 1 addition & 1 deletion src/data/people.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const MEMBERS: Member[] = [
name: "Andreas Feuerpfeil",
bio: "Condensed matter physicist and julia enthusiast, developing open-source software for many-body physics in the @ManyBodyLab organization.",
github: "AFeuerpfeil",
linkedin: "andreas-feuerpfeil",
linkedin: "andreasfeuerpfeil",
avatar: "https://avatars.githubusercontent.com/u/36232041?v=4",
},
];