Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
a8e0d9a74d v1 2026-02-08 22:41:54 +00:00
8 changed files with 446 additions and 686 deletions

View File

@ -1,4 +1,5 @@
require('dotenv').config();
module.exports = {
production: {
@ -12,10 +13,10 @@ module.exports = {
seederStorage: 'sequelize',
},
development: {
username: 'postgres',
username: process.env.DB_USER || 'postgres',
dialect: 'postgres',
password: '',
database: 'db_site_recrutement_ats',
password: process.env.DB_PASS || '',
database: process.env.DB_NAME || 'db_site_recrutement_ats',
host: process.env.DB_HOST || 'localhost',
logging: console.log,
seederStorage: 'sequelize',

View File

@ -0,0 +1,37 @@
module.exports = {
async up(queryInterface, Sequelize) {
const [roles] = await queryInterface.sequelize.query(
`SELECT id FROM "roles" WHERE name = 'Public' LIMIT 1;`
);
const [permissions] = await queryInterface.sequelize.query(
`SELECT id FROM "permissions" WHERE name = 'READ_JOB_OFFERS' LIMIT 1;`
);
if (roles.length && permissions.length) {
await queryInterface.bulkInsert('rolesPermissionsPermissions', [
{
roles_permissionsId: roles[0].id,
permissionId: permissions[0].id,
createdAt: new Date(),
updatedAt: new Date(),
},
]);
}
},
async down(queryInterface, Sequelize) {
const [roles] = await queryInterface.sequelize.query(
`SELECT id FROM "roles" WHERE name = 'Public' LIMIT 1;`
);
const [permissions] = await queryInterface.sequelize.query(
`SELECT id FROM "permissions" WHERE name = 'READ_JOB_OFFERS' LIMIT 1;`
);
if (roles.length && permissions.length) {
await queryInterface.bulkDelete('rolesPermissionsPermissions', {
roles_permissionsId: roles[0].id,
permissionId: permissions[0].id,
});
}
},
};

View File

@ -95,7 +95,7 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
app.use('/api/job_offers', passport.authenticate('jwt', {session: false}), job_offersRoutes);
app.use('/api/job_offers', job_offersRoutes);
app.use('/api/applications', passport.authenticate('jwt', {session: false}), applicationsRoutes);

View File

@ -1,6 +1,6 @@
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload, mdiBriefcaseVariantOutline } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement } from 'react'
import React, { ReactElement, useEffect, useState } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
@ -12,389 +12,74 @@ import FormField from '../../components/FormField'
import BaseDivider from '../../components/BaseDivider'
import BaseButtons from '../../components/BaseButtons'
import BaseButton from '../../components/BaseButton'
import FormCheckRadio from '../../components/FormCheckRadio'
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
import FormFilePicker from '../../components/FormFilePicker'
import FormImagePicker from '../../components/FormImagePicker'
import { SwitchField } from '../../components/SwitchField'
import { SelectField } from '../../components/SelectField'
import { SelectFieldMany } from "../../components/SelectFieldMany";
import {RichTextField} from "../../components/RichTextField";
import { create } from '../../stores/applications/applicationsSlice'
import { useAppDispatch } from '../../stores/hooks'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
import { useRouter } from 'next/router'
import moment from 'moment';
const initialValues = {
job_offer: '',
candidate: '',
status: 'submitted',
applied_at: '',
status_changed_at: '',
cover_letter: '',
attachment_file: [],
notes: '',
rating: '',
}
import axios from 'axios'
const ApplicationsNew = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const { jobId } = router.query
const { currentUser } = useAppSelector((state) => state.auth)
const [job, setJob] = useState<any>(null)
useEffect(() => {
if (jobId) {
axios.get(`/job_offers/${jobId}`).then(res => setJob(res.data)).catch(err => console.error(err))
}
}, [jobId])
const handleSubmit = async (data) => {
await dispatch(create(data))
await router.push('/applications/applications-list')
const payload = {
...data,
job_offer: jobId || data.job_offer,
candidate: currentUser?.id,
applied_at: new Date().toISOString(),
status: 'submitted'
}
await dispatch(create(payload))
await router.push('/')
}
return (
<>
<Head>
<title>{getPageTitle('New Item')}</title>
<title>{getPageTitle('Nouvelle Candidature')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
<SectionTitleLineWithButton icon={mdiBriefcaseVariantOutline} title={job ? `Postuler pour : ${job.title}` : "Nouvelle Candidature"} main>
{''}
</SectionTitleLineWithButton>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<CardBox>
<Formik
initialValues={
initialValues
}
initialValues={{
job_offer: jobId || '',
candidate: currentUser?.id || '',
status: 'submitted',
cover_letter: '',
attachment_file: [],
notes: '',
rating: 0
}}
enableReinitialize
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label="JobOffer" labelFor="job_offer">
{!jobId && (
<FormField label="Offre d&apos;emploi" labelFor="job_offer">
<Field name="job_offer" id="job_offer" component={SelectField} options={[]} itemRef={'job_offers'}></Field>
</FormField>
)}
<FormField label="Candidate" labelFor="candidate">
<Field name="candidate" id="candidate" component={SelectField} options={[]} itemRef={'users'}></Field>
</FormField>
<FormField label="Status" labelFor="status">
<Field name="status" id="status" component="select">
<option value="submitted">submitted</option>
<option value="in_review">in_review</option>
<option value="shortlisted">shortlisted</option>
<option value="interview">interview</option>
<option value="offer">offer</option>
<option value="hired">hired</option>
<option value="rejected">rejected</option>
<option value="withdrawn">withdrawn</option>
</Field>
</FormField>
<FormField
label="AppliedAt"
>
<Field
type="datetime-local"
name="applied_at"
placeholder="AppliedAt"
/>
</FormField>
<FormField
label="StatusChangedAt"
>
<Field
type="datetime-local"
name="status_changed_at"
placeholder="StatusChangedAt"
/>
</FormField>
<FormField label='CoverLetter' hasTextareaHeight>
<FormField label='Lettre de motivation' hasTextareaHeight>
<Field
name='cover_letter'
id='cover_letter'
@ -402,147 +87,41 @@ const ApplicationsNew = () => {
></Field>
</FormField>
<FormField>
<FormField label="CV / Pièces jointes">
<Field
label='AttachmentFile'
label='Télécharger mon CV'
color='info'
icon={mdiUpload}
path={'applications/attachment_file'}
name='attachment_file'
id='attachment_file'
schema={{
size: undefined,
formats: undefined,
}}
component={FormFilePicker}
></Field>
</FormField>
<FormField label='Notes' hasTextareaHeight>
<Field
name='notes'
id='notes'
component={RichTextField}
></Field>
</FormField>
<FormField
label="Rating"
>
<Field
type="number"
name="rating"
placeholder="Rating"
/>
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/applications/applications-list')}/>
<BaseButton type="submit" color="info" label="Envoyer ma candidature" />
<BaseButton type='reset' color='danger' outline label='Annuler' onClick={() => router.back()}/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</div>
<div className="lg:col-span-1">
{job && (
<div className="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<h3 className="font-bold text-slate-800 mb-2">Récapitulatif de l&apos;offre</h3>
<p className="text-indigo-600 font-medium mb-4">{job.company_name}</p>
<div className="text-sm text-slate-500 space-y-1">
<p>Lieu : {job.city}, {job.country}</p>
<p>Contrat : {job.contract_type?.replace('_', ' ')}</p>
</div>
</div>
)}
</div>
</div>
</SectionMain>
</>
)
@ -550,11 +129,7 @@ const ApplicationsNew = () => {
ApplicationsNew.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated
permission={'CREATE_APPLICATIONS'}
>
<LayoutAuthenticated permission={'CREATE_APPLICATIONS'}>
{page}
</LayoutAuthenticated>
)

View File

@ -1,166 +1,123 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import axios from 'axios';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import { mdiBriefcaseVariantOutline, mdiMapMarkerOutline, mdiCurrencyUsd, mdiClockOutline } from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
export default function Home() {
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(true);
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Site Recrutement ATS'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
const fetchJobs = async () => {
try {
const response = await axios.get('/job_offers');
setJobs(response.data?.rows || []);
} catch (error) {
console.error('Error fetching jobs:', error);
} finally {
setLoading(false);
}
fetchData();
};
fetchJobs();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="bg-slate-50 min-h-screen">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Accueil')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Site Recrutement ATS app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
{/* Hero Section */}
<div className="bg-indigo-700 py-20 px-6 text-white text-center">
<h1 className="text-4xl md:text-6xl font-extrabold mb-4">
Trouvez votre prochain job de rêve
</h1>
<p className="text-xl md:text-2xl text-indigo-100 max-w-3xl mx-auto mb-8">
Découvrez des opportunités passionnantes et postulez en quelques clics.
</p>
<div className="flex justify-center space-x-4">
<BaseButton label="Parcourir les offres" color="white" href="#jobs" />
<BaseButton label="Recruteur ? Publiez une offre" color="info" href="/login" outline />
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
{/* Jobs Section */}
<SectionMain id="jobs">
<div className="mb-12 text-center">
<h2 className="text-3xl font-bold text-slate-800">Offres d&apos;emploi récentes</h2>
<p className="text-slate-500 mt-2">Rejoignez les meilleures entreprises</p>
</div>
{loading ? (
<div className="flex justify-center py-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
</div>
) : jobs.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{jobs.map((job: any) => (
<div key={job.id} className="bg-white rounded-2xl shadow-sm hover:shadow-md transition-shadow p-6 border border-slate-100 flex flex-col h-full">
<div className="flex items-center justify-between mb-4">
<div className="bg-indigo-50 p-3 rounded-xl">
<BaseIcon path={mdiBriefcaseVariantOutline} size={24} className="text-indigo-600" />
</div>
<span className="text-xs font-semibold px-2.5 py-0.5 rounded-full bg-green-100 text-green-800 uppercase">
{job.contract_type?.replace('_', ' ') || 'CDI'}
</span>
</div>
<h3 className="text-xl font-bold text-slate-800 mb-1">{job.title}</h3>
<p className="text-indigo-600 font-medium mb-4">{job.company_name}</p>
<div className="space-y-2 mb-6 flex-grow">
<div className="flex items-center text-slate-500 text-sm">
<BaseIcon path={mdiMapMarkerOutline} size={16} className="mr-2" />
{job.city}, {job.country}
</div>
<div className="flex items-center text-slate-500 text-sm">
<BaseIcon path={mdiCurrencyUsd} size={16} className="mr-2" />
{job.salary_min && job.salary_max ? `${job.salary_min} - ${job.salary_max} ${job.currency || '€'}` : 'Salaire non précisé'}
</div>
<div className="flex items-center text-slate-500 text-sm">
<BaseIcon path={mdiClockOutline} size={16} className="mr-2" />
{job.workplace_type || 'On-site'}
</div>
</div>
<Link href={`/job_offers/${job.id}/public`} className="mt-auto">
<div className="w-full text-center py-3 bg-slate-800 text-white rounded-xl font-semibold hover:bg-slate-900 transition-colors">
Voir les détails
</div>
</Link>
</div>
))}
</div>
) : (
<div className="text-center py-20 bg-white rounded-3xl border border-dashed border-slate-300">
<p className="text-slate-500">Aucune offre d&apos;emploi disponible pour le moment.</p>
<BaseButton label="Revenir plus tard" color="info" className="mt-4" outline />
</div>
)}
</SectionMain>
{/* Footer-ish section */}
<div className="bg-slate-900 text-white py-12 px-6 text-center">
<p className="mb-4">© 2026 Site Recrutement ATS. Tous droits réservés.</p>
<div className="flex justify-center space-x-6 text-sm text-slate-400">
<Link href="/privacy-policy">Politique de confidentialité</Link>
<Link href="/terms-of-use">Conditions d&apos;utilisation</Link>
<Link href="/login">Espace Recruteur</Link>
</div>
</div>
</div>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,178 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import axios from 'axios';
import BaseButton from '../../../components/BaseButton';
import BaseDivider from '../../../components/BaseDivider';
import LayoutGuest from '../../../layouts/Guest';
import SectionMain from '../../../components/SectionMain';
import { getPageTitle } from '../../../config';
import { mdiMapMarkerOutline, mdiCurrencyUsd, mdiClockOutline, mdiArrowLeft, mdiOfficeBuildingOutline } from '@mdi/js';
import BaseIcon from '../../../components/BaseIcon';
import { useAppSelector } from '../../../stores/hooks';
export default function JobPublicView() {
const router = useRouter();
const { job_offersId } = router.query;
const [job, setJob] = useState<any>(null);
const [loading, setLoading] = useState(true);
const { currentUser } = useAppSelector((state) => state.auth);
useEffect(() => {
if (job_offersId) {
const fetchJob = async () => {
try {
const response = await axios.get(`/job_offers/${job_offersId}`);
setJob(response.data);
} catch (error) {
console.error('Error fetching job:', error);
} finally {
setLoading(false);
}
};
fetchJob();
}
}, [job_offersId]);
const handleApply = () => {
if (!currentUser) {
router.push(`/register?redirect=/job_offers/${job_offersId}/public&apply=true`);
} else {
router.push(`/applications/new?jobId=${job_offersId}`);
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
</div>
);
}
if (!job) {
return (
<SectionMain>
<div className="text-center py-20">
<h2 className="text-2xl font-bold text-slate-800">Offre non trouvée</h2>
<BaseButton label="Retour à l&apos;accueil" color="info" className="mt-4" onClick={() => router.push('/')} />
</div>
</SectionMain>
);
}
return (
<div className="bg-slate-50 min-h-screen">
<Head>
<title>{getPageTitle(job.title)}</title>
</Head>
<div className="bg-white border-b border-slate-200 py-4 px-6 sticky top-0 z-10">
<div className="max-w-5xl mx-auto flex items-center justify-between">
<button onClick={() => router.push('/')} className="flex items-center text-slate-600 hover:text-indigo-600 transition-colors">
<BaseIcon path={mdiArrowLeft} size={20} className="mr-2" />
Retour aux offres
</button>
<BaseButton label="Postuler maintenant" color="info" onClick={handleApply} />
</div>
</div>
<SectionMain className="max-w-5xl mx-auto py-12">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Main Content */}
<div className="lg:col-span-2 space-y-8">
<div className="bg-white rounded-3xl p-8 shadow-sm border border-slate-100">
<h1 className="text-3xl md:text-4xl font-extrabold text-slate-800 mb-4">{job.title}</h1>
<div className="flex flex-wrap gap-4 mb-8">
<span className="px-3 py-1 bg-indigo-50 text-indigo-700 rounded-full text-sm font-semibold">
{job.contract_type?.replace('_', ' ').toUpperCase() || 'CDI'}
</span>
<span className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm font-semibold">
{job.experience_level?.toUpperCase() || 'JUNIOR'}
</span>
<span className="px-3 py-1 bg-green-50 text-green-700 rounded-full text-sm font-semibold">
{job.workplace_type || 'On-site'}
</span>
</div>
<div className="prose prose-slate max-w-none">
<h3 className="text-xl font-bold text-slate-800 mb-4">Description du poste</h3>
<div className="whitespace-pre-wrap text-slate-600 leading-relaxed mb-8">
{job.description || 'Aucune description fournie.'}
</div>
<h3 className="text-xl font-bold text-slate-800 mb-4">Profil recherché</h3>
<div className="whitespace-pre-wrap text-slate-600 leading-relaxed mb-8">
{job.requirements || 'Aucun pré-requis spécifié.'}
</div>
<h3 className="text-xl font-bold text-slate-800 mb-4">Avantages</h3>
<div className="whitespace-pre-wrap text-slate-600 leading-relaxed">
{job.benefits || 'Aucun avantage spécifié.'}
</div>
</div>
</div>
</div>
{/* Sidebar */}
<div className="space-y-6">
<div className="bg-white rounded-3xl p-6 shadow-sm border border-slate-100 sticky top-24">
<h3 className="text-lg font-bold text-slate-800 mb-6">Détails de l&apos;offre</h3>
<div className="space-y-4">
<div className="flex items-start">
<BaseIcon path={mdiOfficeBuildingOutline} size={20} className="text-slate-400 mr-3 mt-0.5" />
<div>
<p className="text-xs text-slate-400 uppercase font-bold tracking-wider">Entreprise</p>
<p className="text-slate-700 font-semibold">{job.company_name}</p>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiMapMarkerOutline} size={20} className="text-slate-400 mr-3 mt-0.5" />
<div>
<p className="text-xs text-slate-400 uppercase font-bold tracking-wider">Localisation</p>
<p className="text-slate-700 font-semibold">{job.city}, {job.country}</p>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiCurrencyUsd} size={20} className="text-slate-400 mr-3 mt-0.5" />
<div>
<p className="text-xs text-slate-400 uppercase font-bold tracking-wider">Salaire</p>
<p className="text-slate-700 font-semibold">
{job.salary_min && job.salary_max ? `${job.salary_min} - ${job.salary_max} ${job.currency || '€'}` : 'Non précisé'}
</p>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiClockOutline} size={20} className="text-slate-400 mr-3 mt-0.5" />
<div>
<p className="text-xs text-slate-400 uppercase font-bold tracking-wider">Type de contrat</p>
<p className="text-slate-700 font-semibold">{job.contract_type?.replace('_', ' ') || 'CDI'}</p>
</div>
</div>
</div>
<BaseDivider />
<BaseButton label="Postuler maintenant" color="info" className="w-full" onClick={handleApply} />
<p className="text-center text-xs text-slate-400 mt-4">
Référence : {job.reference_code || 'N/A'}
</p>
</div>
</div>
</div>
</SectionMain>
</div>
);
}
JobPublicView.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -65,7 +65,7 @@ export default function Login() {
// Redirect to dashboard if user is logged in
useEffect(() => {
if (currentUser?.id) {
router.push('/dashboard');
router.push(router.query.redirect as string || '/dashboard');
}
}, [currentUser?.id, router]);
// Show error message if there is one

View File

@ -18,32 +18,39 @@ import axios from "axios";
export default function Register() {
const [loading, setLoading] = React.useState(false);
const router = useRouter();
const { redirect } = router.query;
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: response } = await axios.post('/auth/signup',value);
await router.push('/login')
setLoading(false)
notify('success', 'Please check your email for verification link')
} catch (error) {
await axios.post('/auth/signup', value);
notify('success', 'Inscription réussie ! Veuillez vous connecter.');
setTimeout(() => {
router.push(`/login${redirect ? `?redirect=${redirect}` : ''}`);
}, 2000);
} catch (error: any) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
const message = error.response?.data || 'Une erreur est survenue.';
notify('error', message);
}
};
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle('Inscription')}</title>
</Head>
<SectionFullScreen bg='violet'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
<div className="mb-6 text-center">
<h1 className="text-2xl font-bold text-slate-800">Rejoignez-nous</h1>
<p className="text-slate-500">Créez votre compte candidat</p>
</div>
<Formik
initialValues={{
email: '',
@ -54,13 +61,13 @@ export default function Register() {
>
<Form>
<FormField label='Email' help='Please enter your email'>
<Field type='email' name='email' />
<FormField label='Email' help='Entrez votre adresse email'>
<Field type='email' name='email' placeholder="exemple@email.com" />
</FormField>
<FormField label='Password' help='Please enter your password'>
<FormField label='Mot de passe' help='Choisissez un mot de passe sécurisé'>
<Field type='password' name='password' />
</FormField>
<FormField label='Confirm Password' help='Please confirm your password'>
<FormField label='Confirmez le mot de passe' help='Répétez votre mot de passe'>
<Field type='password' name='confirm' />
</FormField>
@ -69,15 +76,18 @@ export default function Register() {
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
label={loading ? 'Chargement...' : 'S\'inscrire' }
color='info'
className="w-full"
/>
</BaseButtons>
<div className="mt-4 text-center text-sm text-slate-500">
Déjà un compte ?{' '}
<Link href={`/login${redirect ? `?redirect=${redirect}` : ''}`} className="text-indigo-600 font-semibold hover:underline">
Se connecter
</Link>
</div>
</Form>
</Formik>
</CardBox>
@ -87,6 +97,8 @@ export default function Register() {
);
}
import Link from 'next/link';
Register.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};