first commit
This commit is contained in:
parent
b1fc9f95e4
commit
135afa6d19
27 changed files with 2798 additions and 134 deletions
76
design-system.md
Normal file
76
design-system.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Design System - Plateforme d'Emploi
|
||||
|
||||
## Principes Fondamentaux
|
||||
- Minimalisme et simplicité
|
||||
- Espace blanc généreux
|
||||
- Typographie claire et lisible
|
||||
- Hiérarchie visuelle forte
|
||||
- Animations subtiles et fluides
|
||||
|
||||
## Palette de Couleurs
|
||||
- **Primaire** :
|
||||
- Bleu Apple (#007AFF)
|
||||
- Blanc (#FFFFFF)
|
||||
- Noir (#000000)
|
||||
- **Secondaire** :
|
||||
- Gris clair (#F5F5F7)
|
||||
- Gris moyen (#86868B)
|
||||
- Gris foncé (#1D1D1F)
|
||||
- **Accents** :
|
||||
- Vert (#34C759) - pour les actions positives
|
||||
- Rouge (#FF3B30) - pour les alertes/erreurs
|
||||
|
||||
## Typographie
|
||||
- **Titres** : SF Pro Display
|
||||
- H1: 48px, 600
|
||||
- H2: 36px, 600
|
||||
- H3: 24px, 600
|
||||
- **Corps** : SF Pro Text
|
||||
- Paragraphe: 16px, 400
|
||||
- Petit texte: 14px, 400
|
||||
- Légendes: 12px, 400
|
||||
|
||||
## Espacement
|
||||
- **Base** : 8px
|
||||
- **Petit** : 4px
|
||||
- **Moyen** : 16px
|
||||
- **Grand** : 24px
|
||||
- **Très grand** : 32px
|
||||
|
||||
## Composants Clés
|
||||
|
||||
### Boutons
|
||||
- **Primaire** : Fond bleu, texte blanc, coins arrondis (8px)
|
||||
- **Secondaire** : Fond transparent, bordure bleue, texte bleu
|
||||
- **Tertiaire** : Texte bleu uniquement
|
||||
|
||||
### Cartes
|
||||
- Ombre légère
|
||||
- Coins arrondis (12px)
|
||||
- Espacement intérieur généreux
|
||||
- Transitions fluides au survol
|
||||
|
||||
### Formulaires
|
||||
- Champs avec bordures subtiles
|
||||
- Labels flottants
|
||||
- États de focus et d'erreur clairs
|
||||
- Boutons d'action bien visibles
|
||||
|
||||
### Navigation
|
||||
- Barre de navigation fixe en haut
|
||||
- Menu latéral pour les sections principales
|
||||
- Indicateurs d'état clairs
|
||||
|
||||
## Animations
|
||||
- Transitions douces (0.3s)
|
||||
- Effets de survol subtils
|
||||
- Animations de chargement minimalistes
|
||||
- Transitions de page fluides
|
||||
|
||||
## Responsive Design
|
||||
- Breakpoints :
|
||||
- Mobile : < 768px
|
||||
- Tablet : 768px - 1024px
|
||||
- Desktop : > 1024px
|
||||
- Adaptation fluide des composants
|
||||
- Hiérarchie visuelle maintenue sur tous les écrans
|
63
docker-compose.yml
Normal file
63
docker-compose.yml
Normal file
|
@ -0,0 +1,63 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
db:
|
||||
image: supabase/postgres:latest
|
||||
container_name: supabase-db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
|
||||
studio:
|
||||
image: supabase/studio:latest
|
||||
container_name: supabase-studio
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
meta:
|
||||
image: supabase/postgres-meta:latest
|
||||
container_name: supabase-meta
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: db
|
||||
PG_META_DB_PASSWORD: postgres
|
||||
|
||||
auth:
|
||||
image: supabase/gotrue:latest
|
||||
container_name: supabase-auth
|
||||
ports:
|
||||
- "9999:9999"
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
API_EXTERNAL_URL: http://localhost:9999
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_HOST: db
|
||||
GOTRUE_DB_PORT: 5432
|
||||
GOTRUE_DB_NAME: postgres
|
||||
GOTRUE_DB_USER: postgres
|
||||
GOTRUE_DB_PASSWORD: postgres
|
||||
GOTRUE_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
GOTRUE_JWT_EXP: 3600
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
|
||||
rest:
|
||||
image: postgrest/postgrest:latest
|
||||
container_name: supabase-rest
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres
|
||||
PGRST_DB_SCHEMA: public
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
|
||||
volumes:
|
||||
db_data:
|
68
fdr.md
Normal file
68
fdr.md
Normal file
|
@ -0,0 +1,68 @@
|
|||
Phase 1 : Infrastructure de Base et Parcours Candidat Initial
|
||||
|
||||
Objectif Principal : Mettre en place l'architecture de base de l'application et permettre aux candidats de s'inscrire, de créer un profil simple et de rechercher des offres d'emploi via l'API France Travail.
|
||||
Étapes :
|
||||
Initialisation du Projet : Créer un nouveau projet Next.js.
|
||||
Configuration de la Base de Données et de l'Authentification : Configurer Supabase pour la base de données et l'authentification des utilisateurs.
|
||||
Installation des Librairies : Installer les librairies nécessaires : Zustand, Tailwind CSS, Material UI.
|
||||
Structure du Projet : Créer la structure de base des dossiers et des composants.
|
||||
Formulaires d'Inscription et de Connexion (Candidats) : Implémenter les interfaces pour l'inscription et la connexion des candidats.
|
||||
Gestion des Sessions Utilisateur : Mettre en place la gestion des sessions pour les candidats connectés.
|
||||
Formulaire de Création de Profil Candidat : Développer le formulaire permettant aux candidats de renseigner leurs informations essentielles (nom, prénom, e-mail, compétences techniques, niveau d'expérience, type de poste recherché, localisation souhaitée).
|
||||
Sauvegarde du Profil Candidat : Implémenter la sauvegarde des informations du profil dans la base de données Supabase.
|
||||
Interface de Recherche d'Offres : Créer une interface de recherche simple avec un champ de mots-clés et un filtre de localisation.
|
||||
Intégration de l'API France Travail : Implémenter les appels à l'API France Travail pour récupérer les offres d'emploi tech.
|
||||
Affichage des Résultats de Recherche : Afficher les offres d'emploi récupérées (titre, entreprise si disponible, localisation, brève description).
|
||||
|
||||
Phase 2 : Système de Matching Basique et Score de Compatibilité Primaire
|
||||
|
||||
Objectif Principal : Implémenter un algorithme de matching simple basé sur les compétences techniques et afficher un score de compatibilité basique.
|
||||
Étapes :
|
||||
Logique de Matching Initial : Implémenter l'algorithme comparant les compétences techniques du candidat avec les mots-clés des offres, en tenant compte du type de poste et de la localisation.
|
||||
Calcul du Score de Compatibilité Primaire : Attribuer un score basé sur le nombre de compétences techniques communes.
|
||||
Affichage du Score dans les Résultats : Afficher le score de compatibilité (par exemple, en pourcentage) à côté de chaque offre dans les résultats de recherche.
|
||||
Page de Détail de l'Offre : Créer une page pour afficher toutes les informations détaillées d'une offre.
|
||||
Récupération des Détails de l'Offre via l'API : Implémenter l'appel à l'API France Travail pour obtenir les détails d'une offre spécifique.
|
||||
Affichage du Score de Compatibilité sur la Page de Détail : Afficher le score de compatibilité du candidat pour l'offre consultée.
|
||||
|
||||
Phase 3 : Interface Recruteur Basique
|
||||
|
||||
Objectif Principal : Permettre aux recruteurs (vous dans un premier temps) de visualiser les profils des candidats inscrits.
|
||||
Étapes :
|
||||
Système d'Authentification Recruteur : Mettre en place un système d'authentification distinct ou basé sur des rôles pour les recruteurs.
|
||||
Tableau de Bord Recruteur : Développer l'interface affichant la liste des candidats inscrits avec leurs informations de base (nom, compétences principales, niveau d'expérience).
|
||||
Filtrage des Candidats : Implémenter la possibilité pour les recruteurs de filtrer les candidats par compétences techniques.
|
||||
Consultation du Profil Détail : Permettre aux recruteurs de cliquer sur un candidat pour afficher son profil complet.
|
||||
|
||||
Phase 4 : Intégration du Test de Personnalité et Définition des Valeurs
|
||||
|
||||
Objectif Principal : Ajouter la possibilité pour les candidats de réaliser un test de personnalité simple et de définir leurs valeurs.
|
||||
Étapes :
|
||||
Choix du Test de Personnalité : Sélectionner un questionnaire de personnalité court et pertinent.
|
||||
Implémentation du Questionnaire : Intégrer l'affichage du questionnaire dans l'application.
|
||||
Stockage des Résultats du Test : Sauvegarder les résultats du test de personnalité dans la base de données du candidat.
|
||||
Affichage des Résultats du Test dans le Profil : Rendre les résultats du test visibles sur le profil du candidat.
|
||||
Création de la Liste de Valeurs : Définir une liste de valeurs prédéfinies.
|
||||
Sélection des Valeurs par le Candidat : Permettre aux candidats de sélectionner leurs valeurs importantes dans leur profil.
|
||||
Stockage des Valeurs Sélectionnées : Sauvegarder les valeurs choisies par le candidat dans la base de données.
|
||||
|
||||
Phase 5 : Historique des Offres Consultées
|
||||
|
||||
Objectif Principal : Permettre aux candidats de voir un historique des offres qu'ils ont consultées.
|
||||
Étapes :
|
||||
Création de la Table "OffresConsultées" : Définir la structure de la table dans la base de données pour enregistrer les offres consultées.
|
||||
Enregistrement des Consultations : Implémenter la logique pour enregistrer chaque fois qu'un candidat consulte une offre détaillée.
|
||||
Page d'Historique des Offres : Créer une page dans l'espace candidat pour afficher la liste des offres consultées, avec la date de consultation et un lien vers l'offre.
|
||||
|
||||
Itérations et Améliorations Futures :
|
||||
|
||||
Matching basé sur les valeurs.
|
||||
Suggestions d'amélioration (formations).
|
||||
Fonctionnalités avancées pour les recruteurs (gestion des candidatures, publication d'offres).
|
||||
Intégration de l'IA.
|
||||
|
||||
Points Importants :
|
||||
|
||||
Logique Séquentielle : Les étapes sont numérotées pour refléter un ordre logique de développement, mais certaines étapes au sein d'une même phase peuvent être réalisées en parallèle.
|
||||
Flexibilité : Cette liste d'étapes peut être ajustée en fonction de vos besoins et des défis rencontrés.
|
||||
Tests : N'oubliez pas de prévoir des tests après chaque étape ou groupe d'étapes pour garantir la qualité.
|
47
init.sql
Normal file
47
init.sql
Normal file
|
@ -0,0 +1,47 @@
|
|||
-- Enable the pgcrypto extension for UUID generation
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- Create profiles table
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
email TEXT UNIQUE,
|
||||
skills TEXT[],
|
||||
experience_level TEXT,
|
||||
job_type TEXT[],
|
||||
location TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
|
||||
);
|
||||
|
||||
-- Create RLS policies
|
||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Users can view their own profile"
|
||||
ON profiles FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can update their own profile"
|
||||
ON profiles FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "Users can insert their own profile"
|
||||
ON profiles FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Create function to handle new user signup
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (user_id, email)
|
||||
VALUES (new.id, new.email);
|
||||
RETURN new;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Create trigger for new user signup
|
||||
CREATE OR REPLACE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
1094
package-lock.json
generated
1094
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
@ -9,19 +9,28 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
"@mui/material": "^7.0.2",
|
||||
"@supabase/auth-helpers-nextjs": "^0.10.0",
|
||||
"@supabase/ssr": "^0.6.1",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"framer-motion": "^12.7.3",
|
||||
"next": "15.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.3.0"
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.0",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
1
pitch.md
Normal file
1
pitch.md
Normal file
|
@ -0,0 +1 @@
|
|||
L'application que je souhaite créer est une plateforme d'emploi innovante conçue pour aller au-delà de la simple mise en relation par mots-clés. En s'inspirant de plateformes comme HelloWork, notre solution intègre un système de matching basique et un score de compatibilité primaire dès la première phase. L'objectif est de rapidement identifier les offres qui correspondent le mieux aux compétences techniques et aux préférences des candidats. Dans les phases ultérieures, nous enrichirons l'expérience avec un test de personnalité simple et la possibilité pour les candidats de définir leurs valeurs, afin d'améliorer la pertinence du matching avec la culture et les attentes des entreprises. Une interface dédiée permettra aux recruteurs de visualiser les profils des candidats et d'accéder à ces informations pour des recrutements plus éclairés.
|
5
src/app/auth/login/page.tsx
Normal file
5
src/app/auth/login/page.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import LoginForm from '@/components/auth/LoginForm';
|
||||
|
||||
export default function LoginPage() {
|
||||
return <LoginForm />;
|
||||
}
|
5
src/app/auth/register/page.tsx
Normal file
5
src/app/auth/register/page.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import RegisterForm from '@/components/auth/RegisterForm';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return <RegisterForm />;
|
||||
}
|
87
src/app/components/ElegantButton.tsx
Normal file
87
src/app/components/ElegantButton.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
'use client';
|
||||
|
||||
import { Button, ButtonProps } from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface ElegantButtonProps extends Omit<ButtonProps, 'variant'> {
|
||||
variant?: 'primary' | 'secondary' | 'tertiary';
|
||||
}
|
||||
|
||||
export default function ElegantButton({
|
||||
children,
|
||||
variant = 'primary',
|
||||
sx,
|
||||
...props
|
||||
}: ElegantButtonProps) {
|
||||
const getVariantStyles = () => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return {
|
||||
backgroundColor: '#007AFF',
|
||||
color: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: '#0062cc',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 122, 255, 0.3)',
|
||||
},
|
||||
};
|
||||
case 'secondary':
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
color: '#007AFF',
|
||||
border: '1px solid #007AFF',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
};
|
||||
case 'tertiary':
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
color: '#007AFF',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button
|
||||
{...props}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: 500,
|
||||
borderRadius: '12px',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent)',
|
||||
transform: 'translateX(-100%)',
|
||||
transition: 'transform 0.6s ease',
|
||||
},
|
||||
'&:hover::before': {
|
||||
transform: 'translateX(100%)',
|
||||
},
|
||||
...getVariantStyles(),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
204
src/app/components/Header.tsx
Normal file
204
src/app/components/Header.tsx
Normal file
|
@ -0,0 +1,204 @@
|
|||
'use client';
|
||||
|
||||
import { AppBar, Toolbar, Button, Typography, Box, IconButton, Menu, MenuItem, useMediaQuery, useTheme } from '@mui/material';
|
||||
import Link from 'next/link';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import WorkOutlineIcon from '@mui/icons-material/WorkOutline';
|
||||
import BusinessCenterIcon from '@mui/icons-material/BusinessCenter';
|
||||
import SchoolIcon from '@mui/icons-material/School';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Header() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [mobileMenuAnchor, setMobileMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleMobileMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setMobileMenuAnchor(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMobileMenuClose = () => {
|
||||
setMobileMenuAnchor(null);
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Offres', icon: <WorkOutlineIcon />, href: '/offres' },
|
||||
{ label: 'Entreprises', icon: <BusinessCenterIcon />, href: '/entreprises' },
|
||||
{ label: 'Formations', icon: <SchoolIcon />, href: '/formations' },
|
||||
];
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="static"
|
||||
elevation={0}
|
||||
sx={{
|
||||
backgroundColor: 'white',
|
||||
color: 'black',
|
||||
borderBottom: '1px solid #F5F5F7'
|
||||
}}
|
||||
>
|
||||
<Toolbar sx={{
|
||||
justifyContent: 'space-between',
|
||||
maxWidth: '1200px',
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
px: { xs: 2, md: 4 }
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component={Link}
|
||||
href="/"
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
color: 'black',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
JobMatch
|
||||
</Typography>
|
||||
|
||||
{!isMobile && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{navItems.map((item) => (
|
||||
<Button
|
||||
key={item.label}
|
||||
component={Link}
|
||||
href={item.href}
|
||||
startIcon={item.icon}
|
||||
sx={{
|
||||
color: '#1D1D1F',
|
||||
textTransform: 'none',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
color: '#007AFF',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{isMobile ? (
|
||||
<>
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleMobileMenuOpen}
|
||||
sx={{ color: '#1D1D1F' }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={mobileMenuAnchor}
|
||||
open={Boolean(mobileMenuAnchor)}
|
||||
onClose={handleMobileMenuClose}
|
||||
sx={{
|
||||
'& .MuiPaper-root': {
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
component={Link}
|
||||
href={item.href}
|
||||
onClick={handleMobileMenuClose}
|
||||
sx={{
|
||||
py: 1,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem
|
||||
component={Link}
|
||||
href="/signup"
|
||||
onClick={handleMobileMenuClose}
|
||||
sx={{
|
||||
py: 1,
|
||||
px: 2,
|
||||
color: '#007AFF',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Inscription
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
href="/login"
|
||||
onClick={handleMobileMenuClose}
|
||||
sx={{
|
||||
py: 1,
|
||||
px: 2,
|
||||
color: '#007AFF',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connexion
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/signup"
|
||||
sx={{
|
||||
color: '#007AFF',
|
||||
textTransform: 'none',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Inscription
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/login"
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: '#007AFF',
|
||||
textTransform: 'none',
|
||||
fontWeight: 500,
|
||||
borderRadius: '8px',
|
||||
'&:hover': {
|
||||
backgroundColor: '#0062cc',
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 4px 12px rgba(0, 122, 255, 0.2)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connexion
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Providers } from './providers';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
|
@ -13,21 +14,19 @@ const geistMono = Geist_Mono({
|
|||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "JobMatch - Plateforme d'emploi intelligente",
|
||||
description: "Trouvez votre prochain emploi tech avec une approche intelligente basée sur vos compétences et aspirations",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<html lang="fr">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
137
src/app/login/page.tsx
Normal file
137
src/app/login/page.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Box, Container, Typography, TextField, Button, Link as MuiLink } from '@mui/material';
|
||||
import Link from 'next/link';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { setUser } = useAuthStore();
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setUser(data.user);
|
||||
router.push('/profile');
|
||||
} catch (error) {
|
||||
setError('Email ou mot de passe incorrect');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 'calc(100vh - 64px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
py: 8,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 4,
|
||||
fontWeight: 700,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Connexion
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleLogin}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 3,
|
||||
p: 4,
|
||||
borderRadius: '16px',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
backgroundColor: '#007AFF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#0062cc',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isLoading ? 'Connexion...' : 'Se connecter'}
|
||||
</Button>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
color: '#86868B',
|
||||
}}
|
||||
>
|
||||
Pas encore de compte ?{' '}
|
||||
<MuiLink
|
||||
component={Link}
|
||||
href="/signup"
|
||||
sx={{
|
||||
color: '#007AFF',
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Créer un compte
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
289
src/app/page.tsx
289
src/app/page.tsx
|
@ -1,103 +1,200 @@
|
|||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import { Typography, Box, Container, TextField, InputAdornment, Chip } from '@mui/material';
|
||||
import Header from './components/Header';
|
||||
import ElegantButton from './components/ElegantButton';
|
||||
import { motion } from 'framer-motion';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||
|
||||
const contractTypes = [
|
||||
'CDI',
|
||||
'CDD',
|
||||
'Stage',
|
||||
'Alternance',
|
||||
'Freelance',
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
<main>
|
||||
<Header />
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'radial-gradient(circle at 50% 50%, rgba(0,122,255,0.05) 0%, rgba(255,255,255,0) 70%)',
|
||||
zIndex: -1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
py: { xs: 6, md: 12 },
|
||||
px: 4,
|
||||
minHeight: 'calc(100vh - 64px)',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
sx={{
|
||||
fontSize: { xs: '2.5rem', md: '4rem' },
|
||||
fontWeight: 700,
|
||||
mb: 3,
|
||||
maxWidth: '1000px',
|
||||
background: 'linear-gradient(45deg, #000000, #007AFF)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Trouvez votre prochain emploi tech avec une approche intelligente
|
||||
</Typography>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontSize: { xs: '1.1rem', md: '1.5rem' },
|
||||
color: '#86868B',
|
||||
mb: 6,
|
||||
maxWidth: '800px',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Découvrez des opportunités qui correspondent vraiment à vos compétences et aspirations
|
||||
</Typography>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
gap: 2,
|
||||
width: '100%',
|
||||
maxWidth: '800px',
|
||||
mb: 4,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Métier, compétence, mot-clé..."
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: '#007AFF' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
sx: {
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
},
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
placeholder="Où ? (Ville, département...)"
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<LocationOnIcon sx={{ color: '#007AFF' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
sx: {
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'white',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
},
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
mb: 4,
|
||||
}}
|
||||
>
|
||||
{contractTypes.map((type, index) => (
|
||||
<motion.div
|
||||
key={type}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.6 + index * 0.1 }}
|
||||
>
|
||||
<Chip
|
||||
label={type}
|
||||
clickable
|
||||
sx={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgba(0, 122, 255, 0.2)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.05)',
|
||||
borderColor: '#007AFF',
|
||||
},
|
||||
px: 1,
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</Box>
|
||||
<ElegantButton
|
||||
variant="primary"
|
||||
size="large"
|
||||
href="/search"
|
||||
sx={{
|
||||
fontSize: '1.2rem',
|
||||
px: 6,
|
||||
py: 2,
|
||||
}}
|
||||
>
|
||||
Rechercher des offres
|
||||
</ElegantButton>
|
||||
</motion.div>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
17
src/app/providers.tsx
Normal file
17
src/app/providers.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import theme from './theme';
|
||||
import { AuthProvider } from '@/contexts/AuthContext';
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
153
src/app/signup/page.tsx
Normal file
153
src/app/signup/page.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Box, Container, Typography, TextField, Button, Link as MuiLink } from '@mui/material';
|
||||
import Link from 'next/link';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
|
||||
export default function Signup() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { setUser } = useAuthStore();
|
||||
|
||||
const handleSignup = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Les mots de passe ne correspondent pas');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setUser(data.user);
|
||||
router.push('/profile');
|
||||
} catch (error) {
|
||||
setError('Une erreur est survenue lors de l\'inscription');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: 'calc(100vh - 64px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
py: 8,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
mb: 4,
|
||||
fontWeight: 700,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Créer un compte
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSignup}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 3,
|
||||
p: 4,
|
||||
borderRadius: '16px',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Confirmer le mot de passe"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
backgroundColor: '#007AFF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#0062cc',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isLoading ? 'Inscription...' : 'S\'inscrire'}
|
||||
</Button>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
color: '#86868B',
|
||||
}}
|
||||
>
|
||||
Déjà un compte ?{' '}
|
||||
<MuiLink
|
||||
component={Link}
|
||||
href="/login"
|
||||
sx={{
|
||||
color: '#007AFF',
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Se connecter
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
64
src/app/theme.ts
Normal file
64
src/app/theme.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#007AFF',
|
||||
light: '#4da3ff',
|
||||
dark: '#0062cc',
|
||||
},
|
||||
secondary: {
|
||||
main: '#86868B',
|
||||
light: '#a3a3a7',
|
||||
dark: '#6a6a6e',
|
||||
},
|
||||
background: {
|
||||
default: '#FFFFFF',
|
||||
paper: '#F5F5F7',
|
||||
},
|
||||
text: {
|
||||
primary: '#000000',
|
||||
secondary: '#86868B',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"SF Pro Display", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
h1: {
|
||||
fontSize: '3rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: '2.25rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
body1: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
borderRadius: '8px',
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
color: '#000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
58
src/components/AuthGuard.tsx
Normal file
58
src/components/AuthGuard.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter();
|
||||
const { user, setUser, isLoading, setIsLoading } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
const checkUser = async () => {
|
||||
try {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
setUser(session?.user ?? null);
|
||||
} catch (error) {
|
||||
console.error('Error checking session:', error);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkUser();
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [setUser, setIsLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
98
src/components/auth/LoginForm.tsx
Normal file
98
src/components/auth/LoginForm.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
TextField,
|
||||
Typography,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function LoginForm() {
|
||||
const { signIn } = useAuth();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await signIn(email, password);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Une erreur est survenue');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
sx={{
|
||||
maxWidth: 400,
|
||||
mx: 'auto',
|
||||
mt: 4,
|
||||
p: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" component="h1" gutterBottom align="center">
|
||||
Connexion
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : 'Se connecter'}
|
||||
</Button>
|
||||
|
||||
<Typography variant="body2" align="center" sx={{ mt: 2 }}>
|
||||
Pas encore de compte ?{' '}
|
||||
<Link href="/auth/register" passHref>
|
||||
<Button variant="text" color="primary">
|
||||
S'inscrire
|
||||
</Button>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
29
src/components/auth/LogoutButton.tsx
Normal file
29
src/components/auth/LogoutButton.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
'use client';
|
||||
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Button } from '@mui/material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function LogoutButton() {
|
||||
const { signOut } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await signOut();
|
||||
router.push('/auth/login');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la déconnexion:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Déconnexion
|
||||
</Button>
|
||||
);
|
||||
}
|
139
src/components/auth/RegisterForm.tsx
Normal file
139
src/components/auth/RegisterForm.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
TextField,
|
||||
Typography,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function RegisterForm() {
|
||||
const { signUp } = useAuth();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Les mots de passe ne correspondent pas');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await signUp(email, password);
|
||||
setSuccess(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Une erreur est survenue');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 400,
|
||||
mx: 'auto',
|
||||
mt: 4,
|
||||
p: 3,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
Un email de confirmation a été envoyé à {email}
|
||||
</Alert>
|
||||
<Link href="/auth/login" passHref>
|
||||
<Button variant="contained" color="primary">
|
||||
Retour à la connexion
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
sx={{
|
||||
maxWidth: 400,
|
||||
mx: 'auto',
|
||||
mt: 4,
|
||||
p: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" component="h1" gutterBottom align="center">
|
||||
Inscription
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Confirmer le mot de passe"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : "S'inscrire"}
|
||||
</Button>
|
||||
|
||||
<Typography variant="body2" align="center" sx={{ mt: 2 }}>
|
||||
Déjà un compte ?{' '}
|
||||
<Link href="/auth/login" passHref>
|
||||
<Button variant="text" color="primary">
|
||||
Se connecter
|
||||
</Button>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
74
src/contexts/AuthContext.tsx
Normal file
74
src/contexts/AuthContext.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
type AuthContextType = {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<void>;
|
||||
signUp: (email: string, password: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
user: null,
|
||||
loading: true,
|
||||
signIn: async () => {},
|
||||
signUp: async () => {},
|
||||
signOut: async () => {},
|
||||
});
|
||||
|
||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Vérifier la session active
|
||||
const checkSession = async () => {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkSession();
|
||||
|
||||
// Écouter les changements d'authentification
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) throw error;
|
||||
};
|
||||
|
||||
const signUp = async (email: string, password: string) => {
|
||||
const { error } = await supabase.auth.signUp({ email, password });
|
||||
if (error) throw error;
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) throw error;
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, signIn, signUp, signOut }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
6
src/lib/supabase.ts
Normal file
6
src/lib/supabase.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:8000';
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'your-anon-key';
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
16
src/store/auth.ts
Normal file
16
src/store/auth.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { create } from 'zustand';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
setUser: (user: User | null) => void;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
setUser: (user) => set({ user }),
|
||||
isLoading: true,
|
||||
setIsLoading: (isLoading) => set({ isLoading }),
|
||||
}));
|
16
start-supabase.sh
Executable file
16
start-supabase.sh
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Start Supabase services
|
||||
docker-compose up -d
|
||||
|
||||
# Wait for services to be ready
|
||||
echo "Waiting for services to be ready..."
|
||||
sleep 10
|
||||
|
||||
# Initialize the database
|
||||
echo "Initializing database..."
|
||||
docker exec -i supabase-db psql -U postgres -d postgres < init.sql
|
||||
|
||||
echo "Supabase is ready!"
|
||||
echo "Studio: http://localhost:3000"
|
||||
echo "API: http://localhost:3001"
|
1
supabase
Submodule
1
supabase
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 84914090f9c06973b788a63edf8a3e14ae2a77fc
|
147
wireframes.md
Normal file
147
wireframes.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Maquettes - Phase 1
|
||||
|
||||
## Page d'Accueil
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Logo Inscription Connexion |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Trouvez votre prochain emploi tech |
|
||||
| avec une approche intelligente |
|
||||
| |
|
||||
| [Rechercher des offres] |
|
||||
| |
|
||||
| +----------------------------------+ |
|
||||
| | Découvrez comment nous | |
|
||||
| | révolutionnons le recrutement | |
|
||||
| +----------------------------------+ |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Page d'Inscription
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Logo Retour |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Créez votre compte |
|
||||
| |
|
||||
| [Email] |
|
||||
| [Mot de passe] |
|
||||
| [Confirmer le mot de passe] |
|
||||
| |
|
||||
| [Créer mon compte] |
|
||||
| |
|
||||
| Déjà un compte ? [Se connecter] |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Page de Connexion
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Logo Retour |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Connectez-vous |
|
||||
| |
|
||||
| [Email] |
|
||||
| [Mot de passe] |
|
||||
| |
|
||||
| [Se connecter] |
|
||||
| |
|
||||
| Mot de passe oublié ? |
|
||||
| |
|
||||
| Pas encore de compte ? [S'inscrire] |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Formulaire de Profil
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Logo Menu |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Complétez votre profil |
|
||||
| |
|
||||
| Informations personnelles |
|
||||
| [Nom] |
|
||||
| [Prénom] |
|
||||
| [Email] |
|
||||
| |
|
||||
| Compétences |
|
||||
| [Ajouter une compétence] |
|
||||
| - Compétence 1 |
|
||||
| - Compétence 2 |
|
||||
| |
|
||||
| Expérience |
|
||||
| [Niveau d'expérience] |
|
||||
| [Type de poste recherché] |
|
||||
| [Localisation souhaitée] |
|
||||
| |
|
||||
| [Enregistrer mon profil] |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Page de Recherche
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Logo Profil Déconnexion |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| [Rechercher...] |
|
||||
| |
|
||||
| Filtres |
|
||||
| [Localisation] |
|
||||
| [Type de contrat] |
|
||||
| [Niveau d'expérience] |
|
||||
| |
|
||||
| Résultats |
|
||||
| +----------------------------------+ |
|
||||
| | Développeur Full Stack | |
|
||||
| | Paris - CDI | |
|
||||
| | Score de compatibilité: 85% | |
|
||||
| +----------------------------------+ |
|
||||
| |
|
||||
| +----------------------------------+ |
|
||||
| | UX Designer | |
|
||||
| | Lyon - CDI | |
|
||||
| | Score de compatibilité: 72% | |
|
||||
| +----------------------------------+ |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Page de Détail d'Offre
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Logo Retour |
|
||||
+------------------------------------------+
|
||||
| |
|
||||
| Développeur Full Stack |
|
||||
| Paris - CDI |
|
||||
| |
|
||||
| Score de compatibilité: 85% |
|
||||
| |
|
||||
| Description |
|
||||
| Lorem ipsum dolor sit amet... |
|
||||
| |
|
||||
| Compétences requises |
|
||||
| - JavaScript |
|
||||
| - React |
|
||||
| - Node.js |
|
||||
| |
|
||||
| [Postuler] |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
## Notes de Design
|
||||
- Utilisation de la palette de couleurs définie dans le design system
|
||||
- Espacement cohérent selon les guidelines
|
||||
- Typographie SF Pro pour tous les textes
|
||||
- Animations fluides sur les interactions
|
||||
- Design responsive adapté à tous les écrans
|
Loading…
Add table
Add a link
Reference in a new issue