Implement better theme and refactored pages
All checks were successful
release-image / release-image (push) Successful in 10m21s
All checks were successful
release-image / release-image (push) Successful in 10m21s
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -17,8 +17,10 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4991,6 +4993,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-themes": {
|
||||||
|
"version": "0.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||||
|
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@@ -5798,6 +5809,15 @@
|
|||||||
"is-arrayish": "^0.3.1"
|
"is-arrayish": "^0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -19,8 +19,10 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -45,71 +45,81 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--background: oklch(1 0 0);
|
/* Light Mode Palette */
|
||||||
--foreground: oklch(0.145 0 0);
|
--background: oklch(0.99 0 0); /* Very light off-white */
|
||||||
--card: oklch(1 0 0);
|
--foreground: oklch(0.1 0 0); /* Very dark gray */
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card: oklch(0.99 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--card-foreground: oklch(0.1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover: oklch(0.99 0 0);
|
||||||
--primary: oklch(0.205 0 0);
|
--popover-foreground: oklch(0.1 0 0);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary: oklch(0.45 0.15 260); /* Deep indigo/blue */
|
||||||
--secondary: oklch(0.97 0 0);
|
--primary-foreground: oklch(0.98 0 0); /* Very light off-white for text on primary */
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary: oklch(0.94 0 0); /* Light gray */
|
||||||
--muted: oklch(0.97 0 0);
|
--secondary-foreground: oklch(0.3 0 0); /* Medium-dark gray */
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted: oklch(0.94 0 0);
|
||||||
--accent: oklch(0.97 0 0);
|
--muted-foreground: oklch(0.5 0 0); /* Medium gray */
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent: oklch(0.94 0.03 260); /* Subtle hint of primary color */
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--accent-foreground: oklch(0.3 0.05 260); /* Darker desaturated primary */
|
||||||
--border: oklch(0.922 0 0);
|
--destructive: oklch(0.6 0.15 20); /* Standard red */
|
||||||
--input: oklch(0.922 0 0);
|
--border: oklch(0.85 0 0); /* Medium-light gray */
|
||||||
--ring: oklch(0.708 0 0);
|
--input: oklch(0.9 0 0); /* Slightly lighter than border */
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--ring: oklch(0.7 0 0); /* Medium gray, for focus rings */
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
/* Chart Colors (Light Mode) - Distinct and harmonious */
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--chart-1: oklch(0.55 0.18 270); /* Deep purple-blue */
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-2: oklch(0.65 0.15 180); /* Teal */
|
||||||
--sidebar: oklch(0.985 0 0);
|
--chart-3: oklch(0.7 0.12 90); /* Muted green-yellow */
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--chart-4: oklch(0.6 0.16 30); /* Orange-brown */
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--chart-5: oklch(0.75 0.1 330); /* Muted pink */
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
/* Sidebar Colors (Light Mode) */
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar: oklch(0.97 0 0); /* Slightly darker background for sidebar */
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-foreground: oklch(0.145 0 0); /* Standard foreground */
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-primary: oklch(0.45 0.15 260); /* Same as primary */
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-accent: oklch(0.94 0.03 260);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3 0.05 260);
|
||||||
|
--sidebar-border: oklch(0.85 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
/* Dark Mode Palette */
|
||||||
--foreground: oklch(0.985 0 0);
|
--background: oklch(0.12 0 0); /* Very dark charcoal */
|
||||||
--card: oklch(0.205 0 0);
|
--foreground: oklch(0.92 0 0); /* Light gray */
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card: oklch(0.18 0 0); /* Slightly lighter dark for cards */
|
||||||
--popover: oklch(0.205 0 0);
|
--card-foreground: oklch(0.92 0 0);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover: oklch(0.18 0 0);
|
||||||
--primary: oklch(0.922 0 0);
|
--popover-foreground: oklch(0.92 0 0);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary: oklch(0.7 0.15 260); /* Brighter, more vibrant indigo for dark mode */
|
||||||
--secondary: oklch(0.269 0 0);
|
--primary-foreground: oklch(0.15 0 0); /* Dark gray for text on primary */
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary: oklch(0.22 0 0); /* Darker gray */
|
||||||
--muted: oklch(0.269 0 0);
|
--secondary-foreground: oklch(0.85 0 0); /* Lighter gray */
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted: oklch(0.22 0 0);
|
||||||
--accent: oklch(0.269 0 0);
|
--muted-foreground: oklch(0.6 0 0); /* Mid-gray */
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent: oklch(0.22 0.03 260); /* Subtle dark hint of primary */
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--accent-foreground: oklch(0.8 0.05 260); /* Lighter desaturated primary */
|
||||||
--border: oklch(1 0 0 / 10%);
|
--destructive: oklch(0.7 0.15 20); /* Brighter red for dark mode */
|
||||||
--input: oklch(1 0 0 / 15%);
|
--border: oklch(0.25 0 0); /* Medium dark gray */
|
||||||
--ring: oklch(0.556 0 0);
|
--input: oklch(0.2 0 0); /* Darker than border */
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--ring: oklch(0.4 0 0); /* Mid-dark gray */
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
/* Chart Colors (Dark Mode) - Brighter and distinct */
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-1: oklch(0.7 0.18 270); /* Brighter purple-blue */
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-2: oklch(0.75 0.15 180); /* Brighter teal */
|
||||||
--sidebar: oklch(0.205 0 0);
|
--chart-3: oklch(0.8 0.12 90); /* Brighter muted green-yellow */
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--chart-4: oklch(0.75 0.16 30); /* Brighter orange-brown */
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--chart-5: oklch(0.85 0.1 330); /* Brighter muted pink */
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
/* Sidebar Colors (Dark Mode) */
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar: oklch(0.18 0 0); /* Slightly lighter dark for sidebar */
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-foreground: oklch(0.92 0 0);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-primary: oklch(0.7 0.15 260);
|
||||||
|
--sidebar-primary-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-accent: oklch(0.22 0.03 260);
|
||||||
|
--sidebar-accent-foreground: oklch(0.8 0.05 260);
|
||||||
|
--sidebar-border: oklch(0.25 0 0);
|
||||||
|
--sidebar-ring: oklch(0.4 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -119,4 +129,4 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
// app/layout.tsx
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
||||||
|
import { AuthProvider } from '@/context/AuthContext';
|
||||||
|
import { ThemeToggle } from '@/components/ThemeToggle'; // Import ThemeToggle
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -23,12 +28,27 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<AuthProvider>
|
||||||
|
{/* ThemeToggle placed in a fixed position */}
|
||||||
|
<div className="top-4 right-4 z-50"> {/* Add these classes for positioning */}
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</AuthProvider>
|
||||||
|
<Toaster />
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,51 @@
|
|||||||
|
// app/nodes/[clientId]/page.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router'; // For Pages Router
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import IndividualClientPage from '@/components/IndividualClientPage';
|
import IndividualClientPage from '@/components/IndividualClientPage';
|
||||||
import LoginPage from '@/components/LoginPage';
|
import LoginPage from '@/components/LoginPage';
|
||||||
import { UserRoles } from '@/types';
|
import { UserRoles } from '@/types';
|
||||||
import {Button} from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
const ClientDetailPage: React.FC = () => {
|
const ClientDetailPage: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { clientId } = router.query;
|
const params = useParams(); // params can be null or an object with string | string[] values
|
||||||
const { user, loading, token, hasPermission, logout } = useAuth(); // Call useAuth once here
|
|
||||||
|
const { user, loading, token, hasPermission, logout } = useAuth();
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">Loading Authentication...</div>;
|
return <div className="flex items-center justify-center min-h-screen bg-background text-foreground">Loading Authentication...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user || !token || !hasPermission(UserRoles.MOD)) {
|
// Safely extract clientId, handling the case where params might be null or clientId might be undefined
|
||||||
// Redirect to login or show access denied if not authenticated or authorized
|
const clientId = params?.clientId; // Use optional chaining
|
||||||
|
|
||||||
|
if (!clientId) {
|
||||||
|
// This covers cases where params is null, or clientId property is missing/undefined
|
||||||
|
// or if the URL param isn't properly captured.
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans">
|
<div className="flex items-center justify-center min-h-screen bg-background text-foreground">
|
||||||
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-800 shadow-md">
|
Client ID not found in URL.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure clientIdentifier is a string (use the first element if it's an array)
|
||||||
|
const clientIdentifier = Array.isArray(clientId) ? clientId[0] : clientId;
|
||||||
|
|
||||||
|
if (!user || !token || !hasPermission(UserRoles.MOD)) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground font-sans">
|
||||||
|
<header className="flex justify-between items-center p-4 bg-card text-card-foreground shadow-md">
|
||||||
<h1 className="text-xl font-bold">Radio App Admin</h1>
|
<h1 className="text-xl font-bold">Radio App Admin</h1>
|
||||||
</header>
|
</header>
|
||||||
<main className="p-6">
|
<main className="p-6">
|
||||||
{!user ? (
|
{!user ? (
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-red-500 text-lg">
|
<div className="text-center text-destructive text-lg">
|
||||||
You do not have permission to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
|
You do not have permission to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -37,21 +54,14 @@ const ClientDetailPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure clientId is a string before passing
|
|
||||||
const clientIdentifier = Array.isArray(clientId) ? clientId[0] : clientId;
|
|
||||||
|
|
||||||
if (!clientIdentifier) {
|
|
||||||
return <div className="text-center text-red-500 text-lg mt-10">Client ID not found in URL.</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans">
|
<div className="min-h-screen bg-background text-foreground font-sans">
|
||||||
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-800 shadow-md">
|
<header className="flex justify-between items-center p-4 bg-card text-card-foreground shadow-md">
|
||||||
<h1 className="text-xl font-bold">Radio App Admin</h1>
|
<h1 className="text-xl font-bold">Radio App Admin</h1>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-sm">Logged in as: {user.username} ({user.role})</span>
|
<span className="text-sm text-muted-foreground">Logged in as: {user.username} ({user.role})</span>
|
||||||
<Button onClick={() => router.push('/')} variant="outline">Back to Management</Button>
|
<Button onClick={() => router.push('/')} variant="outline">Back to Management</Button>
|
||||||
<Button onClick={logout} variant="outline">Logout</Button> {/* Use the destructured logout */}
|
<Button onClick={logout} variant="outline">Logout</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="p-6">
|
<main className="p-6">
|
||||||
@@ -9,6 +9,7 @@ import { useAuth } from '@/context/AuthContext';
|
|||||||
// You'll need to import authAwareFetch directly or ensure your useAuth hook provides it for registration if not handling it internally.
|
// You'll need to import authAwareFetch directly or ensure your useAuth hook provides it for registration if not handling it internally.
|
||||||
// For this example, we'll assume direct import as it's a separate utility.
|
// For this example, we'll assume direct import as it's a separate utility.
|
||||||
import { authAwareFetch } from '@/utils/AuthAwareFetch'; // Import authAwareFetch
|
import { authAwareFetch } from '@/utils/AuthAwareFetch'; // Import authAwareFetch
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
const LoginPage: React.FC = () => {
|
||||||
@@ -35,7 +36,7 @@ const LoginPage: React.FC = () => {
|
|||||||
const handleRegister = async (): Promise<void> => {
|
const handleRegister = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const response = await authAwareFetch(
|
const response = await authAwareFetch(
|
||||||
'/auth/register_user', // The registration endpoint
|
'/auth/register',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -48,7 +49,9 @@ const LoginPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert('Registration successful! You can now log in.');
|
toast("Registration Successful!", {
|
||||||
|
description: "You can now log in with your new account.",
|
||||||
|
});
|
||||||
setIsRegistering(false); // Switch back to login form
|
setIsRegistering(false); // Switch back to login form
|
||||||
setUsername('');
|
setUsername('');
|
||||||
setPassword('');
|
setPassword('');
|
||||||
|
|||||||
113
src/components/ThemeProvider.tsx
Normal file
113
src/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// components/ThemeProvider.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
// === CORRECTED LINE HERE ===
|
||||||
|
type Theme = "light" | "dark" | "system";
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
defaultTheme?: Theme; // This can now correctly be "system"
|
||||||
|
storageKey?: string;
|
||||||
|
attribute?: string; // Add this prop for applying class to html
|
||||||
|
enableSystem?: boolean; // Add this prop to enable system theme detection
|
||||||
|
disableTransitionOnChange?: boolean; // Add this prop for transitions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
setTheme: (theme: Theme) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
attribute = "class", // Default to 'class' as per next-themes pattern
|
||||||
|
enableSystem = true, // Default to true for system theme detection
|
||||||
|
disableTransitionOnChange = false, // Default to false
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return defaultTheme; // For SSR, return default
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedTheme = localStorage.getItem(storageKey);
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme as Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If defaultTheme is "system", check system preference
|
||||||
|
if (defaultTheme === "system") {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultTheme;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
// Apply the theme class
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
|
|
||||||
|
if (theme === "system" && enableSystem) {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
root.classList.add(systemTheme);
|
||||||
|
} else {
|
||||||
|
root.classList.add(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle transition disabling (optional)
|
||||||
|
if (disableTransitionOnChange) {
|
||||||
|
root.classList.add('no-transition'); // Add a class to disable transitions
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
root.classList.remove('no-transition');
|
||||||
|
}, 10); // Small delay to allow class to be applied before removal
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [theme, enableSystem, disableTransitionOnChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Listen for system theme changes if defaultTheme is 'system' and system is enabled
|
||||||
|
if (enableSystem) {
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
// Only update if the current theme is "system"
|
||||||
|
if (theme === "system") {
|
||||||
|
setThemeState(e.matches ? "dark" : "light");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}
|
||||||
|
}, [enableSystem, theme]);
|
||||||
|
|
||||||
|
|
||||||
|
const setTheme = (newTheme: Theme) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(storageKey, newTheme);
|
||||||
|
}
|
||||||
|
setThemeState(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme }} {...props}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
22
src/components/ThemeToggle.tsx
Normal file
22
src/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTheme } from "@/components/ThemeProvider"; // Adjust path as needed
|
||||||
|
import { Button } from "@/components/ui/button"; // Assuming you have a Shadcn UI Button component
|
||||||
|
import { Sun, Moon } from "lucide-react"; // Assuming you have lucide-react for icons
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost" // Use your preferred button variant
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||||
|
>
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// pages/_app.tsx
|
|
||||||
import type { AppProps } from 'next/app';
|
|
||||||
import { AuthProvider } from '@/context/AuthContext';
|
|
||||||
import '@/app/globals.css'; // Assuming your global styles are here
|
|
||||||
|
|
||||||
function DRB({ Component, pageProps }: AppProps) {
|
|
||||||
return (
|
|
||||||
<AuthProvider>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</AuthProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DRB;
|
|
||||||
Reference in New Issue
Block a user