first commit
This commit is contained in:
parent
b1fc9f95e4
commit
135afa6d19
27 changed files with 2798 additions and 134 deletions
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>
|
||||
);
|
||||
}
|
291
src/app/page.tsx
291
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>
|
||||
|
||||
<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"
|
||||
<main>
|
||||
<Header />
|
||||
|
||||
<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 }),
|
||||
}));
|
Loading…
Add table
Add a link
Reference in a new issue