feat: adding login/RegisterForm and new columns in PG
This commit is contained in:
@@ -1,11 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TeamsService, userService } from "@/services";
|
import { TeamsService, userService } from "@/services";
|
||||||
import { AuthService } from "@/services";
|
import { AuthService } from "@/services";
|
||||||
import {
|
import { LoginLayout, AuthWrapper, LoginLoading } from "@/components/login";
|
||||||
LoginLayout,
|
|
||||||
LoginFormWrapper,
|
|
||||||
LoginLoading,
|
|
||||||
} from "@/components/login";
|
|
||||||
|
|
||||||
export default async function LoginPage() {
|
export default async function LoginPage() {
|
||||||
try {
|
try {
|
||||||
@@ -20,19 +16,15 @@ export default async function LoginPage() {
|
|||||||
const userProfile = await userService.getUserByUuid(userUuid);
|
const userProfile = await userService.getUserByUuid(userUuid);
|
||||||
|
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
// Passer le profil utilisateur pour permettre la modification
|
// Rediriger vers l'accueil si déjà connecté
|
||||||
return (
|
redirect("/");
|
||||||
<LoginLayout>
|
|
||||||
<LoginFormWrapper teams={teams} initialUser={userProfile} />
|
|
||||||
</LoginLayout>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas connecté, afficher le formulaire de connexion
|
// Si l'utilisateur n'est pas connecté, afficher le formulaire d'auth
|
||||||
return (
|
return (
|
||||||
<LoginLayout>
|
<LoginLayout>
|
||||||
<LoginFormWrapper teams={teams} />
|
<AuthWrapper teams={teams} />
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
64
components/login/auth-wrapper.tsx
Normal file
64
components/login/auth-wrapper.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LoginForm, RegisterForm } from "./index";
|
||||||
|
|
||||||
|
interface AuthWrapperProps {
|
||||||
|
teams: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthWrapper({ teams }: AuthWrapperProps) {
|
||||||
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async (email: string, password: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// TODO: Implémenter la logique de login
|
||||||
|
console.log("Login attempt:", { email, password });
|
||||||
|
// await authClient.login(email, password);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login failed:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegister = async (data: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
teamId: string;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// TODO: Implémenter la logique de register
|
||||||
|
console.log("Register attempt:", data);
|
||||||
|
// await authClient.register(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Register failed:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLogin) {
|
||||||
|
return (
|
||||||
|
<LoginForm
|
||||||
|
onSubmit={handleLogin}
|
||||||
|
onSwitchToRegister={() => setIsLogin(false)}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RegisterForm
|
||||||
|
teams={teams}
|
||||||
|
onSubmit={handleRegister}
|
||||||
|
onSwitchToLogin={() => setIsLogin(true)}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
export { LoginFormWrapper } from "./login-form-wrapper";
|
export { LoginFormWrapper } from "./login-form-wrapper";
|
||||||
|
export { LoginForm } from "./login-form";
|
||||||
|
export { RegisterForm } from "./register-form";
|
||||||
|
export { AuthWrapper } from "./auth-wrapper";
|
||||||
export { LoginLoading } from "./login-loading";
|
export { LoginLoading } from "./login-loading";
|
||||||
export { LoginLayout } from "./login-layout";
|
export { LoginLayout } from "./login-layout";
|
||||||
|
|||||||
117
components/login/login-form.tsx
Normal file
117
components/login/login-form.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Code2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
onSubmit: (email: string, password: string) => void;
|
||||||
|
onSwitchToRegister: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginForm({
|
||||||
|
onSubmit,
|
||||||
|
onSwitchToRegister,
|
||||||
|
loading = false,
|
||||||
|
}: LoginFormProps) {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (email && password) {
|
||||||
|
onSubmit(email, password);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = email.length > 0 && password.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
|
||||||
|
<Code2 className="h-4 w-4 text-blue-400" />
|
||||||
|
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-bold mb-4 text-white">Connexion</h1>
|
||||||
|
<p className="text-lg text-slate-400 mb-8">
|
||||||
|
Connectez-vous à votre compte PeakSkills
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="max-w-md mx-auto">
|
||||||
|
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Se connecter</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Entrez vos identifiants pour accéder à votre compte
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-slate-300">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="text-slate-300">
|
||||||
|
Mot de passe
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Votre mot de passe"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
|
disabled={!isValid || loading}
|
||||||
|
>
|
||||||
|
{loading ? "Connexion..." : "Se connecter"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-6 border-t border-white/10">
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Pas encore de compte ?{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSwitchToRegister}
|
||||||
|
className="text-blue-400 hover:text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Créer un compte
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
327
components/login/register-form.tsx
Normal file
327
components/login/register-form.tsx
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo, useRef, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Team } from "@/lib/types";
|
||||||
|
import { Search, Building2, ChevronDown, Check, Code2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface RegisterFormProps {
|
||||||
|
teams: Team[];
|
||||||
|
onSubmit: (data: {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
teamId: string;
|
||||||
|
}) => void;
|
||||||
|
onSwitchToLogin: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterForm({
|
||||||
|
teams,
|
||||||
|
onSubmit,
|
||||||
|
onSwitchToLogin,
|
||||||
|
loading = false,
|
||||||
|
}: RegisterFormProps) {
|
||||||
|
const [firstName, setFirstName] = useState("");
|
||||||
|
const [lastName, setLastName] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [teamId, setTeamId] = useState("");
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isTeamDropdownOpen, setIsTeamDropdownOpen] = useState(false);
|
||||||
|
const teamDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState<"below" | "above">(
|
||||||
|
"below"
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
alert("Les mots de passe ne correspondent pas");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (firstName && lastName && email && password && teamId) {
|
||||||
|
onSubmit({ firstName, lastName, email, password, teamId });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid =
|
||||||
|
firstName.length > 0 &&
|
||||||
|
lastName.length > 0 &&
|
||||||
|
email.length > 0 &&
|
||||||
|
password.length > 0 &&
|
||||||
|
password === confirmPassword &&
|
||||||
|
teamId.length > 0;
|
||||||
|
|
||||||
|
// Group teams by direction and filter by search term
|
||||||
|
const teamsByDirection = useMemo(() => {
|
||||||
|
const filteredTeams = teams.filter(
|
||||||
|
(team) =>
|
||||||
|
team.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
team.direction.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredTeams.reduce((acc, team) => {
|
||||||
|
if (!acc[team.direction]) {
|
||||||
|
acc[team.direction] = [];
|
||||||
|
}
|
||||||
|
acc[team.direction].push(team);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, Team[]>);
|
||||||
|
}, [teams, searchTerm]);
|
||||||
|
|
||||||
|
// Calculate dropdown position when opening
|
||||||
|
const handleDropdownToggle = () => {
|
||||||
|
if (!isTeamDropdownOpen && teamDropdownRef.current) {
|
||||||
|
const rect = teamDropdownRef.current.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const spaceBelow = viewportHeight - rect.bottom;
|
||||||
|
const spaceAbove = rect.top;
|
||||||
|
const dropdownHeight = 300;
|
||||||
|
|
||||||
|
setDropdownPosition(
|
||||||
|
spaceBelow >= dropdownHeight || spaceBelow > spaceAbove
|
||||||
|
? "below"
|
||||||
|
: "above"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsTeamDropdownOpen(!isTeamDropdownOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
teamDropdownRef.current &&
|
||||||
|
!teamDropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsTeamDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedTeam = teams.find((team) => team.id === teamId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
|
||||||
|
<Code2 className="h-4 w-4 text-blue-400" />
|
||||||
|
<span className="text-sm font-medium text-slate-200">PeakSkills</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-bold mb-4 text-white">Créer un compte</h1>
|
||||||
|
<p className="text-lg text-slate-400 mb-8">
|
||||||
|
Rejoignez PeakSkills et commencez votre évaluation
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="max-w-md mx-auto">
|
||||||
|
<Card className="bg-white/5 border-white/10 backdrop-blur-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white">Inscription</CardTitle>
|
||||||
|
<CardDescription className="text-slate-400">
|
||||||
|
Créez votre compte pour commencer
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="firstName" className="text-slate-300">
|
||||||
|
Prénom
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
|
value={firstName}
|
||||||
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
|
placeholder="Votre prénom"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lastName" className="text-slate-300">
|
||||||
|
Nom
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="lastName"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
|
placeholder="Votre nom"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-slate-300">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="text-slate-300">
|
||||||
|
Mot de passe
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Mot de passe"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirmPassword" className="text-slate-300">
|
||||||
|
Confirmer
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="Confirmer"
|
||||||
|
required
|
||||||
|
className="bg-white/10 border-white/20 text-white placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="team" className="text-slate-300">
|
||||||
|
Équipe
|
||||||
|
</Label>
|
||||||
|
<div className="relative" ref={teamDropdownRef}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between bg-white/10 border-white/20 text-white hover:bg-white/20"
|
||||||
|
onClick={handleDropdownToggle}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={selectedTeam ? "text-white" : "text-slate-400"}
|
||||||
|
>
|
||||||
|
{selectedTeam
|
||||||
|
? selectedTeam.name
|
||||||
|
: "Sélectionnez votre équipe"}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isTeamDropdownOpen && (
|
||||||
|
<div
|
||||||
|
className={`absolute left-0 right-0 bg-gray-800 border border-white/20 rounded-md shadow-lg z-50 max-h-[300px] overflow-y-auto ${
|
||||||
|
dropdownPosition === "below"
|
||||||
|
? "top-full mt-1"
|
||||||
|
: "bottom-full mb-1"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="sticky top-0 bg-gray-800 border-b border-white/20 p-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-slate-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Rechercher une équipe..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-8 h-8 text-sm bg-gray-700 border-white/20 text-white"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Object.entries(teamsByDirection).map(
|
||||||
|
([direction, directionTeams]) => (
|
||||||
|
<div key={direction}>
|
||||||
|
<div className="px-3 py-2 text-sm font-semibold text-slate-400 bg-gray-700/50 border-b border-white/20 flex items-center gap-2">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
{direction}
|
||||||
|
</div>
|
||||||
|
{directionTeams.map((team) => (
|
||||||
|
<button
|
||||||
|
key={team.id}
|
||||||
|
type="button"
|
||||||
|
className={`w-full px-3 py-2 text-left hover:bg-gray-700/50 flex items-center justify-between text-white ${
|
||||||
|
team.id === teamId ? "bg-blue-600/20" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setTeamId(team.id);
|
||||||
|
setIsTeamDropdownOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{team.name}</span>
|
||||||
|
{team.id === teamId && (
|
||||||
|
<Check className="h-4 w-4 text-blue-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Object.keys(teamsByDirection).length === 0 && (
|
||||||
|
<div className="px-3 py-4 text-center text-sm text-slate-400">
|
||||||
|
Aucune équipe trouvée pour "{searchTerm}"
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
|
disabled={!isValid || loading}
|
||||||
|
>
|
||||||
|
{loading ? "Création..." : "Créer le compte"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-6 border-t border-white/10">
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Déjà un compte ?{" "}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSwitchToLogin}
|
||||||
|
className="text-blue-400 hover:text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
package.json
10
package.json
@@ -44,7 +44,10 @@
|
|||||||
"@radix-ui/react-toggle": "1.1.1",
|
"@radix-ui/react-toggle": "1.1.1",
|
||||||
"@radix-ui/react-toggle-group": "1.1.1",
|
"@radix-ui/react-toggle-group": "1.1.1",
|
||||||
"@radix-ui/react-tooltip": "1.1.6",
|
"@radix-ui/react-tooltip": "1.1.6",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/pg": "^8.11.10",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"pg": "^8.12.0",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
@@ -65,9 +69,7 @@
|
|||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9",
|
||||||
"zod": "3.25.67",
|
"zod": "3.25.67"
|
||||||
"pg": "^8.12.0",
|
|
||||||
"@types/pg": "^8.11.10"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
@@ -76,8 +78,8 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
"tw-animate-css": "1.3.3",
|
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -104,12 +104,18 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: 1.1.6
|
specifier: 1.1.6
|
||||||
version: 1.1.6(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 1.1.6(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: ^8.11.10
|
specifier: ^8.11.10
|
||||||
version: 8.15.5
|
version: 8.15.5
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.21(postcss@8.5.6)
|
version: 10.4.21(postcss@8.5.6)
|
||||||
|
bcrypt:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -1345,6 +1351,9 @@ packages:
|
|||||||
'@tailwindcss/postcss@4.1.12':
|
'@tailwindcss/postcss@4.1.12':
|
||||||
resolution: {integrity: sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==}
|
resolution: {integrity: sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==}
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||||
|
|
||||||
'@types/d3-array@3.2.1':
|
'@types/d3-array@3.2.1':
|
||||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||||
|
|
||||||
@@ -1397,6 +1406,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss: ^8.1.0
|
postcss: ^8.1.0
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
browserslist@4.25.3:
|
browserslist@4.25.3:
|
||||||
resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
|
resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -1712,6 +1725,14 @@ packages:
|
|||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-addon-api@8.5.0:
|
||||||
|
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||||
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4:
|
||||||
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-releases@2.0.19:
|
node-releases@2.0.19:
|
||||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||||
|
|
||||||
@@ -3042,6 +3063,10 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
tailwindcss: 4.1.12
|
tailwindcss: 4.1.12
|
||||||
|
|
||||||
|
'@types/bcrypt@6.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.17.2
|
||||||
|
|
||||||
'@types/d3-array@3.2.1': {}
|
'@types/d3-array@3.2.1': {}
|
||||||
|
|
||||||
'@types/d3-color@3.1.3': {}
|
'@types/d3-color@3.1.3': {}
|
||||||
@@ -3098,6 +3123,11 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
|
|
||||||
|
bcrypt@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-addon-api: 8.5.0
|
||||||
|
node-gyp-build: 4.8.4
|
||||||
|
|
||||||
browserslist@4.25.3:
|
browserslist@4.25.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001735
|
caniuse-lite: 1.0.30001735
|
||||||
@@ -3392,6 +3422,10 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
|
node-addon-api@8.5.0: {}
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-releases@2.0.19: {}
|
node-releases@2.0.19: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ CREATE TABLE users (
|
|||||||
first_name VARCHAR(100) NOT NULL,
|
first_name VARCHAR(100) NOT NULL,
|
||||||
last_name VARCHAR(100) NOT NULL,
|
last_name VARCHAR(100) NOT NULL,
|
||||||
team_id VARCHAR(50) REFERENCES teams(id),
|
team_id VARCHAR(50) REFERENCES teams(id),
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (uuid_id),
|
PRIMARY KEY (uuid_id),
|
||||||
UNIQUE(first_name, last_name, team_id),
|
|
||||||
UNIQUE(uuid_id)
|
UNIQUE(uuid_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -89,7 +90,8 @@ CREATE INDEX idx_teams_direction ON teams(direction);
|
|||||||
CREATE INDEX idx_skills_category_id ON skills(category_id);
|
CREATE INDEX idx_skills_category_id ON skills(category_id);
|
||||||
CREATE INDEX idx_skill_links_skill_id ON skill_links(skill_id);
|
CREATE INDEX idx_skill_links_skill_id ON skill_links(skill_id);
|
||||||
CREATE INDEX idx_users_team_id ON users(team_id);
|
CREATE INDEX idx_users_team_id ON users(team_id);
|
||||||
CREATE INDEX idx_users_unique_person ON users(first_name, last_name, team_id);
|
CREATE INDEX idx_users_email ON users(email);
|
||||||
|
|
||||||
CREATE INDEX idx_users_uuid_id ON users(uuid_id); -- Index on UUID for performance
|
CREATE INDEX idx_users_uuid_id ON users(uuid_id); -- Index on UUID for performance
|
||||||
CREATE INDEX idx_user_evaluations_user_uuid ON user_evaluations(user_uuid);
|
CREATE INDEX idx_user_evaluations_user_uuid ON user_evaluations(user_uuid);
|
||||||
CREATE INDEX idx_user_evaluations_user_id ON user_evaluations(user_id); -- Legacy index
|
CREATE INDEX idx_user_evaluations_user_id ON user_evaluations(user_id); -- Legacy index
|
||||||
|
|||||||
22
scripts/migrations/add_auth_fields.sql
Normal file
22
scripts/migrations/add_auth_fields.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Ajout des champs email et password à la table users
|
||||||
|
-- Étape 1: Ajouter les colonnes sans contraintes NOT NULL
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN email VARCHAR(255),
|
||||||
|
ADD COLUMN password_hash VARCHAR(255);
|
||||||
|
|
||||||
|
-- Étape 2: Remplir les colonnes avec des valeurs par défaut pour les données existantes
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
email = CONCAT(LOWER(first_name), '.', LOWER(last_name), '@peakskills.local'),
|
||||||
|
password_hash = '$2b$10$default.hash.for.existing.users.placeholder'
|
||||||
|
WHERE email IS NULL;
|
||||||
|
|
||||||
|
-- Étape 3: Ajouter les contraintes NOT NULL après avoir rempli les données
|
||||||
|
ALTER TABLE users
|
||||||
|
ALTER COLUMN email SET NOT NULL,
|
||||||
|
ALTER COLUMN password_hash SET NOT NULL;
|
||||||
|
|
||||||
|
-- Étape 4: Ajouter l'index et la contrainte unique sur l'email
|
||||||
|
CREATE INDEX idx_users_email ON users(email);
|
||||||
|
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
|
||||||
|
|
||||||
Reference in New Issue
Block a user