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
+77
View File
@@ -0,0 +1,77 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export interface Category {
id: string;
name: string;
color: string;
}
interface CategoryFiltersProps {
categories: Category[];
}
export function CategoryFilters({ categories }: CategoryFiltersProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const activeCategory = searchParams.get("category");
const handleFilter = (categoryId: string | null) => {
const params = new URLSearchParams(searchParams);
if (categoryId) {
params.set("category", categoryId);
} else {
params.delete("category");
}
const query = params.toString();
router.push(query ? `${pathname}?${query}` : pathname);
};
return (
<section className="max-w-6xl mx-auto px-4 sm:px-6 pb-6 sm:pb-8">
{/* Scroll horizontal sur mobile, wrap sur desktop */}
<div className="flex gap-2 overflow-x-auto pb-2 sm:pb-0 sm:flex-wrap sm:overflow-visible scrollbar-none">
<button
onClick={() => handleFilter(null)}
className={`shrink-0 cursor-pointer px-3 sm:px-4 py-1.5 sm:py-2 rounded-full font-mono text-xs sm:text-sm transition-all border
${
!activeCategory
? "bg-cyan-500/20 text-cyan-400 border-cyan-500/30"
: "bg-slate-800/50 border-slate-700/50 text-slate-400 hover:border-slate-600/50"
}`}
type="button"
>
Tous
</button>
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => handleFilter(cat.id)}
className={`shrink-0 cursor-pointer px-3 sm:px-4 py-1.5 sm:py-2 rounded-full font-mono text-xs sm:text-sm transition-all border
${
activeCategory === cat.id
? ""
: "bg-slate-800/50 border-slate-700/50 hover:border-slate-600/50"
}`}
style={{
color: cat.color,
backgroundColor:
activeCategory === cat.id
? `${cat.color}20`
: undefined,
borderColor:
activeCategory === cat.id
? `${cat.color}50`
: undefined,
}}
type="button"
>
{cat.name}
</button>
))}
</div>
</section>
);
}
+21
View File
@@ -0,0 +1,21 @@
import type { Tutorial } from "@/lib/tutorials";
const colors: Record<string, string> = {
Débutant: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30",
Intermédiaire: "bg-amber-500/20 text-amber-400 border-amber-500/30",
Avancé: "bg-rose-500/20 text-rose-400 border-rose-500/30",
};
interface DifficultyBadgeProps {
level: Tutorial["difficulty"];
}
export function DifficultyBadge({ level }: DifficultyBadgeProps) {
return (
<span
className={`px-2 py-0.5 text-xs font-mono border rounded ${colors[level]}`}
>
{level}
</span>
);
}
+125
View File
@@ -0,0 +1,125 @@
import type { Tutorial } from "@/lib/tutorials";
import Link from "next/link";
import { DifficultyBadge } from "./DifficultyBadge";
interface TutorialCardProps {
tutorial: Tutorial;
}
export function TutorialCard({ tutorial }: TutorialCardProps) {
return (
<Link
href={`/tutos/${tutorial.slug}`}
className="group relative block h-full"
>
<div className="absolute -inset-px bg-linear-to-r from-cyan-500/20 via-violet-500/20 to-fuchsia-500/20 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-500 blur-sm" />
<article className="relative h-full flex flex-col bg-slate-900/80 border border-slate-700/50 rounded-lg p-6 hover:border-slate-600/50 transition-all duration-300">
{/* Terminal header */}
<div className="flex items-center gap-2 mb-4 pb-3 border-b border-slate-700/50">
<div className="flex gap-1.5">
<span className="w-3 h-3 rounded-full bg-rose-500/80" />
<span className="w-3 h-3 rounded-full bg-amber-500/80" />
<span className="w-3 h-3 rounded-full bg-emerald-500/80" />
</div>
<span className="text-slate-500 text-xs font-mono ml-2 truncate">
~/tutos/{tutorial.slug}
</span>
</div>
{/* Content */}
<div className="flex-1 flex flex-col space-y-3">
<div className="flex items-center gap-3 flex-wrap">
<span
className="font-mono text-sm px-2 py-0.5 rounded border"
style={{
backgroundColor: `${tutorial.category.color}20`,
borderColor: `${tutorial.category.color}40`,
color: tutorial.category.color,
}}
>
{tutorial.category.name}
</span>
<DifficultyBadge level={tutorial.difficulty} />
</div>
<h3 className="text-xl font-semibold text-slate-100 group-hover:text-cyan-300 transition-colors">
{tutorial.title}
</h3>
<p className="flex-1 text-slate-400 text-sm leading-relaxed line-clamp-3">
{tutorial.excerpt}
</p>
<div className="flex items-center gap-4 pt-2 text-xs text-slate-500 font-mono">
<span className="flex items-center gap-1">
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{new Date(tutorial.date).toLocaleDateString(
"fr-FR",
{
day: "numeric",
month: "short",
year: "numeric",
}
)}
</span>
{tutorial.readTime && (
<span className="flex items-center gap-1">
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{tutorial.readTime}
</span>
)}
</div>
</div>
{/* Hover prompt */}
<div className="mt-4 pt-3 border-t border-slate-700/50 opacity-0 group-hover:opacity-100 transition-all duration-300">
<div className="flex items-center justify-between mb-2">
<span className="text-slate-300 text-sm font-medium">
Commencer la lecture
</span>
<svg
className="w-5 h-5 text-cyan-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>
</svg>
</div>
<div className="h-0.5 bg-slate-700/50 rounded-full overflow-hidden">
<div className="h-full w-0 group-hover:w-full bg-linear-to-r from-cyan-500 to-violet-500 transition-all duration-700 ease-out"></div>
</div>
</div>
</article>
</Link>
);
}
+24
View File
@@ -0,0 +1,24 @@
import type { Tutorial } from "@/lib/tutorials";
import { TutorialCard } from "./TutorialCard";
interface TutorialsGridProps {
tutorials: Tutorial[];
}
export function TutorialsGrid({ tutorials }: TutorialsGridProps) {
return (
<section className="max-w-6xl mx-auto px-4 sm:px-6 pb-12 sm:pb-20">
{tutorials.length === 0 ? (
<p className="text-slate-500 text-sm font-mono text-center">
Aucun tutoriel pour l&apos;instant.
</p>
) : (
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{tutorials.map((tutorial) => (
<TutorialCard key={tutorial.id} tutorial={tutorial} />
))}
</div>
)}
</section>
);
}
+4
View File
@@ -0,0 +1,4 @@
export { DifficultyBadge } from "./DifficultyBadge";
export { TutorialCard } from "./TutorialCard";
export { CategoryFilters, type Category } from "./CategoryFilters";
export { TutorialsGrid } from "./TutorialsGrid";