Implement theme provider and settings page for customizable appearance

This commit is contained in:
2025-06-19 03:57:19 +00:00
committed by GitHub
parent 6f3cf5e579
commit d910ff1a93
4 changed files with 306 additions and 22 deletions

View File

@@ -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<ThemeContextType | undefined>(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<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('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 (
<ThemeContext.Provider
value={{
theme,
setTheme,
}}
>
<div className={`theme-${theme}`}>
{children}
</div>
</ThemeContext.Provider>
);
};

View File

@@ -41,6 +41,126 @@ body {
--chart-5: 27 87% 67%; --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 { .dark {
--background: 240 10% 3.9%; --background: 240 10% 3.9%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
@@ -77,7 +197,7 @@ body {
::selection { background-color: var(rgb(59 130 246)); } ::selection { background-color: var(rgb(59 130 246)); }
::marker { color: var(rgb(59 130 246)); } ::marker { color: var(rgb(59 130 246)); }
::selection { ::selection {
background: var(--primary); background: var(--primary);
} }

View File

@@ -5,6 +5,7 @@ import localFont from "next/font/local";
import "./globals.css"; import "./globals.css";
import { AudioPlayerProvider } from "./components/AudioPlayerContext"; import { AudioPlayerProvider } from "./components/AudioPlayerContext";
import { NavidromeProvider } from "./components/NavidromeContext"; import { NavidromeProvider } from "./components/NavidromeContext";
import { ThemeProvider } from "./components/ThemeProvider";
import { Metadata } from "next"; import { Metadata } from "next";
import type { Viewport } from 'next'; import type { Viewport } from 'next';
import Ihateserverside from './components/ihateserverside'; import Ihateserverside from './components/ihateserverside';
@@ -54,16 +55,38 @@ export default function Layout({ children }: LayoutProps) {
return ( return (
<html lang="en"> <html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiase dark bg-background`}> <head>
<NavidromeProvider> <script
<AudioPlayerProvider> dangerouslySetInnerHTML={{
<SpeedInsights /> __html: `
<Analytics /> (function() {
<Ihateserverside> const savedTheme = localStorage.getItem('theme');
{children} const theme = (savedTheme === 'blue' || savedTheme === 'violet') ? savedTheme : 'blue';
</Ihateserverside>
</AudioPlayerProvider> // Apply theme class
</NavidromeProvider> document.documentElement.classList.add('theme-' + theme);
// Apply dark mode based on system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
})();
`,
}}
/>
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background`}>
<ThemeProvider>
<NavidromeProvider>
<AudioPlayerProvider>
<SpeedInsights />
<Analytics />
<Ihateserverside>
{children}
</Ihateserverside>
</AudioPlayerProvider>
</NavidromeProvider>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@@ -1,20 +1,80 @@
'use client';
import React from 'react'; import React from 'react';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useTheme } from '@/app/components/ThemeProvider';
const SettingsPage = () => { const SettingsPage = () => {
const { theme, setTheme } = useTheme();
return ( return (
<div className="container mx-auto p-4"> <div className="container mx-auto p-6 max-w-2xl">
<Label>Theme</Label> <div className="space-y-6">
<Select> <div>
<SelectTrigger> <h1 className="text-3xl font-semibold tracking-tight">Settings</h1>
<SelectValue>Light</SelectValue> <p className="text-muted-foreground">Customize your music experience</p>
</SelectTrigger> </div>
<SelectContent>
<SelectItem value="light">Light</SelectItem> <Card>
<SelectItem value="dark">Dark</SelectItem> <CardHeader>
</SelectContent> <CardTitle>Appearance</CardTitle>
</Select> <CardDescription>
Customize how the application looks and feels
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="theme-select">Color Theme</Label>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger id="theme-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="blue">Blue</SelectItem>
<SelectItem value="violet">Violet</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-sm text-muted-foreground">
<p><strong>Theme:</strong> Choose between blue and violet color schemes</p>
<p><strong>Dark Mode:</strong> Automatically follows your system preferences</p>
</div>
</CardContent>
</Card>
{/* Theme Preview */}
<Card>
<CardHeader>
<CardTitle>Preview</CardTitle>
<CardDescription>
See how your theme looks
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center space-x-4 p-4 border rounded-lg">
<div className="w-12 h-12 bg-primary rounded-full flex items-center justify-center">
<span className="text-primary-foreground font-semibold"></span>
</div>
<div className="flex-1">
<p className="font-semibold">Sample Song Title</p>
<p className="text-sm text-muted-foreground">Sample Artist</p>
</div>
<div className="text-sm text-muted-foreground">3:42</div>
</div>
<div className="flex space-x-2">
<div className="h-8 w-16 bg-secondary rounded"></div>
<div className="h-8 w-16 bg-accent rounded"></div>
<div className="h-8 w-16 bg-muted rounded"></div>
</div>
</div>
</CardContent>
</Card>
</div>
</div> </div>
); );
}; };