Skip to content

Commit 22e8d0a

Browse files
committed
feat: implement theme provider and toggle functionality with dark mode support
1 parent 25ca249 commit 22e8d0a

9 files changed

Lines changed: 139 additions & 42 deletions

File tree

app/components/home/HeroSection.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function HeroSection() {
1616
return (
1717
<section className="relative min-h-[500px] sm:min-h-[600px] lg:min-h-[700px] flex items-center justify-center overflow-hidden">
1818
{/* Animated Background */}
19-
<div className="absolute inset-0 bg-linear-to-br from-gray-900 via-gray-800 to-black dark:from-black dark:via-gray-900 dark:to-gray-800">
19+
<div className="absolute inset-0 bg-linear-to-br from-gray-100 via-white to-gray-200 dark:from-black dark:via-gray-900 dark:to-gray-800 transition-colors">
2020
{/* Animated geometric shapes */}
2121
<motion.div
2222
animate={{
@@ -29,7 +29,7 @@ export default function HeroSection() {
2929
repeat: Infinity,
3030
ease: 'linear',
3131
}}
32-
className="absolute top-1/4 left-1/4 w-64 h-64 bg-wso2-orange/20 rounded-full blur-3xl"
32+
className="absolute top-1/4 left-1/4 w-64 h-64 bg-wso2-orange/15 dark:bg-wso2-orange/20 rounded-full blur-3xl"
3333
/>
3434
<motion.div
3535
animate={{
@@ -42,7 +42,7 @@ export default function HeroSection() {
4242
repeat: Infinity,
4343
ease: 'linear',
4444
}}
45-
className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-wso2-orange/10 rounded-full blur-3xl"
45+
className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-wso2-orange/10 dark:bg-wso2-orange/15 rounded-full blur-3xl"
4646
/>
4747
<motion.div
4848
animate={{
@@ -54,11 +54,11 @@ export default function HeroSection() {
5454
repeat: Infinity,
5555
ease: 'easeInOut',
5656
}}
57-
className="absolute top-1/2 left-1/2 w-72 h-72 bg-orange-500/20 rounded-full blur-3xl transform -translate-x-1/2 -translate-y-1/2"
57+
className="absolute top-1/2 left-1/2 w-72 h-72 bg-orange-400/15 dark:bg-orange-500/20 rounded-full blur-3xl transform -translate-x-1/2 -translate-y-1/2"
5858
/>
5959

6060
{/* Grid overlay */}
61-
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,115,0,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(255,115,0,0.05)_1px,transparent_1px)] bg-size-[50px_50px] mask-[radial-gradient(ellipse_80%_80%_at_50%_50%,black,transparent)]" />
61+
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,115,0,0.08)_1px,transparent_1px),linear-gradient(90deg,rgba(255,115,0,0.08)_1px,transparent_1px)] dark:bg-[linear-gradient(rgba(255,115,0,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(255,115,0,0.05)_1px,transparent_1px)] bg-size-[50px_50px] mask-[radial-gradient(ellipse_80%_80%_at_50%_50%,black,transparent)] transition-colors" />
6262
</div>
6363

6464
{/* Content */}
@@ -69,7 +69,7 @@ export default function HeroSection() {
6969
transition={{ duration: 0.8 }}
7070
>
7171
<motion.h1
72-
className="text-3xl sm:text-4xl md:text-5xl lg:text-7xl font-bold mb-4 sm:mb-6 text-white px-4"
72+
className="text-3xl sm:text-4xl md:text-5xl lg:text-7xl font-bold mb-4 sm:mb-6 text-gray-900 dark:text-white px-4 transition-colors"
7373
initial={{ opacity: 0, y: 20 }}
7474
animate={{ opacity: 1, y: 0 }}
7575
transition={{ delay: 0.2, duration: 0.8 }}
@@ -80,7 +80,7 @@ export default function HeroSection() {
8080
</motion.h1>
8181

8282
<motion.p
83-
className="text-base sm:text-lg md:text-xl lg:text-2xl text-gray-300 mb-8 sm:mb-12 max-w-3xl mx-auto px-4"
83+
className="text-base sm:text-lg md:text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-8 sm:mb-12 max-w-3xl mx-auto px-4 transition-colors"
8484
initial={{ opacity: 0, y: 20 }}
8585
animate={{ opacity: 1, y: 0 }}
8686
transition={{ delay: 0.4, duration: 0.8 }}
@@ -97,7 +97,7 @@ export default function HeroSection() {
9797
<AnimatedButton href="/builderbot" variant="primary" className="text-base sm:text-lg px-6 sm:px-8 py-3 sm:py-4 w-full sm:w-auto">
9898
Try the BuilderBot
9999
</AnimatedButton>
100-
<AnimatedButton href="/pre-builts" variant="outline" className="text-base sm:text-lg px-6 sm:px-8 py-3 sm:py-4 border-white text-white hover:bg-white hover:text-gray-900 w-full sm:w-auto">
100+
<AnimatedButton href="/pre-builts" variant="outlineContrast" className="text-base sm:text-lg px-6 sm:px-8 py-3 sm:py-4 w-full sm:w-auto">
101101
Shop Pre-Built Rigs
102102
</AnimatedButton>
103103
</motion.div>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useThemeStore } from '@/lib/store/themeStore';
5+
6+
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
7+
const theme = useThemeStore((state) => state.theme);
8+
const [mounted, setMounted] = useState(false);
9+
10+
// Set mounted flag
11+
useEffect(() => {
12+
setMounted(true);
13+
}, []);
14+
15+
// Apply theme to DOM whenever it changes - only after mounted
16+
useEffect(() => {
17+
if (!mounted) return;
18+
19+
const root = document.documentElement;
20+
21+
// Remove both classes first
22+
root.classList.remove('dark', 'light');
23+
24+
// Add the current theme class
25+
if (theme === 'dark') {
26+
root.classList.add('dark');
27+
} else {
28+
root.classList.add('light');
29+
}
30+
31+
root.setAttribute('data-theme', theme);
32+
}, [theme, mounted]);
33+
34+
return <>{children}</>;
35+
}

app/components/ui/AnimatedButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface AnimatedButtonProps {
77
children: ReactNode;
88
onClick?: () => void;
99
href?: string;
10-
variant?: 'primary' | 'secondary' | 'outline';
10+
variant?: 'primary' | 'secondary' | 'outline' | 'outlineContrast';
1111
className?: string;
1212
type?: 'button' | 'submit';
1313
}
@@ -26,6 +26,8 @@ export default function AnimatedButton({
2626
primary: 'bg-wso2-orange text-white hover:bg-wso2-orange-dark shadow-lg hover:shadow-xl',
2727
secondary: 'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-white hover:bg-gray-300 dark:hover:bg-gray-700',
2828
outline: 'border-2 border-wso2-orange text-wso2-orange hover:bg-wso2-orange hover:text-white',
29+
outlineContrast:
30+
'border-2 border-wso2-orange text-wso2-orange hover:bg-wso2-orange hover:text-white dark:border-white dark:text-white dark:hover:bg-white dark:hover:text-gray-900',
2931
};
3032

3133
const combinedClasses = `${baseClasses} ${variantClasses[variant]} ${className}`;

app/components/ui/Footer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,14 @@ export default function Footer() {
101101
className="flex flex-col sm:flex-row gap-3 max-w-md w-full md:w-auto"
102102
>
103103
<div className="relative flex-1">
104-
<Mail className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
104+
<Mail className="absolute left-3 top-3 h-5 w-5 text-gray-400 dark:text-gray-300" />
105105
<input
106106
type="email"
107107
value={email}
108108
onChange={(e) => setEmail(e.target.value)}
109109
placeholder="Enter your email"
110110
required
111-
className="w-full pl-10 pr-4 py-3 rounded-lg border-0 focus:outline-none focus:ring-2 focus:ring-white"
111+
className="w-full pl-10 pr-4 py-3 rounded-lg border border-gray-200 dark:border-white/15 bg-white dark:bg-white/5 text-gray-900 dark:text-white placeholder:text-gray-500 dark:placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-wso2-orange focus:border-wso2-orange transition"
112112
/>
113113
</div>
114114
<motion.button

app/globals.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
@import "tailwindcss";
22

3+
@theme {
4+
--color-wso2-orange: #FF7300;
5+
--color-wso2-orange-dark: #E66800;
6+
}
7+
38
@layer base {
49
:root {
510
--background: #ffffff;
611
--foreground: #1f2937;
712
--panel: #f3f4f6;
813
}
914

15+
@media (prefers-color-scheme: dark) {
16+
:root {
17+
--background: #111827;
18+
--foreground: #d1d5db;
19+
--panel: #1f2937;
20+
}
21+
}
22+
1023
.dark {
1124
--background: #111827;
1225
--foreground: #d1d5db;
@@ -46,3 +59,5 @@
4659
background: #E66800; /* wso2-orange-dark */
4760
}
4861
}
62+
63+
@variant dark (&:where(.dark, .dark *));

app/layout.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./globals.css";
33
import Navigation from "./components/ui/Navigation";
44
import Footer from "./components/ui/Footer";
55
import LiveChatWidget from "./components/ui/LiveChatWidget";
6+
import ThemeProvider from "./components/providers/ThemeProvider";
67

78
export const metadata: Metadata = {
89
title: "CS02 - Build Your Dream PC",
@@ -15,20 +16,43 @@ export default function RootLayout({
1516
children: React.ReactNode;
1617
}>) {
1718
return (
18-
<html lang="en">
19+
<html lang="en" suppressHydrationWarning>
1920
<head>
2021
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
2122
<link rel="preconnect" href="https://fonts.googleapis.com" />
2223
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
2324
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet" />
25+
<script
26+
dangerouslySetInnerHTML={{
27+
__html: `
28+
(function() {
29+
try {
30+
const stored = localStorage.getItem('theme-storage');
31+
if (stored) {
32+
const { state } = JSON.parse(stored);
33+
if (state?.theme === 'dark') {
34+
document.documentElement.classList.add('dark');
35+
} else {
36+
document.documentElement.classList.remove('dark');
37+
}
38+
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
39+
document.documentElement.classList.add('dark');
40+
}
41+
} catch (e) {}
42+
})();
43+
`,
44+
}}
45+
/>
2446
</head>
2547
<body className="antialiased">
26-
<Navigation />
27-
<main className="min-h-screen w-full">
28-
{children}
29-
</main>
30-
<Footer />
31-
<LiveChatWidget />
48+
<ThemeProvider>
49+
<Navigation />
50+
<main className="min-h-screen w-full">
51+
{children}
52+
</main>
53+
<Footer />
54+
<LiveChatWidget />
55+
</ThemeProvider>
3256
</body>
3357
</html>
3458
);

lib/store/themeStore.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,35 @@ interface ThemeStore {
77
theme: Theme;
88
toggleTheme: () => void;
99
setTheme: (theme: Theme) => void;
10+
initializeTheme: () => void;
1011
}
1112

1213
export const useThemeStore = create<ThemeStore>()(
1314
persist(
14-
(set) => ({
15+
(set, get) => ({
1516
theme: 'light',
16-
toggleTheme: () =>
17-
set((state) => {
18-
const newTheme = state.theme === 'light' ? 'dark' : 'light';
19-
if (typeof document !== 'undefined') {
20-
if (newTheme === 'dark') {
21-
document.documentElement.classList.add('dark');
22-
} else {
23-
document.documentElement.classList.remove('dark');
24-
}
25-
}
26-
return { theme: newTheme };
27-
}),
28-
setTheme: (theme) =>
29-
set(() => {
30-
if (typeof document !== 'undefined') {
31-
if (theme === 'dark') {
32-
document.documentElement.classList.add('dark');
33-
} else {
34-
document.documentElement.classList.remove('dark');
35-
}
36-
}
37-
return { theme };
38-
}),
17+
toggleTheme: () => {
18+
set((state) => ({
19+
theme: state.theme === 'light' ? 'dark' : 'light'
20+
}));
21+
},
22+
setTheme: (theme) => {
23+
set(() => ({ theme }));
24+
},
25+
initializeTheme: () => {
26+
// Let persist middleware handle rehydration
27+
},
3928
}),
4029
{
4130
name: 'theme-storage',
31+
onRehydrateStorage: () => (state) => {
32+
// If no stored theme, check system preference
33+
if (!state || !state.theme) {
34+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
35+
const theme = prefersDark ? 'dark' : 'light';
36+
useThemeStore.setState({ theme });
37+
}
38+
},
4239
}
4340
)
4441
);

tailwind.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const config: Config = {
1212
colors: {
1313
background: "hsl(var(--background))",
1414
foreground: "hsl(var(--foreground))",
15+
"wso2-orange": "#FF7300",
16+
"wso2-orange-dark": "#E66800",
1517
},
1618
},
1719
},

test-theme.html

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html lang="en" class="dark">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Theme Test</title>
6+
<script src="https://cdn.tailwindcss.com"></script>
7+
<script>
8+
tailwind.config = {
9+
darkMode: 'class'
10+
}
11+
</script>
12+
</head>
13+
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white min-h-screen">
14+
<div class="p-8">
15+
<h1 class="text-4xl font-bold mb-4">Theme Test</h1>
16+
<p class="text-gray-600 dark:text-gray-400">This text should change color</p>
17+
<button onclick="document.documentElement.classList.toggle('dark')" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
18+
Toggle Theme
19+
</button>
20+
</div>
21+
</body>
22+
</html>

0 commit comments

Comments
 (0)