Refactor component imports and structure: Update import paths for various components to improve organization, moving them into appropriate subdirectories. Remove unused components related to user and event management, enhancing code clarity and maintainability across the application.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s

This commit is contained in:
Julien Froidefond
2025-12-12 16:48:41 +01:00
parent 880e96d6e4
commit 97db800c73
27 changed files with 23 additions and 23 deletions

View File

@@ -0,0 +1,71 @@
"use client";
import Link from "next/link";
import { Button, BackgroundSection } from "@/components/ui";
interface HeroSectionProps {
backgroundImage: string;
}
export default function HeroSection({ backgroundImage }: HeroSectionProps) {
return (
<BackgroundSection backgroundImage={backgroundImage} className="pt-24">
<div className="text-center flex flex-col items-center">
{/* Game Title */}
<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">
<span
className="title-animated inline-block relative z-10"
style={{
backgroundImage: `linear-gradient(90deg, #daa520 0%, #ffa500 30%, #ff8c00 50%, #ffa500 70%, #daa520 100%)`,
backgroundSize: "200% auto",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
color: "transparent",
filter: `drop-shadow(0 0 12px rgba(255, 140, 0, 0.4))`,
}}
>
GAME.OF.TECH
</span>
</h1>
</div>
{/* 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">
<span></span>
<span>Peaksys</span>
<span></span>
</div>
{/* Description */}
<p className="text-white text-base md:text-lg max-w-3xl mx-auto mb-12 leading-relaxed px-4">
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
l&apos;expérience, montez en niveau et affrontez vos collègues sur le
classement. L&apos;excellence technique, ça se joue.
</p>
{/* Call-to-Action Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
<Link href="/events">
<Button variant="primary" size="lg">
See events
</Button>
</Link>
<Link href="/leaderboard">
<Button
variant="primary"
size="lg"
className="flex items-center gap-2"
>
<span></span>
<span>See leaderboard</span>
</Button>
</Link>
</div>
</div>
</BackgroundSection>
);
}

View File

@@ -0,0 +1,225 @@
"use client";
import { useState, useEffect, useRef, type ChangeEvent } from "react";
import { Input, Button, Card } from "@/components/ui";
interface ImageSelectorProps {
value: string;
onChange: (url: string) => void;
label: string;
}
export default function ImageSelector({
value,
onChange,
label,
}: ImageSelectorProps) {
const [availableImages, setAvailableImages] = useState<string[]>([]);
const [uploading, setUploading] = useState(false);
const [urlInput, setUrlInput] = useState("");
const [showGallery, setShowGallery] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetchAvailableImages();
}, []);
const fetchAvailableImages = async () => {
try {
const response = await fetch("/api/admin/images/list");
if (response.ok) {
const data = await response.json();
setAvailableImages(data.images || []);
}
} catch (error) {
console.error("Error fetching images:", error);
}
};
const handleFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/admin/images/upload", {
method: "POST",
body: formData,
});
if (response.ok) {
const data = await response.json();
onChange(data.url);
await fetchAvailableImages(); // Rafraîchir la liste
} else {
alert("Erreur lors de l'upload de l'image");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("Erreur lors de l'upload de l'image");
} finally {
setUploading(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
const handleUrlSubmit = () => {
if (urlInput.trim()) {
onChange(urlInput.trim());
setUrlInput("");
}
};
return (
<div className="space-y-3">
<label className="block text-xs sm:text-sm text-gray-300 mb-1 break-words">
{label}
</label>
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
{/* Colonne gauche - Image */}
<div className="flex-shrink-0 flex justify-center sm:justify-start">
{value ? (
<div className="relative w-full sm:w-48 h-40 sm:h-32 border border-pixel-gold/30 rounded overflow-hidden bg-black/60">
<img
src={value}
alt="Preview"
className="w-full h-full object-cover"
onError={(e) => {
const target = e.currentTarget;
// Ne pas boucler si l'image de fallback échoue aussi
const currentSrc = target.src;
const fallbackSrc = "/got-2.jpg";
if (!currentSrc.includes(fallbackSrc)) {
target.src = fallbackSrc;
} else {
target.style.display = "none";
}
}}
/>
<button
onClick={() => onChange("")}
className="absolute top-2 right-2 px-2 py-1 bg-red-900/80 text-red-200 text-xs rounded hover:bg-red-900 transition"
>
</button>
</div>
) : (
<div className="w-full sm:w-48 h-40 sm:h-32 border border-pixel-gold/30 rounded bg-black/60 flex items-center justify-center">
<span className="text-xs text-gray-500">Aucune</span>
</div>
)}
</div>
{/* Colonne droite - Contrôles */}
<div className="flex-1 space-y-3 min-w-0">
{/* Input URL */}
<div className="flex flex-col sm:flex-row gap-2">
<Input
type="text"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()}
placeholder="https://example.com/image.jpg ou /image.jpg"
className="flex-1 text-xs sm:text-sm px-3 py-2 min-w-0"
/>
<Button
onClick={handleUrlSubmit}
variant="primary"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
URL
</Button>
</div>
{/* Upload depuis le disque */}
<div className="flex flex-col sm:flex-row gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
id={`file-${label}`}
/>
<label htmlFor={`file-${label}`}>
<Button
variant="primary"
size="sm"
as="span"
disabled={uploading}
className="flex-1 text-center cursor-pointer"
>
{uploading ? "Upload..." : "Upload depuis le disque"}
</Button>
</label>
<Button
onClick={() => setShowGallery(!showGallery)}
variant="primary"
size="sm"
className="whitespace-nowrap"
>
{showGallery ? "Masquer" : "Galerie"}
</Button>
</div>
{/* Chemin de l'image */}
{value && (
<p className="text-xs text-gray-400 truncate break-all">{value}</p>
)}
</div>
</div>
{/* Galerie d'images */}
{showGallery && (
<Card variant="dark" className="mt-4 p-3 sm:p-4">
<h4 className="text-xs sm:text-sm text-gray-300 mb-3">
Images disponibles
</h4>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 sm:gap-3 max-h-64 overflow-y-auto">
{availableImages.length === 0 ? (
<div className="col-span-full text-center text-gray-400 text-sm py-4">
Aucune image disponible
</div>
) : (
availableImages.map((imageUrl) => (
<button
key={imageUrl}
onClick={() => {
onChange(imageUrl);
setShowGallery(false);
}}
className={`relative aspect-video rounded overflow-hidden border-2 transition ${
value === imageUrl
? "border-pixel-gold"
: "border-pixel-gold/30 hover:border-pixel-gold/50"
}`}
>
<img
src={imageUrl}
alt={imageUrl}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
{value === imageUrl && (
<div className="absolute inset-0 bg-pixel-gold/20 flex items-center justify-center">
<span className="text-pixel-gold text-xs"></span>
</div>
)}
</button>
))
)}
</div>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,12 @@
"use client";
import { type ReactNode } from "react";
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
export default function SessionProvider({ children }: { children: ReactNode }) {
return (
<NextAuthSessionProvider basePath="/api/auth">
{children}
</NextAuthSessionProvider>
);
}