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.
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 49s
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 49s
This commit is contained in:
148
app/globals.css
148
app/globals.css
@@ -2,9 +2,107 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-black text-white;
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
@@ -44,4 +142,52 @@
|
|||||||
.animate-shimmer {
|
.animate-shimmer {
|
||||||
animation: shimmer 2s infinite;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
|||||||
import { Orbitron, Rajdhani } from "next/font/google";
|
import { Orbitron, Rajdhani } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import SessionProvider from "@/components/layout/SessionProvider";
|
import SessionProvider from "@/components/layout/SessionProvider";
|
||||||
|
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
const orbitron = Orbitron({
|
const orbitron = Orbitron({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -27,9 +28,14 @@ export default function RootLayout({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="fr" className={`${orbitron.variable} ${rajdhani.variable}`}>
|
<html
|
||||||
|
lang="fr"
|
||||||
|
className={`${orbitron.variable} ${rajdhani.variable} dark-cyan`}
|
||||||
|
>
|
||||||
<body className="antialiased">
|
<body className="antialiased">
|
||||||
<SessionProvider>{children}</SessionProvider>
|
<ThemeProvider>
|
||||||
|
<SessionProvider>{children}</SessionProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
75
components/Avatar.tsx
Normal file
75
components/Avatar.tsx
Normal file
@@ -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<string | null | undefined>(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 (
|
||||||
|
<div
|
||||||
|
className={`${sizeClass} rounded-full border overflow-hidden flex items-center justify-center relative ${className} ${borderClassName}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displaySrc ? (
|
||||||
|
<img
|
||||||
|
key={displaySrc}
|
||||||
|
src={displaySrc}
|
||||||
|
alt={username}
|
||||||
|
className="w-full h-full object-cover absolute inset-0"
|
||||||
|
onError={() => setAvatarError(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span
|
||||||
|
className={`font-bold ${displaySrc ? "hidden" : ""}`}
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
|
>
|
||||||
|
{initial}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,15 +15,15 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
|||||||
<div className="w-full flex justify-center mb-4 overflow-hidden">
|
<div className="w-full flex justify-center mb-4 overflow-hidden">
|
||||||
<h1 className="text-4xl sm:text-5xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight relative break-words">
|
<h1 className="text-4xl sm:text-5xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight relative break-words">
|
||||||
<span
|
<span
|
||||||
className="title-animated inline-block relative z-10"
|
className="title-animated inline-block relative z-10 font-gaming"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(90deg, #daa520 0%, #ffa500 30%, #ff8c00 50%, #ffa500 70%, #daa520 100%)`,
|
backgroundImage: `linear-gradient(90deg, var(--accent-color) 0%, var(--accent) 30%, var(--accent) 50%, var(--accent) 70%, var(--accent-color) 100%)`,
|
||||||
backgroundSize: "200% auto",
|
backgroundSize: "200% auto",
|
||||||
WebkitBackgroundClip: "text",
|
WebkitBackgroundClip: "text",
|
||||||
WebkitTextFillColor: "transparent",
|
WebkitTextFillColor: "transparent",
|
||||||
backgroundClip: "text",
|
backgroundClip: "text",
|
||||||
color: "transparent",
|
color: "transparent",
|
||||||
filter: `drop-shadow(0 0 12px rgba(255, 140, 0, 0.4))`,
|
filter: `drop-shadow(0 0 12px color-mix(in srgb, var(--accent-color) 40%, transparent))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
GAME.OF.TECH
|
GAME.OF.TECH
|
||||||
@@ -32,14 +32,20 @@ export default function HeroSection({ backgroundImage }: HeroSectionProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
<div className="text-pixel-gold text-xl md:text-2xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 mb-8 tracking-wider">
|
<div
|
||||||
|
className="text-xl md:text-2xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 mb-8 tracking-wider"
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
|
>
|
||||||
<span>✦</span>
|
<span>✦</span>
|
||||||
<span>Peaksys</span>
|
<span>Peaksys</span>
|
||||||
<span>✦</span>
|
<span>✦</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-white text-base md:text-lg max-w-3xl mx-auto mb-12 leading-relaxed px-4">
|
<p
|
||||||
|
className="text-base md:text-lg max-w-3xl mx-auto mb-12 leading-relaxed px-4"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
>
|
||||||
Transformez votre apprentissage en aventure. Participez aux ateliers,
|
Transformez votre apprentissage en aventure. Participez aux ateliers,
|
||||||
défiez-vous sur les katas, partagez vos connaissances lors des
|
défiez-vous sur les katas, partagez vos connaissances lors des
|
||||||
présentations et progressez dans votre parcours tech. Gagnez de
|
présentations et progressez dans votre parcours tech. Gagnez de
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useSession, signOut } from "next-auth/react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import PlayerStats from "@/components/profile/PlayerStats";
|
import PlayerStats from "@/components/profile/PlayerStats";
|
||||||
import { Button } from "@/components/ui";
|
import { Button, ThemeToggle } from "@/components/ui";
|
||||||
|
|
||||||
interface UserData {
|
interface UserData {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -42,17 +42,30 @@ export default function Navigation({
|
|||||||
const isAdmin = initialIsAdmin ?? session?.user?.role === "ADMIN";
|
const isAdmin = initialIsAdmin ?? session?.user?.role === "ADMIN";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="w-full fixed top-0 left-0 z-50 px-4 sm:px-8 py-3 bg-black/80 backdrop-blur-sm border-b border-gray-800/30">
|
<nav
|
||||||
|
className="w-full fixed top-0 left-0 z-50 px-4 sm:px-8 py-3 backdrop-blur-sm border-b"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
"color-mix(in srgb, var(--background) 80%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--gray-800) 30%, transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||||
{/* Logo - Left */}
|
{/* Logo - Left */}
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="flex flex-col hover:opacity-80 transition-opacity"
|
className="flex flex-col hover:opacity-80 transition-opacity"
|
||||||
>
|
>
|
||||||
<div className="text-white text-lg sm:text-xl font-gaming font-bold tracking-tight">
|
<div
|
||||||
|
className="text-lg sm:text-xl font-gaming font-bold tracking-tight"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
>
|
||||||
GAME.OF.TECH
|
GAME.OF.TECH
|
||||||
</div>
|
</div>
|
||||||
<div className="text-pixel-gold text-[10px] sm:text-xs font-gaming-subtitle font-semibold flex items-center gap-1 tracking-wide">
|
<div
|
||||||
|
className="text-[10px] sm:text-xs font-gaming-subtitle font-semibold flex items-center gap-1 tracking-wide"
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
|
>
|
||||||
<span>✦</span>
|
<span>✦</span>
|
||||||
<span>Peaksys</span>
|
<span>Peaksys</span>
|
||||||
<span>✦</span>
|
<span>✦</span>
|
||||||
@@ -63,26 +76,54 @@ export default function Navigation({
|
|||||||
<div className="hidden md:flex items-center gap-6">
|
<div className="hidden md:flex items-center gap-6">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
className="transition text-xs font-normal uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
HOME
|
HOME
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/events"
|
href="/events"
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
className="transition text-xs font-normal uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
EVENTS
|
EVENTS
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/leaderboard"
|
href="/leaderboard"
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
className="transition text-xs font-normal uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
LEADERBOARD
|
LEADERBOARD
|
||||||
</Link>
|
</Link>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
className="text-pixel-gold hover:text-orange-400 transition text-xs font-normal uppercase tracking-widest"
|
className="transition text-xs font-normal uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
ADMIN
|
ADMIN
|
||||||
</Link>
|
</Link>
|
||||||
@@ -91,6 +132,11 @@ export default function Navigation({
|
|||||||
|
|
||||||
{/* Right Side */}
|
{/* Right Side */}
|
||||||
<div className="flex items-center gap-2 sm:gap-4">
|
<div className="flex items-center gap-2 sm:gap-4">
|
||||||
|
{/* Theme Toggle */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* PlayerStats - Hidden on mobile */}
|
{/* PlayerStats - Hidden on mobile */}
|
||||||
{isAuthenticated && !isAuthPage && (
|
{isAuthenticated && !isAuthPage && (
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
@@ -113,7 +159,14 @@ export default function Navigation({
|
|||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
className="transition text-xs font-normal uppercase tracking-widest"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
@@ -129,7 +182,14 @@ export default function Navigation({
|
|||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
className="md:hidden text-white hover:text-pixel-gold transition p-2"
|
className="md:hidden transition p-2"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -153,28 +213,61 @@ export default function Navigation({
|
|||||||
|
|
||||||
{/* Mobile Menu */}
|
{/* Mobile Menu */}
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<div className="md:hidden absolute top-full left-0 w-full bg-black/95 backdrop-blur-sm border-b border-gray-800/30">
|
<div
|
||||||
|
className="md:hidden absolute top-full left-0 w-full backdrop-blur-sm border-b"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
"color-mix(in srgb, var(--background) 95%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--gray-800) 30%, transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="px-4 py-4 flex flex-col gap-4">
|
<div className="px-4 py-4 flex flex-col gap-4">
|
||||||
|
{/* Theme Toggle Mobile */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile Navigation Links */}
|
{/* Mobile Navigation Links */}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest py-2"
|
className="transition text-xs font-normal uppercase tracking-widest py-2"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
HOME
|
HOME
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/events"
|
href="/events"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest py-2"
|
className="transition text-xs font-normal uppercase tracking-widest py-2"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
EVENTS
|
EVENTS
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/leaderboard"
|
href="/leaderboard"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest py-2"
|
className="transition text-xs font-normal uppercase tracking-widest py-2"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
LEADERBOARD
|
LEADERBOARD
|
||||||
</Link>
|
</Link>
|
||||||
@@ -182,7 +275,14 @@ export default function Navigation({
|
|||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className="text-pixel-gold hover:text-orange-400 transition text-xs font-normal uppercase tracking-widest py-2"
|
className="transition text-xs font-normal uppercase tracking-widest py-2"
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
ADMIN
|
ADMIN
|
||||||
</Link>
|
</Link>
|
||||||
@@ -191,13 +291,25 @@ export default function Navigation({
|
|||||||
|
|
||||||
{/* Mobile PlayerStats */}
|
{/* Mobile PlayerStats */}
|
||||||
{isAuthenticated && !isAuthPage && (
|
{isAuthenticated && !isAuthPage && (
|
||||||
<div className="lg:hidden pt-2 border-t border-gray-800/30">
|
<div
|
||||||
|
className="lg:hidden pt-2 border-t"
|
||||||
|
style={{
|
||||||
|
borderColor:
|
||||||
|
"color-mix(in srgb, var(--gray-800) 30%, transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PlayerStats initialUserData={initialUserData} />
|
<PlayerStats initialUserData={initialUserData} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile Auth Buttons */}
|
{/* Mobile Auth Buttons */}
|
||||||
<div className="flex flex-col gap-3 pt-2 border-t border-gray-800/30">
|
<div
|
||||||
|
className="flex flex-col gap-3 pt-2 border-t"
|
||||||
|
style={{
|
||||||
|
borderColor:
|
||||||
|
"color-mix(in srgb, var(--gray-800) 30%, transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -215,7 +327,14 @@ export default function Navigation({
|
|||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest py-2"
|
className="transition text-xs font-normal uppercase tracking-widest py-2"
|
||||||
|
style={{ color: "var(--foreground)" }}
|
||||||
|
onMouseEnter={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--accent-color)")
|
||||||
|
}
|
||||||
|
onMouseLeave={(e) =>
|
||||||
|
(e.currentTarget.style.color = "var(--foreground)")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Connexion
|
Connexion
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
success: "bg-green-900/50 border-green-500/50 text-green-400",
|
success: "border",
|
||||||
error: "bg-red-900/50 border-red-500/50 text-red-400",
|
error: "border",
|
||||||
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
|
warning: "border",
|
||||||
info: "bg-blue-900/50 border-blue-500/50 text-blue-400",
|
info: "border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Alert({
|
export default function Alert({
|
||||||
@@ -20,9 +20,33 @@ export default function Alert({
|
|||||||
className = "",
|
className = "",
|
||||||
...props
|
...props
|
||||||
}: AlertProps) {
|
}: AlertProps) {
|
||||||
|
const variantStyles = {
|
||||||
|
success: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--success) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--success) 50%, transparent)",
|
||||||
|
color: "var(--success)",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--destructive) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--destructive) 50%, transparent)",
|
||||||
|
color: "var(--destructive)",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--yellow) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--yellow) 50%, transparent)",
|
||||||
|
color: "var(--yellow)",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--blue) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--blue) 50%, transparent)",
|
||||||
|
color: "var(--blue)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`border rounded px-4 py-3 text-sm ${variantClasses[variant]} ${className}`}
|
className={`border rounded px-4 py-3 text-sm ${variantClasses[variant]} ${className}`}
|
||||||
|
style={variantStyles[variant]}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ export default function Avatar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${sizeClass} rounded-full border overflow-hidden bg-black/60 flex items-center justify-center relative ${className} ${borderClassName}`}
|
className={`${sizeClass} rounded-full border overflow-hidden flex items-center justify-center relative ${className} ${borderClassName}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{displaySrc ? (
|
{displaySrc ? (
|
||||||
<img
|
<img
|
||||||
@@ -61,7 +65,8 @@ export default function Avatar({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<span
|
<span
|
||||||
className={`text-pixel-gold font-bold ${displaySrc ? "hidden" : ""}`}
|
className={`font-bold ${displaySrc ? "hidden" : ""}`}
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
>
|
>
|
||||||
{initial}
|
{initial}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -29,7 +29,16 @@ export default function BackgroundSection({
|
|||||||
>
|
>
|
||||||
{/* Dark overlay for readability */}
|
{/* Dark overlay for readability */}
|
||||||
{overlay && (
|
{overlay && (
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
<div
|
||||||
|
className="absolute inset-0 bg-gradient-to-b"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom,
|
||||||
|
color-mix(in srgb, var(--background) 70%, transparent),
|
||||||
|
color-mix(in srgb, var(--background) 60%, transparent),
|
||||||
|
color-mix(in srgb, var(--background) 80%, transparent)
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: "bg-pixel-gold/20 border-pixel-gold/50 text-pixel-gold",
|
default: "border",
|
||||||
success: "bg-green-900/50 border-green-500/50 text-green-400",
|
success: "border",
|
||||||
warning: "bg-yellow-900/50 border-yellow-500/50 text-yellow-400",
|
warning: "border",
|
||||||
danger: "bg-red-900/50 border-red-500/50 text-red-400",
|
danger: "border",
|
||||||
info: "bg-blue-900/30 border-blue-500/50 text-blue-400",
|
info: "border",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
@@ -28,9 +28,38 @@ export default function Badge({
|
|||||||
className = "",
|
className = "",
|
||||||
...props
|
...props
|
||||||
}: BadgeProps) {
|
}: BadgeProps) {
|
||||||
|
const variantStyles = {
|
||||||
|
default: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--accent-color) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--accent-color) 50%, transparent)",
|
||||||
|
color: "var(--accent-color)",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--success) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--success) 50%, transparent)",
|
||||||
|
color: "var(--success)",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--yellow) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--yellow) 50%, transparent)",
|
||||||
|
color: "var(--yellow)",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--destructive) 20%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--destructive) 50%, transparent)",
|
||||||
|
color: "var(--destructive)",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--blue) 15%, transparent)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--blue) 50%, transparent)",
|
||||||
|
color: "var(--blue)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-block border uppercase rounded whitespace-nowrap flex-shrink-0 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
className={`inline-block border uppercase rounded whitespace-nowrap flex-shrink-0 ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||||
|
style={variantStyles[variant]}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -10,14 +10,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary:
|
primary: "btn-primary border transition-colors",
|
||||||
"border-pixel-gold/50 bg-black/60 text-white hover:bg-pixel-gold/10 hover:border-pixel-gold",
|
secondary: "btn-secondary border transition-colors",
|
||||||
secondary:
|
success: "btn-success border transition-colors",
|
||||||
"border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30 hover:border-gray-500",
|
danger: "btn-danger border transition-colors",
|
||||||
success:
|
ghost: "btn-ghost border-transparent transition-colors",
|
||||||
"border-green-500/50 bg-green-900/20 text-green-400 hover:bg-green-900/30",
|
|
||||||
danger: "border-red-500/50 bg-red-900/20 text-red-400 hover:bg-red-900/30",
|
|
||||||
ghost: "border-transparent bg-transparent text-white hover:text-pixel-gold",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: "bg-black/60 border border-pixel-gold/30",
|
default: "border",
|
||||||
dark: "bg-black/80 border border-pixel-gold/30",
|
dark: "border",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Card({
|
export default function Card({
|
||||||
@@ -18,9 +18,21 @@ export default function Card({
|
|||||||
className = "",
|
className = "",
|
||||||
...props
|
...props
|
||||||
}: CardProps) {
|
}: CardProps) {
|
||||||
|
const variantStyles = {
|
||||||
|
default: {
|
||||||
|
backgroundColor: "var(--card)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
backgroundColor: "var(--card-hover)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg backdrop-blur-sm ${variantClasses[variant]} ${className}`}
|
className={`rounded-lg backdrop-blur-sm ${variantClasses[variant]} ${className}`}
|
||||||
|
style={variantStyles[variant]}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -21,7 +21,18 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition ${className}`}
|
className={`w-full px-4 py-3 border rounded focus:outline-none transition ${className}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--input)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = "var(--accent-color)";
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = "var(--border)";
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -39,11 +39,18 @@ export default function Modal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
className="fixed inset-0 z-[200] flex items-center justify-center p-4 backdrop-blur-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "color-mix(in srgb, var(--background) 80%, transparent)",
|
||||||
|
}}
|
||||||
onClick={closeOnOverlayClick ? onClose : undefined}
|
onClick={closeOnOverlayClick ? onClose : undefined}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`bg-black border-2 border-pixel-gold/70 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
|
className={`border-2 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card-hover)",
|
||||||
|
borderColor: "color-mix(in srgb, var(--accent-color) 70%, transparent)",
|
||||||
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -10,14 +10,30 @@ interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantClasses = {
|
const getGradientStyle = (variant: "hp" | "xp" | "default", percentage: number) => {
|
||||||
hp: {
|
if (variant === "hp") {
|
||||||
high: "from-green-600 to-green-700",
|
if (percentage > 60) {
|
||||||
medium: "from-yellow-600 to-orange-700",
|
return {
|
||||||
low: "from-red-700 to-red-900",
|
background: `linear-gradient(to right, var(--success), color-mix(in srgb, var(--success) 90%, transparent))`,
|
||||||
},
|
};
|
||||||
xp: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
|
} else if (percentage > 30) {
|
||||||
default: "from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80",
|
return {
|
||||||
|
background: `linear-gradient(to right, var(--yellow), color-mix(in srgb, var(--accent) 90%, transparent))`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(to right, var(--destructive), color-mix(in srgb, var(--destructive) 90%, transparent))`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(to right,
|
||||||
|
color-mix(in srgb, var(--accent-color) 80%, transparent),
|
||||||
|
color-mix(in srgb, var(--accent-color) 70%, transparent),
|
||||||
|
color-mix(in srgb, var(--accent-color) 80%, transparent)
|
||||||
|
)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProgressBar({
|
export default function ProgressBar({
|
||||||
@@ -31,40 +47,42 @@ export default function ProgressBar({
|
|||||||
}: ProgressBarProps) {
|
}: ProgressBarProps) {
|
||||||
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
||||||
|
|
||||||
let gradientClass = "";
|
|
||||||
if (variant === "hp") {
|
|
||||||
if (percentage > 60) {
|
|
||||||
gradientClass = variantClasses.hp.high;
|
|
||||||
} else if (percentage > 30) {
|
|
||||||
gradientClass = variantClasses.hp.medium;
|
|
||||||
} else {
|
|
||||||
gradientClass = variantClasses.hp.low;
|
|
||||||
}
|
|
||||||
} else if (variant === "xp") {
|
|
||||||
gradientClass = variantClasses.xp;
|
|
||||||
} else {
|
|
||||||
gradientClass = variantClasses.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} {...props}>
|
<div className={className} {...props}>
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
<div className="flex justify-between text-xs mb-1" style={{ color: "var(--gray-400)" }}>
|
||||||
<span>{label || variant.toUpperCase()}</span>
|
<span>{label || variant.toUpperCase()}</span>
|
||||||
<span>
|
<span>
|
||||||
{value} / {max}
|
{value} / {max}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative h-2 sm:h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
<div
|
||||||
|
className="relative h-2 sm:h-3 border rounded overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--gray-900)",
|
||||||
|
borderColor: "var(--gray-700)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 bg-gradient-to-r ${gradientClass} transition-all duration-1000 ease-out`}
|
className="absolute inset-0 transition-all duration-1000 ease-out"
|
||||||
style={{ width: `${percentage}%` }}
|
style={{
|
||||||
|
width: `${percentage}%`,
|
||||||
|
...getGradientStyle(variant, percentage),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(to right, transparent, color-mix(in srgb, var(--foreground) 10%, transparent), transparent)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{variant === "hp" && percentage < 30 && (
|
{variant === "hp" && percentage < 30 && (
|
||||||
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
|
<div
|
||||||
|
className="absolute inset-0 border rounded animate-pulse"
|
||||||
|
style={{ borderColor: "var(--destructive)" }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,21 +28,28 @@ export default function SectionTitle({
|
|||||||
|
|
||||||
let titleClasses = `${baseClasses} ${sizeClasses[size]} ${className}`;
|
let titleClasses = `${baseClasses} ${sizeClasses[size]} ${className}`;
|
||||||
|
|
||||||
|
const titleStyles: React.CSSProperties & {
|
||||||
|
WebkitBackgroundClip?: string;
|
||||||
|
WebkitTextFillColor?: string;
|
||||||
|
} = {};
|
||||||
if (variant === "gradient") {
|
if (variant === "gradient") {
|
||||||
titleClasses += " bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent";
|
titleClasses += " bg-clip-text text-transparent";
|
||||||
|
titleStyles.background = `linear-gradient(to right, var(--accent-color), var(--accent), var(--accent-color))`;
|
||||||
|
titleStyles.WebkitBackgroundClip = "text";
|
||||||
|
titleStyles.WebkitTextFillColor = "transparent";
|
||||||
} else if (variant === "gold") {
|
} else if (variant === "gold") {
|
||||||
titleClasses += " text-pixel-gold";
|
titleStyles.color = "var(--accent-color)";
|
||||||
} else {
|
} else {
|
||||||
titleClasses += " text-white";
|
titleStyles.color = "var(--foreground)";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className={titleClasses} {...props}>
|
<h1 className={titleClasses} style={titleStyles} {...props}>
|
||||||
{variant === "gradient" ? (
|
{variant === "gradient" ? (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
textShadow: `0 0 30px color-mix(in srgb, var(--accent-color) 50%, transparent)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -52,7 +59,10 @@ export default function SectionTitle({
|
|||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
<div
|
||||||
|
className="text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide"
|
||||||
|
style={{ color: "var(--accent-color)" }}
|
||||||
|
>
|
||||||
<span>✦</span>
|
<span>✦</span>
|
||||||
<span>{subtitle}</span>
|
<span>{subtitle}</span>
|
||||||
<span>✦</span>
|
<span>✦</span>
|
||||||
@@ -61,4 +71,3 @@ export default function SectionTitle({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,20 @@ export default function StarRating({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onMouseEnter={() => !disabled && setHoverValue(star)}
|
onMouseEnter={() => !disabled && setHoverValue(star)}
|
||||||
onMouseLeave={() => !disabled && setHoverValue(0)}
|
onMouseLeave={() => !disabled && setHoverValue(0)}
|
||||||
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${
|
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${sizeClasses[size]}`}
|
||||||
star <= displayValue
|
style={{
|
||||||
? "text-pixel-gold"
|
color: star <= displayValue ? "var(--accent-color)" : "var(--gray-500)",
|
||||||
: "text-gray-600 hover:text-gray-500"
|
}}
|
||||||
} ${sizeClasses[size]}`}
|
onMouseEnter={(e) => {
|
||||||
|
if (!disabled && star > displayValue) {
|
||||||
|
e.currentTarget.style.color = "var(--gray-400)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!disabled && star > displayValue) {
|
||||||
|
e.currentTarget.style.color = "var(--gray-500)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
||||||
>
|
>
|
||||||
★
|
★
|
||||||
|
|||||||
@@ -27,7 +27,18 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
value={value}
|
value={value}
|
||||||
className={`w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition resize-none ${className}`}
|
className={`w-full px-4 py-3 border rounded focus:outline-none transition resize-none ${className}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--input)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = "var(--accent-color)";
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = "var(--border)";
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{showCharCount && maxLength && (
|
{showCharCount && maxLength && (
|
||||||
|
|||||||
31
components/ui/ThemeToggle.tsx
Normal file
31
components/ui/ThemeToggle.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
|
export default function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="px-3 py-1 border rounded text-xs uppercase tracking-widest transition"
|
||||||
|
style={{
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
backgroundColor: "var(--card)",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = "var(--accent-color)";
|
||||||
|
e.currentTarget.style.backgroundColor = "color-mix(in srgb, var(--accent-color) 10%, transparent)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = "var(--border)";
|
||||||
|
e.currentTarget.style.backgroundColor = "var(--card)";
|
||||||
|
}}
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === "dark" ? "🌙" : "💎"} {theme === "dark" ? "Gold" : "Cyan"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,4 +11,5 @@ export { default as SectionTitle } from "./SectionTitle";
|
|||||||
export { default as BackgroundSection } from "./BackgroundSection";
|
export { default as BackgroundSection } from "./BackgroundSection";
|
||||||
export { default as Alert } from "./Alert";
|
export { default as Alert } from "./Alert";
|
||||||
export { default as CloseButton } from "./CloseButton";
|
export { default as CloseButton } from "./CloseButton";
|
||||||
|
export { default as ThemeToggle } from "./ThemeToggle";
|
||||||
|
|
||||||
|
|||||||
66
contexts/ThemeContext.tsx
Normal file
66
contexts/ThemeContext.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
|
||||||
|
|
||||||
|
type Theme = "dark" | "dark-cyan";
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
setTheme: (theme: Theme) => void;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
initialTheme?: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, initialTheme = "dark-cyan" }: ThemeProviderProps) {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(initialTheme);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
// Apply theme class to document element
|
||||||
|
document.documentElement.className = theme;
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mounted) {
|
||||||
|
// Load theme from localStorage if available
|
||||||
|
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
||||||
|
if (savedTheme && (savedTheme === "dark" || savedTheme === "dark-cyan")) {
|
||||||
|
setThemeState(savedTheme);
|
||||||
|
document.documentElement.className = savedTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mounted]);
|
||||||
|
|
||||||
|
const setTheme = (newTheme: Theme) => {
|
||||||
|
setThemeState(newTheme);
|
||||||
|
document.documentElement.className = newTheme;
|
||||||
|
localStorage.setItem("theme", newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const newTheme = theme === "dark" ? "dark-cyan" : "dark";
|
||||||
|
setTheme(newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||||
|
{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;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user