feat: ajout structure projet + config Node 20

This commit is contained in:
UltraLionFr
2025-12-07 00:11:21 +01:00
parent c728019f04
commit 25d49d04f0
60 changed files with 4260 additions and 157 deletions
+159
View File
@@ -0,0 +1,159 @@
"use client";
import { gsap } from "gsap";
import { useEffect, useRef, useState } from "react";
interface HeroSectionProps {
tutorialsCount: number;
categoriesCount: number;
title?: string | null;
filtered?: boolean;
}
// Effet de décryptage
function useScrambleText(
text: string,
options: {
duration?: number;
delay?: number;
chars?: string;
} = {}
) {
const [displayText, setDisplayText] = useState("");
const {
duration = 1.5,
delay = 0.3,
chars = "!@#$%^&*()_+-=[]{}|;:,.<>?/~`0123456789",
} = options;
useEffect(() => {
const finalText = text;
const length = finalText.length;
let currentIndex = 0;
// Délai initial
const startTimeout = setTimeout(() => {
const scrambleInterval = setInterval(() => {
let result = "";
for (let i = 0; i < length; i++) {
if (i < currentIndex) {
result += finalText[i];
} else if (finalText[i] === " ") {
result += " ";
} else {
// Caractère aléatoire
result +=
chars[Math.floor(Math.random() * chars.length)];
}
}
setDisplayText(result);
}, 30);
// Animation gsap pour révéler progressivement
gsap.to(
{ value: 0 },
{
value: length,
duration,
ease: "power2.inOut",
onUpdate: function () {
currentIndex = Math.floor(this.targets()[0].value);
},
onComplete: () => {
clearInterval(scrambleInterval);
setDisplayText(finalText);
},
}
);
return () => clearInterval(scrambleInterval);
}, delay * 1000);
return () => clearTimeout(startTimeout);
}, [text, duration, delay, chars]);
return displayText;
}
export function HeroSection({
tutorialsCount,
categoriesCount,
title,
filtered = false,
}: HeroSectionProps) {
const commandText = filtered
? `ls --filter="${title}"`
: "cat hello_world.txt";
const scrambledText = useScrambleText(commandText, {
duration: 1.2,
delay: 0.5,
chars: "█▓▒░!@#$%&*<>[]{}|",
});
// Animation du curseur clignotant
const cursorRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (cursorRef.current) {
gsap.to(cursorRef.current, {
opacity: 0,
duration: 0.5,
repeat: -1,
yoyo: true,
ease: "power2.inOut",
});
}
}, []);
return (
<section className="max-w-6xl mx-auto px-6 pt-16 pb-12">
<div className="space-y-4">
<div className="flex items-center gap-2 text-slate-500 font-mono text-sm">
<span className="text-emerald-400">$</span>
<span className="relative">
{scrambledText}
<span
ref={cursorRef}
className="inline-block w-2 h-4 bg-emerald-400 ml-0.5 align-middle"
/>
</span>
</div>
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight">
<span className="bg-linear-to-r from-cyan-400 via-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
{title || "Mes Tutoriels"}
</span>
</h1>
<p className="text-slate-400 text-lg max-w-2xl leading-relaxed">
{filtered
? `${tutorialsCount} tutoriel${
tutorialsCount > 1 ? "s" : ""
} dans cette catégorie.`
: "Une collection de guides pratiques. Apprenez à votre rythme avec des exemples concrets."}
</p>
</div>
{/* Stats */}
<div className="mt-10 flex flex-wrap gap-8">
<div className="space-y-1">
<div className="text-3xl font-bold text-slate-100">
{tutorialsCount}
</div>
<div className="text-sm text-slate-500 font-mono">
tutoriel{tutorialsCount > 1 ? "s" : ""}
{filtered ? " (filtrés)" : ""}
</div>
</div>
<div className="space-y-1">
<div className="text-3xl font-bold text-slate-100">
{categoriesCount}
</div>
<div className="text-sm text-slate-500 font-mono">
catégories
</div>
</div>
</div>
</section>
);
}
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { signOut } from "next-auth/react";
import { useState } from "react";
export function LogoutButton() {
const [loading, setLoading] = useState(false);
const handleLogout = async () => {
setLoading(true);
await signOut({ callbackUrl: "/" });
};
return (
<button
onClick={handleLogout}
disabled={loading}
className="cursor-pointer flex items-center gap-2 px-4 py-2 rounded-lg font-mono text-sm
bg-rose-500/10 border border-rose-500/30 text-rose-400
hover:bg-rose-500/20 hover:border-rose-500/50 transition-all
disabled:opacity-50 disabled:cursor-not-allowed"
type="button"
>
{loading ? (
<svg
className="w-4 h-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
) : (
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
)}
{loading ? "Déconnexion..." : "Se déconnecter"}
</button>
);
}
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { ArrowLeftOutlined } from "@lineiconshq/free-icons";
import { Lineicons } from "@lineiconshq/react-lineicons";
import Link from "next/link";
export function BackButton() {
return (
<Link
href="/"
className="group inline-flex items-center gap-2 mb-8 px-4 py-2 rounded-lg font-mono text-sm
bg-slate-800/60 border border-slate-700/70
hover:border-cyan-400/60 hover:text-cyan-300 transition-all duration-200"
>
<Lineicons
icon={ArrowLeftOutlined}
size={16}
className="transition-transform duration-200 group-hover:-translate-x-1"
/>
Retour aux tutoriels
</Link>
);
}
+3
View File
@@ -0,0 +1,3 @@
export { BackButton } from "./back-button";
export { HeroSection } from "./HeroSection";
export { LogoutButton } from "./LogoutButton";