implementazione del Dark Theme
This commit is contained in:
parent
7baf562a45
commit
13dec4e945
8 changed files with 173 additions and 41 deletions
16
index.html
16
index.html
|
|
@ -1,10 +1,22 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>esameits24-26frontend</title>
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<title>Academy Aziendale</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem("academy-theme");
|
||||||
|
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
if (theme === "dark" || (theme !== "light" && theme !== "dark" && prefersDark)) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
import GuestRoute from "@/components/GuestRoute";
|
import GuestRoute from "@/components/GuestRoute";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||||
|
|
@ -22,6 +23,7 @@ import Statistiche from "@/pages/Statistiche";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem storageKey="academy-theme">
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
@ -84,5 +86,6 @@ export default function App() {
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { NavLink, useNavigate } from "react-router-dom";
|
import { NavLink, useNavigate } from "react-router-dom";
|
||||||
import { LogOut } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import { getNavItemsForRole } from "@/config/navigation";
|
import { getNavItemsForRole } from "@/config/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -27,9 +28,14 @@ export default function AppSidebar({ onNavigate }: AppSidebarProps) {
|
||||||
return (
|
return (
|
||||||
<aside className="flex min-h-svh w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground">
|
<aside className="flex min-h-svh w-64 shrink-0 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground">
|
||||||
<div className="border-b px-4 py-4">
|
<div className="border-b px-4 py-4">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
<p className="text-sm font-semibold">Academy Aziendale</p>
|
<p className="text-sm font-semibold">Academy Aziendale</p>
|
||||||
<p className="text-xs text-muted-foreground">Gestione formazione</p>
|
<p className="text-xs text-muted-foreground">Gestione formazione</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ThemeToggle compact />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav className="flex flex-1 flex-col gap-1 p-3">
|
<nav className="flex flex-1 flex-col gap-1 p-3">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ import { useAuth } from "../context/AuthContext";
|
||||||
export default function GuestRoute({ children }: { children: React.ReactNode }) {
|
export default function GuestRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
if (loading) return <p className="loading">Caricamento...</p>;
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<p className="flex min-h-svh items-center justify-center text-sm text-muted-foreground">
|
||||||
|
Caricamento...
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (user) return <Navigate to="/dashboard" replace />;
|
if (user) return <Navigate to="/dashboard" replace />;
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|
|
||||||
9
src/components/ThemeProvider.tsx
Normal file
9
src/components/ThemeProvider.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
}
|
||||||
75
src/components/ThemeToggle.tsx
Normal file
75
src/components/ThemeToggle.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
interface ThemeToggleProps {
|
||||||
|
className?: string;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemeToggle({ className, compact = false }: ThemeToggleProps) {
|
||||||
|
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size={compact ? "icon-sm" : "sm"}
|
||||||
|
className={className}
|
||||||
|
disabled
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon =
|
||||||
|
theme === "system" ? (
|
||||||
|
<Monitor className="size-4" />
|
||||||
|
) : resolvedTheme === "dark" ? (
|
||||||
|
<Moon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Sun className="size-4" />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size={compact ? "icon-sm" : "sm"}
|
||||||
|
className={className}
|
||||||
|
aria-label="Cambia tema"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{!compact && <span className="sr-only">Cambia tema</span>}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
|
<Sun className="size-4" />
|
||||||
|
Chiaro
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
|
<Moon className="size-4" />
|
||||||
|
Scuro
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
|
<Monitor className="size-4" />
|
||||||
|
Sistema
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -64,8 +64,9 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
color-scheme: light;
|
||||||
--foreground: oklch(0.145 0 0);
|
--background: oklch(0.99 0.002 265);
|
||||||
|
--foreground: oklch(0.2 0.01 265);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
|
|
@ -99,37 +100,53 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
color-scheme: dark;
|
||||||
--foreground: oklch(0.985 0 0);
|
|
||||||
--card: oklch(0.205 0 0);
|
/* Sfondo ~#121212, testo ~#E0E0E0 — WCAG AA, no nero/bianco puri */
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--background: oklch(0.184 0.008 265);
|
||||||
--popover: oklch(0.205 0 0);
|
--foreground: oklch(0.907 0.005 265);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
|
||||||
--primary: oklch(0.922 0 0);
|
--card: oklch(0.22 0.008 265);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--card-foreground: oklch(0.907 0.005 265);
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--popover: oklch(0.24 0.008 265);
|
||||||
--muted: oklch(0.269 0 0);
|
--popover-foreground: oklch(0.907 0.005 265);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
|
||||||
--accent: oklch(0.269 0 0);
|
/* Accento desaturato (evita blu saturo su sfondo scuro) */
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--primary: oklch(0.72 0.07 250);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--primary-foreground: oklch(0.17 0.01 265);
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
--secondary: oklch(0.28 0.01 265);
|
||||||
--ring: oklch(0.556 0 0);
|
--secondary-foreground: oklch(0.88 0.005 265);
|
||||||
--chart-1: oklch(0.87 0 0);
|
|
||||||
--chart-2: oklch(0.556 0 0);
|
--muted: oklch(0.26 0.01 265);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--muted-foreground: oklch(0.72 0.01 265);
|
||||||
--chart-4: oklch(0.371 0 0);
|
|
||||||
--chart-5: oklch(0.269 0 0);
|
--accent: oklch(0.3 0.02 250);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.9 0.005 265);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
/* Rosso desaturato per errori/destructive */
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--destructive: oklch(0.72 0.1 22);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
/* Bordi/input con contrasto ≥3:1 rispetto allo sfondo */
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--border: oklch(0.38 0.01 265);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--input: oklch(0.32 0.01 265);
|
||||||
|
--ring: oklch(0.65 0.06 250);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.72 0.07 250);
|
||||||
|
--chart-2: oklch(0.68 0.08 180);
|
||||||
|
--chart-3: oklch(0.72 0.1 22);
|
||||||
|
--chart-4: oklch(0.75 0.08 85);
|
||||||
|
--chart-5: oklch(0.7 0.06 300);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.17 0.008 265);
|
||||||
|
--sidebar-foreground: oklch(0.9 0.005 265);
|
||||||
|
--sidebar-primary: oklch(0.32 0.04 250);
|
||||||
|
--sidebar-primary-foreground: oklch(0.92 0.005 265);
|
||||||
|
--sidebar-accent: oklch(0.26 0.015 250);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9 0.005 265);
|
||||||
|
--sidebar-border: oklch(0.36 0.01 265);
|
||||||
|
--sidebar-ring: oklch(0.65 0.06 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { CircleAlert } from "lucide-react";
|
import { CircleAlert } from "lucide-react";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -37,7 +38,10 @@ export default function Login() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh items-center justify-center bg-muted/40 p-4">
|
<div className="relative flex min-h-svh items-center justify-center bg-muted/40 p-4">
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Academy Aziendale</CardTitle>
|
<CardTitle className="text-xl">Academy Aziendale</CardTitle>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue