From 5b96cf907ed9217681efee1d95d642d6c9db57a2 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 12 Dec 2025 17:05:22 +0100 Subject: [PATCH] Enhance theming and UI components: Introduce a new dark cyan theme in globals.css, update layout to utilize ThemeProvider for consistent theming, and refactor button and card components to use CSS variables for styling. Improve navigation and alert components with dynamic styles based on theme variables, ensuring a cohesive user experience across the application. --- app/globals.css | 148 ++++++++++++++++++++++++- app/layout.tsx | 10 +- components/Avatar.tsx | 75 +++++++++++++ components/layout/HeroSection.tsx | 16 ++- components/navigation/Navigation.tsx | 155 +++++++++++++++++++++++---- components/ui/Alert.tsx | 32 +++++- components/ui/Avatar.tsx | 9 +- components/ui/BackgroundSection.tsx | 11 +- components/ui/Badge.tsx | 39 ++++++- components/ui/Button.tsx | 13 +-- components/ui/Card.tsx | 16 ++- components/ui/Input.tsx | 13 ++- components/ui/Modal.tsx | 11 +- components/ui/ProgressBar.tsx | 76 ++++++++----- components/ui/SectionTitle.tsx | 23 ++-- components/ui/StarRating.tsx | 19 +++- components/ui/Textarea.tsx | 13 ++- components/ui/ThemeToggle.tsx | 31 ++++++ components/ui/index.ts | 1 + contexts/ThemeContext.tsx | 66 ++++++++++++ 20 files changed, 684 insertions(+), 93 deletions(-) create mode 100644 components/Avatar.tsx create mode 100644 components/ui/ThemeToggle.tsx create mode 100644 contexts/ThemeContext.tsx diff --git a/app/globals.css b/app/globals.css index a03c732..4633996 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,9 +2,107 @@ @tailwind components; @tailwind utilities; +:root { + /* Font variables - will be overridden by Next.js Font */ + --font-orbitron: "Orbitron", sans-serif; + --font-rajdhani: "Rajdhani", sans-serif; + + /* Dark cyan theme (default) */ + --background: #001d2e; + --foreground: #ffffff; + --card: rgba(0, 29, 46, 0.6); + --card-hover: rgba(0, 29, 46, 0.8); + --card-column: rgba(0, 29, 46, 0.8); + --border: rgba(29, 254, 228, 0.3); + --input: rgba(0, 29, 46, 0.6); + --primary: #1dfee4; + --primary-foreground: #001d2e; + --muted: #9ca3af; + --muted-foreground: #9ca3af; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + --accent: #1dfee4; + --destructive: #ef4444; + --success: #10b981; + --purple: #8b5cf6; + --yellow: #eab308; + --green: #10b981; + --blue: #3b82f6; + --pixel-gold: #1dfee4; + --accent-color: #1dfee4; +} + +.dark { + /* Dark gold theme (original) */ + --background: #000000; + --foreground: #ffffff; + --card: rgba(0, 0, 0, 0.6); + --card-hover: rgba(0, 0, 0, 0.8); + --card-column: rgba(0, 0, 0, 0.8); + --border: rgba(218, 165, 32, 0.3); + --input: rgba(0, 0, 0, 0.6); + --primary: #06b6d4; + --primary-foreground: #ffffff; + --muted: #9ca3af; + --muted-foreground: #9ca3af; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + --accent: #ff8c00; + --destructive: #ef4444; + --success: #10b981; + --purple: #8b5cf6; + --yellow: #eab308; + --green: #10b981; + --blue: #3b82f6; + --pixel-gold: #daa520; + --accent-color: #daa520; +} + +.dark-cyan { + /* Dark cyan theme (new) */ + --background: #001d2e; + --foreground: #ffffff; + --card: rgba(0, 29, 46, 0.6); + --card-hover: rgba(0, 29, 46, 0.8); + --card-column: rgba(0, 29, 46, 0.8); + --border: rgba(29, 254, 228, 0.3); + --input: rgba(0, 29, 46, 0.6); + --primary: #1dfee4; + --primary-foreground: #001d2e; + --muted: #9ca3af; + --muted-foreground: #9ca3af; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + --accent: #1dfee4; + --destructive: #ef4444; + --success: #10b981; + --purple: #8b5cf6; + --yellow: #eab308; + --green: #10b981; + --blue: #3b82f6; + --pixel-gold: #1dfee4; + --accent-color: #1dfee4; +} + @layer base { body { - @apply bg-black text-white; + background-color: var(--background); + color: var(--foreground); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", @@ -44,4 +142,52 @@ .animate-shimmer { animation: shimmer 2s infinite; } + + /* Button hover states using CSS variables */ + .btn-primary { + border-color: color-mix(in srgb, var(--accent-color) 50%, transparent); + background-color: color-mix(in srgb, var(--background) 60%, transparent); + color: var(--foreground); + } + .btn-primary:hover:not(:disabled) { + border-color: var(--accent-color); + background-color: color-mix(in srgb, var(--accent-color) 10%, transparent); + } + + .btn-secondary { + border-color: rgba(107, 114, 128, 0.5); + background-color: rgba(31, 41, 55, 0.2); + color: var(--gray-400); + } + .btn-secondary:hover:not(:disabled) { + border-color: var(--gray-500); + background-color: rgba(31, 41, 55, 0.3); + } + + .btn-success { + border-color: rgba(16, 185, 129, 0.5); + background-color: rgba(16, 185, 129, 0.2); + color: var(--success); + } + .btn-success:hover:not(:disabled) { + background-color: rgba(16, 185, 129, 0.3); + } + + .btn-danger { + border-color: rgba(239, 68, 68, 0.5); + background-color: rgba(239, 68, 68, 0.2); + color: var(--destructive); + } + .btn-danger:hover:not(:disabled) { + background-color: rgba(239, 68, 68, 0.3); + } + + .btn-ghost { + border-color: transparent; + background-color: transparent; + color: var(--foreground); + } + .btn-ghost:hover:not(:disabled) { + color: var(--accent-color); + } } diff --git a/app/layout.tsx b/app/layout.tsx index d88dd2a..689447c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { Orbitron, Rajdhani } from "next/font/google"; import "./globals.css"; import SessionProvider from "@/components/layout/SessionProvider"; +import { ThemeProvider } from "@/contexts/ThemeContext"; const orbitron = Orbitron({ subsets: ["latin"], @@ -27,9 +28,14 @@ export default function RootLayout({ children: ReactNode; }>) { return ( - + - {children} + + {children} + ); diff --git a/components/Avatar.tsx b/components/Avatar.tsx new file mode 100644 index 0000000..520f0a1 --- /dev/null +++ b/components/Avatar.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { normalizeAvatarUrl } from "@/lib/avatars"; + +interface AvatarProps { + src: string | null | undefined; + username: string; + size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl"; + className?: string; + borderClassName?: string; + fallbackText?: string; +} + +const sizeClasses = { + xs: "w-6 h-6 text-[8px]", + sm: "w-8 h-8 text-[10px]", + md: "w-10 h-10 text-xs", + lg: "w-16 h-16 sm:w-20 sm:h-20 text-xl sm:text-2xl", + xl: "w-24 h-24 text-4xl", + "2xl": "w-32 h-32 text-4xl", +}; + +export default function Avatar({ + src, + username, + size = "md", + className = "", + borderClassName = "", + fallbackText, +}: AvatarProps) { + const [avatarError, setAvatarError] = useState(false); + const prevSrcRef = useRef(undefined); + + // Reset error state when src changes + useEffect(() => { + if (src !== prevSrcRef.current) { + prevSrcRef.current = src; + // Reset error when src changes to allow retry + // eslint-disable-next-line react-hooks/set-state-in-effect + setAvatarError(false); + } + }, [src]); + + const sizeClass = sizeClasses[size]; + const normalizedSrc = normalizeAvatarUrl(src); + const displaySrc = normalizedSrc && !avatarError ? normalizedSrc : null; + const initial = fallbackText || username.charAt(0).toUpperCase(); + + return ( +
+ {displaySrc ? ( + {username} setAvatarError(true)} + /> + ) : null} + + {initial} + +
+ ); +} diff --git a/components/layout/HeroSection.tsx b/components/layout/HeroSection.tsx index b400796..fd58274 100644 --- a/components/layout/HeroSection.tsx +++ b/components/layout/HeroSection.tsx @@ -15,15 +15,15 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {

GAME.OF.TECH @@ -32,14 +32,20 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {

{/* Subtitle */} -
+
Peaksys
{/* Description */} -

+

Transformez votre apprentissage en aventure. Participez aux ateliers, défiez-vous sur les katas, partagez vos connaissances lors des présentations et progressez dans votre parcours tech. Gagnez de diff --git a/components/navigation/Navigation.tsx b/components/navigation/Navigation.tsx index 9a8d0de..323ff0c 100644 --- a/components/navigation/Navigation.tsx +++ b/components/navigation/Navigation.tsx @@ -5,7 +5,7 @@ import { useSession, signOut } from "next-auth/react"; import { useState } from "react"; import { usePathname } from "next/navigation"; import PlayerStats from "@/components/profile/PlayerStats"; -import { Button } from "@/components/ui"; +import { Button, ThemeToggle } from "@/components/ui"; interface UserData { username: string; @@ -42,17 +42,30 @@ export default function Navigation({ const isAdmin = initialIsAdmin ?? session?.user?.role === "ADMIN"; return ( -