Skip to content

Commit 287bbfb

Browse files
committed
implement themes and toggle
1 parent 045e402 commit 287bbfb

File tree

11 files changed

+246
-30
lines changed

11 files changed

+246
-30
lines changed

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@
1111
"dependencies": {
1212
"@tailwindcss/postcss": "^4.0.11",
1313
"autoprefixer": "^10.4.20",
14+
"lucide-react": "^0.518.0",
1415
"next": "15.2.1",
16+
"next-themes": "^0.4.6",
1517
"react": "^19.0.0",
1618
"react-dom": "^19.0.0"
1719
},
1820
"devDependencies": {
19-
"typescript": "^5.8.2",
21+
"@eslint/eslintrc": "^3.3.0",
2022
"@types/node": "^22.13.9",
2123
"@types/react": "^19.0.10",
2224
"@types/react-dom": "^19.0.4",
23-
"postcss": "^8.5.3",
24-
"tailwindcss": "^4.0.11",
2525
"eslint": "^9.21.0",
2626
"eslint-config-next": "15.2.1",
27-
"@eslint/eslintrc": "^3.3.0"
27+
"postcss": "^8.5.3",
28+
"tailwindcss": "^4.0.11",
29+
"typescript": "^5.8.2"
2830
},
2931
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
3032
}

src/app/globals.css

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,39 @@
1-
@import 'tailwindcss';
1+
@import "tailwindcss";
22

3+
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
34

4-
/* @tailwind base;
5-
@tailwind components;
6-
@tailwind utilities; */
7-
8-
:root {
9-
--background: #ffffff;
10-
--foreground: #171717;
5+
@theme {
6+
--color-card: hsl(207 97% 12%);
7+
--color-background-light: hsl(0 0% 100%);
8+
--color-background-dark: hsl(0, 0%, 5%);
9+
--color-background-pastel: #cfb7e0;
1110
}
1211

13-
@media (prefers-color-scheme: dark) {
14-
:root {
15-
--background: #0a0a0a;
16-
--foreground: #ededed;
12+
@layer base {
13+
[data-theme="light"] {
14+
--color-card: hsl(207 97% 12%);
15+
}
16+
[data-theme="dark"] {
17+
--color-card: hsl(0 0% 96%);
18+
}
19+
[data-theme="pastel"] {
20+
--color-card: hsl(291 46% 83%);
1721
}
1822
}
1923

20-
body {
21-
color: var(--foreground);
22-
background: var(--background);
24+
:root[data-theme="light"] {
25+
background-color: var(--color-background-light);
26+
color: var(--color-background-dark);
27+
transition: background-color 1s, color 1s;
2328
}
29+
30+
:root[data-theme="dark"] {
31+
background-color: var(--color-background-dark);
32+
color: var(--color-background-light);
33+
transition: background-color 1s, color 1s;
34+
}
35+
36+
:root[data-theme="pastel"] {
37+
background-color: var(--color-background-pastel);
38+
transition: background-color 1s, color 1s;
39+
}

src/app/index/experience-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function ExperienceList() {
4949
fill
5050
className="object-cover rounded"
5151
/>
52-
<div className="w-full h-full relative z-10 p-2 hover:opacity-0 dark:bg-black bg-white dark:opacity-90 opacity:90 transition-opacity duration-500">
52+
<div className="w-full h-full relative z-10 p-2 hover:opacity-0 dark:bg-black bg-white opacity-80 transition-opacity duration-500">
5353
<h2 className="text-lg font-bold">{project.title}</h2>
5454
<p className="text-sm pb-2">{project.dateWorked}</p>
5555
<p className="text-sm" dangerouslySetInnerHTML={{ __html: project.description }}></p>

src/app/index/project-summary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const ProjectSummary: React.FC<ProjectSummaryProps> = ({
3737
fill
3838
className="object-cover rounded"
3939
/>
40-
<div className="w-full h-full relative z-10 p-2 hover:opacity-0 dark:bg-black bg-white dark:opacity-90 opacity:90 transition-opacity duration-500">
40+
<div className="w-full h-full relative z-10 p-2 hover:opacity-0 dark:bg-black bg-white opacity-80 transition-opacity duration-500">
4141
<h2 className="text-lg font-bold">{title}</h2>
4242
<p className="text-sm">{dateWorked}</p>
4343
<p className="text-sm pt-2" dangerouslySetInnerHTML={{ __html: description }}></p>

src/app/layout.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { Metadata } from "next";
22
import { Lato } from "next/font/google";
3+
import { ThemeProvider as NextThemesProvider } from 'next-themes'
34

45
import "./globals.css";
6+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
57
import CanvasBackground from "@/components/background";
68

79
const lato = Lato({
@@ -31,13 +33,25 @@ export default function RootLayout({
3133
children: React.ReactNode;
3234
}>) {
3335
return (
34-
<html lang="en">
36+
<html lang="en" suppressHydrationWarning>
37+
3538
<body
3639
className={`antialiased ${lato.className}`}
3740
>
38-
<CanvasBackground/>
39-
{children}
41+
{/* <CanvasBackground /> */}
42+
43+
44+
<NextThemesProvider
45+
enableSystem
46+
enableColorScheme
47+
// disableTransitionOnChange
48+
defaultTheme="system"
49+
>
50+
{children}
51+
</NextThemesProvider>
52+
4053
</body>
41-
</html>
54+
55+
</html >
4256
);
4357
}

src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function Home() {
1212
return (
1313
// yes 200px is somewhat arbitrary idc
1414
<div className="flex flex-col items-center w-full min-w-[200px]">
15-
15+
1616
<div className="xl:w-[1200px] lg:w-[1000px] md:w-[768px] w-full px-4 flex gap-4 flex-col py-2">
1717
<Header />
1818

src/components/background.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ const CanvasBackground = () => {
3030

3131
// Animation function
3232
const drawStars = () => {
33+
// Get background color from computed style
34+
const computedStyle = getComputedStyle(canvas);
35+
const bgColor = computedStyle.backgroundColor || '#000000';
36+
3337
ctx.clearRect(0, 0, canvas.width, canvas.height);
34-
ctx.fillStyle = '#000000';
38+
ctx.fillStyle = bgColor;
3539
ctx.fillRect(0, 0, canvas.width, canvas.height);
3640

3741
stars.forEach(star => {
@@ -66,7 +70,7 @@ const CanvasBackground = () => {
6670
width: '100vw',
6771
height: '100vh',
6872
zIndex: -1,
69-
backgroundColor: '#000000'
73+
backgroundColor: '#000000', // Default background color
7074
}}
7175
/>
7276
);

src/components/header.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import Link from 'next/link'
2+
import { ChangeThemeButton } from './theme-button'
23

34
const Header = () => {
45
return (
56
<header className="w-full">
67

78
<nav className="w-full mx-auto grid grid-cols-[1fr_auto] items-center text-xl">
89

9-
<div className="@container justify-start col-start-1 w-full">
10+
<div className="@container justify-start col-start-1 w-full align-middle">
1011
{/* Container queries are so freaking cool */}
1112
<Link href="/" className="block py-2 p w-fit font-bold text-gray-500 dark:hover:text-gray-200 hover:text-gray-900">
1213
<span className="block @min-[8ch]:hidden w-fit">AT</span>
@@ -16,7 +17,7 @@ const Header = () => {
1617
</Link>
1718
</div>
1819

19-
<div className="justify-self-end gap-2 sm:gap-4 flex justify-end font-bold">
20+
<div className="justify-self-end gap-2 sm:gap-4 flex justify-end items-center font-bold align-middle">
2021
<Link href="/about" className="py-2 text-gray-500 dark:hover:text-gray-200 hover:text-gray-900">
2122
About
2223
</Link>
@@ -26,6 +27,7 @@ const Header = () => {
2627
<Link href="/blog" className="py-2 text-gray-500 dark:hover:text-gray-200 hover:text-gray-900">
2728
Blog
2829
</Link>
30+
<ChangeThemeButton/>
2931
</div>
3032

3133
</nav>

src/components/theme-button.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import { useTheme } from "next-themes";
4+
import { useEffect, useState } from "react";
5+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6+
import { Sun, Moon, Palette } from "lucide-react";
7+
8+
const themes = [
9+
{ id: "light", label: "Light Mode", icon: <Sun size={20} /> },
10+
{ id: "dark", label: "Dark Mode", icon: <Moon size={20} /> },
11+
{ id: "pastel", label: "Pastel Mode", icon: <Palette size={20} /> },
12+
];
13+
14+
export const ChangeThemeButton = () => {
15+
const { theme, setTheme, resolvedTheme } = useTheme();
16+
const [mounted, setMounted] = useState(false);
17+
18+
useEffect(() => {
19+
setMounted(true);
20+
}, []);
21+
22+
useEffect(() => {
23+
if (mounted && !theme) {
24+
const systemTheme =
25+
resolvedTheme ||
26+
(window.matchMedia("(prefers-color-scheme: dark)").matches
27+
? "dark"
28+
: "light");
29+
setTheme(systemTheme);
30+
}
31+
}, [mounted, theme, resolvedTheme, setTheme]);
32+
33+
if (!mounted) {
34+
return (
35+
<button
36+
className="flex items-center justify-center w-10 h-10 rounded-md bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
37+
aria-label="Loading Theme..."
38+
>
39+
...
40+
</button>
41+
);
42+
}
43+
44+
const effectiveTheme = resolvedTheme || theme;
45+
const currentIndex = themes.findIndex((t) => t.id === effectiveTheme);
46+
const currentTheme = themes[currentIndex] || themes[0];
47+
const nextIndex = (currentIndex + 1) % themes.length;
48+
const nextTheme = themes[nextIndex];
49+
50+
const handleClick = () => {
51+
setTheme(nextTheme.id);
52+
};
53+
54+
return (
55+
<button
56+
onClick={handleClick}
57+
className={`flex items-center justify-center w-10 h-10 rounded-md transition-colors
58+
bg-gray-200 text-gray-800 hover:bg-gray-300
59+
dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600`}
60+
aria-label={`Switch to ${nextTheme.label}`}
61+
title={currentTheme.label}
62+
>
63+
{currentTheme.icon}
64+
</button>
65+
);
66+
};

src/components/theme-buttons.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
3+
import { useTheme } from "next-themes";
4+
import { useEffect, useState } from "react";
5+
6+
interface ThemeButtonProps {
7+
themeName: string;
8+
currentTheme: string | undefined;
9+
onClick: () => void;
10+
label: string;
11+
}
12+
13+
interface StaticThemeButtonProps {
14+
label: string;
15+
}
16+
17+
const ThemeButton = ({
18+
themeName,
19+
currentTheme,
20+
onClick,
21+
label,
22+
}: ThemeButtonProps) => {
23+
return (
24+
<button
25+
onClick={onClick}
26+
className={`flex items-center cursor-pointer px-4 py-2 rounded-md transition-colors ${
27+
currentTheme === themeName
28+
? "bg-blue-500 text-white"
29+
: "bg-gray-200 text-gray-800 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
30+
}`}
31+
>
32+
{label}
33+
</button>
34+
);
35+
};
36+
37+
const StaticThemeButton = ({ label }: StaticThemeButtonProps) => {
38+
return (
39+
<button className="flex items-center cursor-pointer px-4 py-2 rounded-md bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
40+
{label}
41+
</button>
42+
);
43+
};
44+
45+
export const ThemeButtons = () => {
46+
const { theme, setTheme, resolvedTheme } = useTheme();
47+
const [mounted, setMounted] = useState<boolean>(false);
48+
49+
interface ThemeOption {
50+
id: string;
51+
label: string;
52+
}
53+
54+
const themes: ThemeOption[] = [
55+
{ id: "light", label: "Light Mode" },
56+
{ id: "dark", label: "Dark Mode" },
57+
{ id: "custom", label: "Custom Mode" },
58+
{ id: "pastel", label: "Pastel Mode" },
59+
];
60+
61+
useEffect(() => {
62+
setMounted(true);
63+
}, []);
64+
65+
useEffect(() => {
66+
if (mounted && !theme) {
67+
const systemTheme =
68+
resolvedTheme ||
69+
(window.matchMedia("(prefers-color-scheme: dark)").matches
70+
? "dark"
71+
: "light");
72+
73+
setTheme(systemTheme);
74+
}
75+
}, [mounted, theme, resolvedTheme, setTheme]);
76+
77+
if (!mounted) {
78+
return (
79+
<div className="flex flex-col gap-4">
80+
{themes.map((themeOption: ThemeOption) => (
81+
<StaticThemeButton key={themeOption.id} label={themeOption.label} />
82+
))}
83+
</div>
84+
);
85+
}
86+
87+
const effectiveTheme = resolvedTheme || theme;
88+
89+
return (
90+
<div className="flex flex-col gap-4">
91+
{themes.map((themeOption: ThemeOption) => (
92+
<ThemeButton
93+
key={themeOption.id}
94+
themeName={themeOption.id}
95+
currentTheme={effectiveTheme}
96+
onClick={() => setTheme(themeOption.id)}
97+
label={themeOption.label}
98+
/>
99+
))}
100+
</div>
101+
);
102+
};

0 commit comments

Comments
 (0)