From d910ff1a93128f5481d19de25a758a6838c34021 Mon Sep 17 00:00:00 2001 From: angel Date: Thu, 19 Jun 2025 03:57:19 +0000 Subject: [PATCH] Implement theme provider and settings page for customizable appearance --- app/components/ThemeProvider.tsx | 81 ++++++++++++++++++++ app/globals.css | 122 ++++++++++++++++++++++++++++++- app/layout.tsx | 43 ++++++++--- app/settings/page.tsx | 82 ++++++++++++++++++--- 4 files changed, 306 insertions(+), 22 deletions(-) create mode 100644 app/components/ThemeProvider.tsx diff --git a/app/components/ThemeProvider.tsx b/app/components/ThemeProvider.tsx new file mode 100644 index 0000000..1a216ea --- /dev/null +++ b/app/components/ThemeProvider.tsx @@ -0,0 +1,81 @@ +'use client'; + +import React, { createContext, useContext, useEffect, useState } from 'react'; + +type Theme = 'blue' | 'violet'; + +interface ThemeContextType { + theme: Theme; + setTheme: (theme: Theme) => void; +} + +const ThemeContext = createContext(undefined); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +interface ThemeProviderProps { + children: React.ReactNode; +} + +export const ThemeProvider: React.FC = ({ children }) => { + const [theme, setTheme] = useState('blue'); + const [mounted, setMounted] = useState(false); + + // Load theme settings from localStorage on component mount + useEffect(() => { + setMounted(true); + + const savedTheme = localStorage.getItem('theme') as Theme | null; + + if (savedTheme && (savedTheme === 'blue' || savedTheme === 'violet')) { + setTheme(savedTheme); + } + }, []); + + // Apply theme changes + useEffect(() => { + if (!mounted) return; + + const root = document.documentElement; + + // Remove existing theme classes + root.classList.remove('theme-blue', 'theme-violet', 'dark'); + + // Add new theme class + root.classList.add(`theme-${theme}`); + + // Always follow system preference for dark mode + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const applySystemTheme = () => { + root.classList.toggle('dark', mediaQuery.matches); + }; + + applySystemTheme(); + mediaQuery.addEventListener('change', applySystemTheme); + + // Save theme to localStorage + localStorage.setItem('theme', theme); + + // Cleanup listener + return () => mediaQuery.removeEventListener('change', applySystemTheme); + }, [theme, mounted]); + + return ( + +
+ {children} +
+
+ ); +}; diff --git a/app/globals.css b/app/globals.css index 2772c54..a231f41 100644 --- a/app/globals.css +++ b/app/globals.css @@ -41,6 +41,126 @@ body { --chart-5: 27 87% 67%; } + @tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 0 0% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --hover: 240 27% 11%; + } + + + /* Blue Theme Dark */ + .theme-blue.dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 0 0% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + --hover: 240 27% 11%; + } + + /* Violet Theme Dark */ + .theme-violet.dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 263.4 70% 50.4%; + --primary-foreground: 210 20% 98%; + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 263.4 70% 50.4%; + } + + /* Default dark mode (fallback) */ + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 0 0% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --hover: 240 27% 11%; + } +} .dark { --background: 240 10% 3.9%; --foreground: 0 0% 98%; @@ -77,7 +197,7 @@ body { ::selection { background-color: var(rgb(59 130 246)); } ::marker { color: var(rgb(59 130 246)); } - + ::selection { background: var(--primary); } diff --git a/app/layout.tsx b/app/layout.tsx index ab55a0e..704e5d5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import localFont from "next/font/local"; import "./globals.css"; import { AudioPlayerProvider } from "./components/AudioPlayerContext"; import { NavidromeProvider } from "./components/NavidromeContext"; +import { ThemeProvider } from "./components/ThemeProvider"; import { Metadata } from "next"; import type { Viewport } from 'next'; import Ihateserverside from './components/ihateserverside'; @@ -54,16 +55,38 @@ export default function Layout({ children }: LayoutProps) { return ( - - - - - - - {children} - - - + +