first commit

This commit is contained in:
el 2025-04-15 19:39:21 +02:00
parent b1fc9f95e4
commit 135afa6d19
27 changed files with 2798 additions and 134 deletions

76
design-system.md Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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
View 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.

View file

@ -0,0 +1,5 @@
import LoginForm from '@/components/auth/LoginForm';
export default function LoginPage() {
return <LoginForm />;
}

View file

@ -0,0 +1,5 @@
import RegisterForm from '@/components/auth/RegisterForm';
export default function RegisterPage() {
return <RegisterForm />;
}

View 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>
);
}

View 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>
);
}

View file

@ -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
View 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>
);
}

View file

@ -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
View 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
View 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
View 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;

View 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}</>;
}

View 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>
);
}

View 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>
);
}

View 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 é 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>
);
}

View 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
View 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
View 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
View 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

@ -0,0 +1 @@
Subproject commit 84914090f9c06973b788a63edf8a3e14ae2a77fc

147
wireframes.md Normal file
View 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