diff --git a/package-lock.json b/package-lock.json index fbde172..2342502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 04669fb..655cdfd 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app/globals.css b/src/app/globals.css index dc98be7..64ed405 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; } -} +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..4194305 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - + - {children} + + + {/* ThemeToggle placed in a fixed position */} +
{/* Add these classes for positioning */} + +
+ + {children} +
+ +
); -} +} \ No newline at end of file diff --git a/src/pages/nodes/[clientId].tsx b/src/app/nodes/[clientId]/page.tsx similarity index 55% rename from src/pages/nodes/[clientId].tsx rename to src/app/nodes/[clientId]/page.tsx index e74b440..1df2208 100644 --- a/src/pages/nodes/[clientId].tsx +++ b/src/app/nodes/[clientId]/page.tsx @@ -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
Loading Authentication...
; + return
Loading Authentication...
; } - 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 ( -
-
+
+ Client ID not found in URL. +
+ ); + } + + // 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 ( +
+

Radio App Admin

{!user ? ( ) : ( -
+
You do not have permission to view this page. Your role: {user.role}. Required: {UserRoles.MOD}.
)} @@ -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
Client ID not found in URL.
; - } - return ( -
-
+
+

Radio App Admin

- Logged in as: {user.username} ({user.role}) + Logged in as: {user.username} ({user.role}) - {/* Use the destructured logout */} +
diff --git a/src/components/LoginPage.tsx b/src/components/LoginPage.tsx index f805a6b..785e73a 100644 --- a/src/components/LoginPage.tsx +++ b/src/components/LoginPage.tsx @@ -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 => { 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(''); diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..2dec67d --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -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(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(() => { + 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 ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} \ No newline at end of file diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..c4d79af --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -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 ( + + ); +} \ No newline at end of file diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..957524e --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -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 ( + + ) +} + +export { Toaster } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx deleted file mode 100644 index a60f2f0..0000000 --- a/src/pages/_app.tsx +++ /dev/null @@ -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 ( - - - - ); -} - -export default DRB; \ No newline at end of file