feat: initialisation complète du projet CESIZen

- Backend Laravel 11 (API REST, JWT auth, PostgreSQL)
- Frontend Next.js 14 (React, TypeScript, Tailwind CSS)
- Docker Compose (PostgreSQL, backend, frontend)
- Modules : auth, profil, tracker émotions, informations, admin
This commit is contained in:
Azmog
2026-03-11 16:27:01 +01:00
commit e3da625927
158 changed files with 25139 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
"use client";
import React from "react";
interface BadgeProps {
children: React.ReactNode;
color?: string;
className?: string;
}
export default function Badge({
children,
color = "#fce117",
className = "",
}: BadgeProps) {
return (
<span
className={`
inline-flex items-center
px-2.5 py-0.5
rounded-full
text-xs font-medium
transition-all duration-200
${className}
`}
style={{
backgroundColor: `${color}20`,
color: color,
}}
>
{children}
</span>
);
}
+82
View File
@@ -0,0 +1,82 @@
"use client";
import React from "react";
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
}
const variantClasses: Record<ButtonVariant, string> = {
primary:
"bg-candlelight-500 hover:bg-candlelight-600 text-black font-semibold shadow-sm hover:shadow-md",
secondary:
"bg-malachite-500 hover:bg-malachite-600 text-white font-semibold shadow-sm hover:shadow-md",
outline:
"border-2 border-candlelight-500 text-candlelight-700 dark:text-candlelight-400 hover:bg-candlelight-50 dark:hover:bg-candlelight-950",
ghost:
"hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300",
danger:
"bg-red-500 hover:bg-red-600 text-white font-semibold shadow-sm hover:shadow-md",
};
const sizeClasses: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-5 py-2.5 text-base",
lg: "px-7 py-3.5 text-lg",
};
export default function Button({
variant = "primary",
size = "md",
loading = false,
disabled,
children,
className = "",
...rest
}: ButtonProps) {
return (
<button
disabled={disabled || loading}
className={`
inline-flex items-center justify-center gap-2
rounded-xl transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2
dark:focus:ring-offset-gray-900
disabled:opacity-50 disabled:cursor-not-allowed
${variantClasses[variant]}
${sizeClasses[size]}
${className}
`}
{...rest}
>
{loading && (
<svg
className="animate-spin h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{children}
</button>
);
}
+27
View File
@@ -0,0 +1,27 @@
"use client";
import React from "react";
interface CardProps {
children: React.ReactNode;
className?: string;
hover?: boolean;
}
export default function Card({ children, className = "", hover = false }: CardProps) {
return (
<div
className={`
bg-white dark:bg-gray-900
rounded-2xl shadow-sm
border border-gray-100 dark:border-gray-800
p-6
transition-all duration-200
${hover ? "hover:shadow-lg hover:-translate-y-0.5" : ""}
${className}
`}
>
{children}
</div>
);
}
+61
View File
@@ -0,0 +1,61 @@
"use client";
import React from "react";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
icon?: React.ReactNode;
}
export default function Input({
label,
error,
icon,
className = "",
id,
...inputProps
}: InputProps) {
const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
return (
<div className="w-full">
{label && (
<label
htmlFor={inputId}
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
>
{label}
</label>
)}
<div className="relative">
{icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-500">
{icon}
</div>
)}
<input
id={inputId}
className={`
w-full rounded-xl border border-gray-300 dark:border-gray-700
bg-white dark:bg-gray-900
text-gray-900 dark:text-gray-100
placeholder:text-gray-400 dark:placeholder:text-gray-500
focus:outline-none focus:ring-2 focus:ring-candlelight-500 focus:border-candlelight-500
transition-all duration-200
px-4 py-2.5 text-base
${icon ? "pl-10" : ""}
${error ? "border-red-500 focus:ring-red-500 focus:border-red-500" : ""}
${className}
`}
{...inputProps}
/>
</div>
{error && (
<p className="mt-1.5 text-sm text-red-500 dark:text-red-400">
{error}
</p>
)}
</div>
);
}
+89
View File
@@ -0,0 +1,89 @@
"use client";
import React, { useEffect, useCallback } from "react";
import { X } from "lucide-react";
type ModalSize = "sm" | "md" | "lg";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
size?: ModalSize;
}
const sizeClasses: Record<ModalSize, string> = {
sm: "max-w-sm",
md: "max-w-lg",
lg: "max-w-2xl",
};
export default function Modal({
isOpen,
onClose,
title,
children,
size = "md",
}: ModalProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
},
[onClose]
);
useEffect(() => {
if (isOpen) {
document.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "";
};
}, [isOpen, handleKeyDown]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-100 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200"
onClick={onClose}
/>
{/* Modal */}
<div
className={`
relative w-full ${sizeClasses[size]}
bg-white dark:bg-gray-900
rounded-2xl shadow-2xl
border border-gray-100 dark:border-gray-800
p-6
animate-in zoom-in-95 fade-in duration-200
`}
>
{/* Header */}
<div className="flex items-center justify-between mb-4">
{title && (
<h2 className="text-xl font-bold font-display text-gray-900 dark:text-gray-100">
{title}
</h2>
)}
<button
onClick={onClose}
className="ml-auto p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-gray-500 dark:text-gray-400"
aria-label="Fermer"
>
<X size={20} />
</button>
</div>
{/* Content */}
<div>{children}</div>
</div>
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
"use client";
import React from "react";
type SkeletonVariant = "text" | "circle" | "rect";
interface SkeletonProps {
className?: string;
variant?: SkeletonVariant;
}
const variantClasses: Record<SkeletonVariant, string> = {
text: "h-4 w-full rounded",
circle: "h-10 w-10 rounded-full",
rect: "h-24 w-full rounded-xl",
};
export default function Skeleton({
className = "",
variant = "text",
}: SkeletonProps) {
return (
<div
className={`
animate-pulse
bg-gray-200 dark:bg-gray-700
${variantClasses[variant]}
${className}
`}
/>
);
}
+52
View File
@@ -0,0 +1,52 @@
"use client";
import React from "react";
interface SliderProps {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
label?: string;
showValue?: boolean;
}
export default function Slider({
value,
onChange,
min = 1,
max = 10,
label,
showValue = true,
}: SliderProps) {
return (
<div className="w-full">
{(label || showValue) && (
<div className="flex items-center justify-between mb-2">
{label && (
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
</label>
)}
{showValue && (
<span className="text-sm font-bold text-candlelight-600 dark:text-candlelight-400 bg-candlelight-50 dark:bg-candlelight-950 px-2 py-0.5 rounded-lg">
{value}
</span>
)}
</div>
)}
<input
type="range"
min={min}
max={max}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-full h-2 rounded-full appearance-none cursor-pointer bg-gray-200 dark:bg-gray-700 accent-candlelight-500"
/>
<div className="flex justify-between mt-1">
<span className="text-xs text-gray-400 dark:text-gray-500">{min}</span>
<span className="text-xs text-gray-400 dark:text-gray-500">{max}</span>
</div>
</div>
);
}
@@ -0,0 +1,35 @@
"use client";
import React from "react";
import { useTheme } from "next-themes";
import { Sun, Moon } from "lucide-react";
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<button
className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
aria-label="Changer le thème"
>
<div className="h-5 w-5" />
</button>
);
}
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-all duration-200 text-gray-600 dark:text-gray-400"
aria-label={theme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
>
{theme === "dark" ? <Sun size={20} /> : <Moon size={20} />}
</button>
);
}