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",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "15.3.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"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": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@@ -5798,6 +5809,15 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "15.3.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -45,71 +45,81 @@
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
/* Light Mode Palette */
|
||||
--background: oklch(0.99 0 0); /* Very light off-white */
|
||||
--foreground: oklch(0.1 0 0); /* Very dark gray */
|
||||
--card: oklch(0.99 0 0);
|
||||
--card-foreground: oklch(0.1 0 0);
|
||||
--popover: oklch(0.99 0 0);
|
||||
--popover-foreground: oklch(0.1 0 0);
|
||||
--primary: oklch(0.45 0.15 260); /* Deep indigo/blue */
|
||||
--primary-foreground: oklch(0.98 0 0); /* Very light off-white for text on primary */
|
||||
--secondary: oklch(0.94 0 0); /* Light gray */
|
||||
--secondary-foreground: oklch(0.3 0 0); /* Medium-dark gray */
|
||||
--muted: oklch(0.94 0 0);
|
||||
--muted-foreground: oklch(0.5 0 0); /* Medium gray */
|
||||
--accent: oklch(0.94 0.03 260); /* Subtle hint of primary color */
|
||||
--accent-foreground: oklch(0.3 0.05 260); /* Darker desaturated primary */
|
||||
--destructive: oklch(0.6 0.15 20); /* Standard red */
|
||||
--border: oklch(0.85 0 0); /* Medium-light gray */
|
||||
--input: oklch(0.9 0 0); /* Slightly lighter than border */
|
||||
--ring: oklch(0.7 0 0); /* Medium gray, for focus rings */
|
||||
|
||||
/* Chart Colors (Light Mode) - Distinct and harmonious */
|
||||
--chart-1: oklch(0.55 0.18 270); /* Deep purple-blue */
|
||||
--chart-2: oklch(0.65 0.15 180); /* Teal */
|
||||
--chart-3: oklch(0.7 0.12 90); /* Muted green-yellow */
|
||||
--chart-4: oklch(0.6 0.16 30); /* Orange-brown */
|
||||
--chart-5: oklch(0.75 0.1 330); /* Muted pink */
|
||||
|
||||
/* Sidebar Colors (Light Mode) */
|
||||
--sidebar: oklch(0.97 0 0); /* Slightly darker background for sidebar */
|
||||
--sidebar-foreground: oklch(0.145 0 0); /* Standard foreground */
|
||||
--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 {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
/* Dark Mode Palette */
|
||||
--background: oklch(0.12 0 0); /* Very dark charcoal */
|
||||
--foreground: oklch(0.92 0 0); /* Light gray */
|
||||
--card: oklch(0.18 0 0); /* Slightly lighter dark for cards */
|
||||
--card-foreground: oklch(0.92 0 0);
|
||||
--popover: oklch(0.18 0 0);
|
||||
--popover-foreground: oklch(0.92 0 0);
|
||||
--primary: oklch(0.7 0.15 260); /* Brighter, more vibrant indigo for dark mode */
|
||||
--primary-foreground: oklch(0.15 0 0); /* Dark gray for text on primary */
|
||||
--secondary: oklch(0.22 0 0); /* Darker gray */
|
||||
--secondary-foreground: oklch(0.85 0 0); /* Lighter gray */
|
||||
--muted: oklch(0.22 0 0);
|
||||
--muted-foreground: oklch(0.6 0 0); /* Mid-gray */
|
||||
--accent: oklch(0.22 0.03 260); /* Subtle dark hint of primary */
|
||||
--accent-foreground: oklch(0.8 0.05 260); /* Lighter desaturated primary */
|
||||
--destructive: oklch(0.7 0.15 20); /* Brighter red for dark mode */
|
||||
--border: oklch(0.25 0 0); /* Medium dark gray */
|
||||
--input: oklch(0.2 0 0); /* Darker than border */
|
||||
--ring: oklch(0.4 0 0); /* Mid-dark gray */
|
||||
|
||||
/* Chart Colors (Dark Mode) - Brighter and distinct */
|
||||
--chart-1: oklch(0.7 0.18 270); /* Brighter purple-blue */
|
||||
--chart-2: oklch(0.75 0.15 180); /* Brighter teal */
|
||||
--chart-3: oklch(0.8 0.12 90); /* Brighter muted green-yellow */
|
||||
--chart-4: oklch(0.75 0.16 30); /* Brighter orange-brown */
|
||||
--chart-5: oklch(0.85 0.1 330); /* Brighter muted pink */
|
||||
|
||||
/* Sidebar Colors (Dark Mode) */
|
||||
--sidebar: oklch(0.18 0 0); /* Slightly lighter dark for sidebar */
|
||||
--sidebar-foreground: oklch(0.92 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 {
|
||||
@@ -119,4 +129,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
// app/layout.tsx
|
||||
import type { Metadata } from "next";
|
||||
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";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -23,12 +28,27 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
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>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,51 @@
|
||||
// app/nodes/[clientId]/page.tsx
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router'; // For Pages Router
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import IndividualClientPage from '@/components/IndividualClientPage';
|
||||
import LoginPage from '@/components/LoginPage';
|
||||
import { UserRoles } from '@/types';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const ClientDetailPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { clientId } = router.query;
|
||||
const { user, loading, token, hasPermission, logout } = useAuth(); // Call useAuth once here
|
||||
const params = useParams(); // params can be null or an object with string | string[] values
|
||||
|
||||
const { user, loading, token, hasPermission, logout } = useAuth();
|
||||
|
||||
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)) {
|
||||
// Redirect to login or show access denied if not authenticated or authorized
|
||||
// Safely extract clientId, handling the case where params might be null or clientId might be undefined
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans">
|
||||
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-800 shadow-md">
|
||||
<div className="flex items-center justify-center min-h-screen bg-background text-foreground">
|
||||
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>
|
||||
</header>
|
||||
<main className="p-6">
|
||||
{!user ? (
|
||||
<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}.
|
||||
</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 (
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-sans">
|
||||
<header className="flex justify-between items-center p-4 bg-white dark:bg-gray-800 shadow-md">
|
||||
<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>
|
||||
<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={logout} variant="outline">Logout</Button> {/* Use the destructured logout */}
|
||||
<Button onClick={logout} variant="outline">Logout</Button>
|
||||
</div>
|
||||
</header>
|
||||
<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.
|
||||
// For this example, we'll assume direct import as it's a separate utility.
|
||||
import { authAwareFetch } from '@/utils/AuthAwareFetch'; // Import authAwareFetch
|
||||
import { toast } from 'sonner';
|
||||
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
@@ -35,7 +36,7 @@ const LoginPage: React.FC = () => {
|
||||
const handleRegister = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await authAwareFetch(
|
||||
'/auth/register_user', // The registration endpoint
|
||||
'/auth/register',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -48,7 +49,9 @@ const LoginPage: React.FC = () => {
|
||||
);
|
||||
|
||||
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
|
||||
setUsername('');
|
||||
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