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
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
This commit is contained in:
71
components/layout/HeroSection.tsx
Normal file
71
components/layout/HeroSection.tsx
Normal 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'expérience, montez en niveau et affrontez vos collègues sur le
|
||||
classement. L'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>
|
||||
);
|
||||
}
|
||||
225
components/layout/ImageSelector.tsx
Normal file
225
components/layout/ImageSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
components/layout/SessionProvider.tsx
Normal file
12
components/layout/SessionProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user