Add src/components/ThemeSwitcher.tsx
This commit is contained in:
177
src/components/ThemeSwitcher.tsx
Normal file
177
src/components/ThemeSwitcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user