Add src/components/ThemeSwitcher.tsx

This commit is contained in:
2026-03-05 03:02:21 +00:00
parent b2c7d5c215
commit 1f20e4b79f

View File

@@ -0,0 +1,177 @@
"use client";
import { useState, useEffect } from "react";
import { Moon, Sun, Palette } from "lucide-react";
interface ThemeOption {
name: string;
label: string;
colors: {
background: string;
card: string;
foreground: string;
primaryCta: string;
secondaryCta: string;
accent: string;
backgroundAccent: string;
};
}
const PRESET_THEMES: Record<string, ThemeOption> = {
light: {
name: "light", label: "Light Mode", colors: {
background: "#ffffff", card: "#f9f9f9", foreground: "#000612e6", primaryCta: "#15479c", secondaryCta: "#f9f9f9", accent: "#e2e2e2", backgroundAccent: "#c4c4c4"},
},
dark: {
name: "dark", label: "Dark Mode", colors: {
background: "#0a0a0a", card: "#1a1a1a", foreground: "#ffffffe6", primaryCta: "#e6e6e6", secondaryCta: "#1a1a1a", accent: "#737373", backgroundAccent: "#737373"},
},
darkBlue: {
name: "darkBlue", label: "Dark Blue", colors: {
background: "#010912", card: "#152840", foreground: "#e6f0ff", primaryCta: "#cee7ff", secondaryCta: "#0e1a29", accent: "#3f5c79", backgroundAccent: "#004a93"},
},
emerald: {
name: "emerald", label: "Emerald", colors: {
background: "#000000", card: "#1f4035", foreground: "#ffffff", primaryCta: "#ffffff", secondaryCta: "#0d2b1f", accent: "#0d5238", backgroundAccent: "#10b981"},
},
violet: {
name: "violet", label: "Violet", colors: {
background: "#030128", card: "#241f48", foreground: "#ffffff", primaryCta: "#ffffff", secondaryCta: "#131136", accent: "#44358a", backgroundAccent: "#b597fe"},
},
ruby: {
name: "ruby", label: "Ruby", colors: {
background: "#000000", card: "#481f1f", foreground: "#ffffff", primaryCta: "#ffffff", secondaryCta: "#361311", accent: "#51000b", backgroundAccent: "#ff2231"},
},
};
export default function ThemeSwitcher() {
const [isOpen, setIsOpen] = useState(false);
const [currentTheme, setCurrentTheme] = useState<string>("light");
const [customColors, setCustomColors] = useState<Record<string, string> | null>(null);
const [showCustom, setShowCustom] = useState(false);
useEffect(() => {
const saved = localStorage.getItem("theme");
const savedCustom = localStorage.getItem("customTheme");
if (saved) setCurrentTheme(saved);
if (savedCustom) {
setCustomColors(JSON.parse(savedCustom));
setShowCustom(true);
}
}, []);
const applyTheme = (theme: string) => {
const themeOption = PRESET_THEMES[theme];
if (themeOption) {
Object.entries(themeOption.colors).forEach(([key, value]) => {
const cssKey = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
document.documentElement.style.setProperty(cssKey, value);
});
setCurrentTheme(theme);
setShowCustom(false);
localStorage.setItem("theme", theme);
localStorage.removeItem("customTheme");
}
};
const applyCustomTheme = (colors: Record<string, string>) => {
Object.entries(colors).forEach(([key, value]) => {
const cssKey = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
if (value) document.documentElement.style.setProperty(cssKey, value);
});
setCustomColors(colors);
setShowCustom(true);
localStorage.setItem("customTheme", JSON.stringify(colors));
};
const handleColorChange = (
colorKey: string,
value: string
) => {
const updated = { ...customColors, [colorKey]: value };
applyCustomTheme(updated);
};
return (
<div className="fixed bottom-6 right-6 z-50">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-12 h-12 rounded-full bg-primary-cta text-white flex items-center justify-center shadow-lg hover:shadow-xl transition-shadow"
aria-label="Toggle theme switcher"
>
<Palette size={20} />
</button>
{isOpen && (
<div className="absolute bottom-16 right-0 bg-card border border-accent rounded-lg shadow-xl p-4 w-80 max-h-96 overflow-y-auto">
<div className="space-y-3">
<h3 className="text-sm font-semibold text-foreground mb-4">Select Theme</h3>
{/* Preset themes */}
<div className="grid grid-cols-2 gap-2">
{Object.entries(PRESET_THEMES).map(([key, theme]) => (
<button
key={key}
onClick={() => applyTheme(key)}
className={`p-2 rounded text-xs font-medium transition-all ${
currentTheme === key && !showCustom
? "bg-primary-cta text-white"
: "bg-background text-foreground border border-accent hover:border-primary-cta"
}`}
>
{theme.label}
</button>
))}
</div>
{/* Custom theme toggle */}
<button
onClick={() => setShowCustom(!showCustom)}
className={`w-full p-2 rounded text-sm font-medium transition-all ${
showCustom
? "bg-accent text-foreground"
: "bg-background text-foreground border border-accent hover:border-accent"
}`}
>
{showCustom ? "Hide Custom" : "Create Custom"}
</button>
{/* Custom color picker */}
{showCustom && (
<div className="space-y-3 pt-3 border-t border-accent">
<div className="text-xs font-semibold text-foreground">Custom Colors</div>
{[
{ key: "background", label: "Background" },
{ key: "card", label: "Card" },
{ key: "foreground", label: "Foreground" },
{ key: "primaryCta", label: "Primary CTA" },
{ key: "secondaryCta", label: "Secondary CTA" },
{ key: "accent", label: "Accent" },
{ key: "backgroundAccent", label: "Background Accent" },
].map(({ key, label }) => (
<div key={key} className="flex items-center gap-2">
<label className="text-xs text-foreground w-24">{label}</label>
<input
type="color"
value={customColors?.[key] || "#000000"}
onChange={(e) => handleColorChange(key, e.target.value)}
className="w-8 h-8 rounded cursor-pointer"
aria-label={`Set ${label} color`}
/>
<input
type="text"
value={customColors?.[key] || "#000000"}
onChange={(e) => handleColorChange(key, e.target.value)}
className="flex-1 px-2 py-1 text-xs bg-background border border-accent rounded text-foreground"
placeholder="#000000"
/>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
);
}