Implement theme provider and settings page for customizable appearance
This commit is contained in:
81
app/components/ThemeProvider.tsx
Normal file
81
app/components/ThemeProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
122
app/globals.css
122
app/globals.css
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user