Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,5 +1,4 @@
|
|||||||
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
production: {
|
production: {
|
||||||
@ -13,10 +12,10 @@ module.exports = {
|
|||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
},
|
},
|
||||||
development: {
|
development: {
|
||||||
username: process.env.DB_USER || 'postgres',
|
username: 'postgres',
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
password: process.env.DB_PASS || '',
|
password: '',
|
||||||
database: process.env.DB_NAME || 'db_site_recrutement_ats',
|
database: 'db_site_recrutement_ats',
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
logging: console.log,
|
logging: console.log,
|
||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -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/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
|
||||||
|
|
||||||
app.use('/api/job_offers', job_offersRoutes);
|
app.use('/api/job_offers', passport.authenticate('jwt', {session: false}), job_offersRoutes);
|
||||||
|
|
||||||
app.use('/api/applications', passport.authenticate('jwt', {session: false}), applicationsRoutes);
|
app.use('/api/applications', passport.authenticate('jwt', {session: false}), applicationsRoutes);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload, mdiBriefcaseVariantOutline } from '@mdi/js'
|
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement, useEffect, useState } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -12,74 +12,389 @@ import FormField from '../../components/FormField'
|
|||||||
import BaseDivider from '../../components/BaseDivider'
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
|
import FormCheckRadio from '../../components/FormCheckRadio'
|
||||||
|
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
import FormFilePicker from '../../components/FormFilePicker'
|
||||||
|
import FormImagePicker from '../../components/FormImagePicker'
|
||||||
|
import { SwitchField } from '../../components/SwitchField'
|
||||||
|
|
||||||
import { SelectField } from '../../components/SelectField'
|
import { SelectField } from '../../components/SelectField'
|
||||||
|
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
import {RichTextField} from "../../components/RichTextField";
|
||||||
|
|
||||||
import { create } from '../../stores/applications/applicationsSlice'
|
import { create } from '../../stores/applications/applicationsSlice'
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
import { useAppDispatch } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import axios from 'axios'
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
job_offer: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
candidate: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
status: 'submitted',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
applied_at: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
status_changed_at: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cover_letter: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
attachment_file: [],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
notes: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
rating: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const ApplicationsNew = () => {
|
const ApplicationsNew = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
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) => {
|
const handleSubmit = async (data) => {
|
||||||
const payload = {
|
await dispatch(create(data))
|
||||||
...data,
|
await router.push('/applications/applications-list')
|
||||||
job_offer: jobId || data.job_offer,
|
|
||||||
candidate: currentUser?.id,
|
|
||||||
applied_at: new Date().toISOString(),
|
|
||||||
status: 'submitted'
|
|
||||||
}
|
}
|
||||||
await dispatch(create(payload))
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Nouvelle Candidature')}</title>
|
<title>{getPageTitle('New Item')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiBriefcaseVariantOutline} title={job ? `Postuler pour : ${job.title}` : "Nouvelle Candidature"} main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={
|
||||||
job_offer: jobId || '',
|
|
||||||
candidate: currentUser?.id || '',
|
initialValues
|
||||||
status: 'submitted',
|
|
||||||
cover_letter: '',
|
}
|
||||||
attachment_file: [],
|
|
||||||
notes: '',
|
|
||||||
rating: 0
|
|
||||||
}}
|
|
||||||
enableReinitialize
|
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
{!jobId && (
|
|
||||||
<FormField label="Offre d'emploi" labelFor="job_offer">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField label="JobOffer" labelFor="job_offer">
|
||||||
<Field name="job_offer" id="job_offer" component={SelectField} options={[]} itemRef={'job_offers'}></Field>
|
<Field name="job_offer" id="job_offer" component={SelectField} options={[]} itemRef={'job_offers'}></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField label='Lettre de motivation' hasTextareaHeight>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
<Field
|
<Field
|
||||||
name='cover_letter'
|
name='cover_letter'
|
||||||
id='cover_letter'
|
id='cover_letter'
|
||||||
@ -87,41 +402,147 @@ const ApplicationsNew = () => {
|
|||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="CV / Pièces jointes">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
label='Télécharger mon CV'
|
label='AttachmentFile'
|
||||||
color='info'
|
color='info'
|
||||||
icon={mdiUpload}
|
icon={mdiUpload}
|
||||||
path={'applications/attachment_file'}
|
path={'applications/attachment_file'}
|
||||||
name='attachment_file'
|
name='attachment_file'
|
||||||
id='attachment_file'
|
id='attachment_file'
|
||||||
|
schema={{
|
||||||
|
size: undefined,
|
||||||
|
formats: undefined,
|
||||||
|
}}
|
||||||
component={FormFilePicker}
|
component={FormFilePicker}
|
||||||
></Field>
|
></Field>
|
||||||
</FormField>
|
</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 />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Envoyer ma candidature" />
|
<BaseButton type="submit" color="info" label="Submit" />
|
||||||
<BaseButton type='reset' color='danger' outline label='Annuler' onClick={() => router.back()}/>
|
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||||
|
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/applications/applications-list')}/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</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'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>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -129,7 +550,11 @@ const ApplicationsNew = () => {
|
|||||||
|
|
||||||
ApplicationsNew.getLayout = function getLayout(page: ReactElement) {
|
ApplicationsNew.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutAuthenticated permission={'CREATE_APPLICATIONS'}>
|
<LayoutAuthenticated
|
||||||
|
|
||||||
|
permission={'CREATE_APPLICATIONS'}
|
||||||
|
|
||||||
|
>
|
||||||
{page}
|
{page}
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,123 +1,166 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import axios from 'axios';
|
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import SectionFullScreen from '../components/SectionFullScreen';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import SectionMain from '../components/SectionMain';
|
import BaseDivider from '../components/BaseDivider';
|
||||||
|
import BaseButtons from '../components/BaseButtons';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { mdiBriefcaseVariantOutline, mdiMapMarkerOutline, mdiCurrencyUsd, mdiClockOutline } from '@mdi/js';
|
import { useAppSelector } from '../stores/hooks';
|
||||||
import BaseIcon from '../components/BaseIcon';
|
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||||
|
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
const fetchJobs = async () => {
|
async function fetchData() {
|
||||||
try {
|
const image = await getPexelsImage();
|
||||||
const response = await axios.get('/job_offers');
|
const video = await getPexelsVideo();
|
||||||
setJobs(response.data?.rows || []);
|
setIllustrationImage(image);
|
||||||
} catch (error) {
|
setIllustrationVideo(video);
|
||||||
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 (
|
return (
|
||||||
<div className="bg-slate-50 min-h-screen">
|
<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',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Accueil')}</title>
|
<title>{getPageTitle('Starter Page')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
{/* Hero Section */}
|
<SectionFullScreen bg='violet'>
|
||||||
<div className="bg-indigo-700 py-20 px-6 text-white text-center">
|
<div
|
||||||
<h1 className="text-4xl md:text-6xl font-extrabold mb-4">
|
className={`flex ${
|
||||||
Trouvez votre prochain job de rêve
|
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||||
</h1>
|
} min-h-screen w-full`}
|
||||||
<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.
|
{contentType === 'image' && contentPosition !== 'background'
|
||||||
</p>
|
? imageBlock(illustrationImage)
|
||||||
<div className="flex justify-center space-x-4">
|
: null}
|
||||||
<BaseButton label="Parcourir les offres" color="white" href="#jobs" />
|
{contentType === 'video' && contentPosition !== 'background'
|
||||||
<BaseButton label="Recruteur ? Publiez une offre" color="info" href="/login" outline />
|
? videoBlock(illustrationVideo)
|
||||||
</div>
|
: 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>
|
</div>
|
||||||
|
|
||||||
{/* Jobs Section */}
|
<BaseButtons>
|
||||||
<SectionMain id="jobs">
|
<BaseButton
|
||||||
<div className="mb-12 text-center">
|
href='/login'
|
||||||
<h2 className="text-3xl font-bold text-slate-800">Offres d'emploi récentes</h2>
|
label='Login'
|
||||||
<p className="text-slate-500 mt-2">Rejoignez les meilleures entreprises</p>
|
color='info'
|
||||||
</div>
|
className='w-full'
|
||||||
|
/>
|
||||||
|
|
||||||
{loading ? (
|
</BaseButtons>
|
||||||
<div className="flex justify-center py-20">
|
</CardBox>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
</SectionFullScreen>
|
||||||
<Link href={`/job_offers/${job.id}/public`} className="mt-auto">
|
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||||
<div className="w-full text-center py-3 bg-slate-800 text-white rounded-xl font-semibold hover:bg-slate-900 transition-colors">
|
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||||
Voir les détails
|
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||||
</div>
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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'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'utilisation</Link>
|
|
||||||
<Link href="/login">Espace Recruteur</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Home.getLayout = function getLayout(page: ReactElement) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,178 +0,0 @@
|
|||||||
|
|
||||||
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'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'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>;
|
|
||||||
};
|
|
||||||
@ -65,7 +65,7 @@ export default function Login() {
|
|||||||
// Redirect to dashboard if user is logged in
|
// Redirect to dashboard if user is logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser?.id) {
|
if (currentUser?.id) {
|
||||||
router.push(router.query.redirect as string || '/dashboard');
|
router.push('/dashboard');
|
||||||
}
|
}
|
||||||
}, [currentUser?.id, router]);
|
}, [currentUser?.id, router]);
|
||||||
// Show error message if there is one
|
// Show error message if there is one
|
||||||
|
|||||||
@ -18,39 +18,32 @@ import axios from "axios";
|
|||||||
export default function Register() {
|
export default function Register() {
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { redirect } = router.query;
|
|
||||||
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
|
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (value) => {
|
const handleSubmit = async (value) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await axios.post('/auth/signup', value);
|
|
||||||
notify('success', 'Inscription réussie ! Veuillez vous connecter.');
|
const { data: response } = await axios.post('/auth/signup',value);
|
||||||
setTimeout(() => {
|
await router.push('/login')
|
||||||
router.push(`/login${redirect ? `?redirect=${redirect}` : ''}`);
|
setLoading(false)
|
||||||
}, 2000);
|
notify('success', 'Please check your email for verification link')
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
console.log('error: ', error)
|
console.log('error: ', error)
|
||||||
const message = error.response?.data || 'Une erreur est survenue.';
|
notify('error', 'Something was wrong. Try again')
|
||||||
notify('error', message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Inscription')}</title>
|
<title>{getPageTitle('Login')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<SectionFullScreen bg='violet'>
|
||||||
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
|
<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
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
email: '',
|
email: '',
|
||||||
@ -61,13 +54,13 @@ export default function Register() {
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
|
|
||||||
<FormField label='Email' help='Entrez votre adresse email'>
|
<FormField label='Email' help='Please enter your email'>
|
||||||
<Field type='email' name='email' placeholder="exemple@email.com" />
|
<Field type='email' name='email' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='Mot de passe' help='Choisissez un mot de passe sécurisé'>
|
<FormField label='Password' help='Please enter your password'>
|
||||||
<Field type='password' name='password' />
|
<Field type='password' name='password' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label='Confirmez le mot de passe' help='Répétez votre mot de passe'>
|
<FormField label='Confirm Password' help='Please confirm your password'>
|
||||||
<Field type='password' name='confirm' />
|
<Field type='password' name='confirm' />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -76,18 +69,15 @@ export default function Register() {
|
|||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
type='submit'
|
type='submit'
|
||||||
label={loading ? 'Chargement...' : 'S\'inscrire' }
|
label={loading ? 'Loading...' : 'Register' }
|
||||||
|
color='info'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
href={'/login'}
|
||||||
|
label={'Login'}
|
||||||
color='info'
|
color='info'
|
||||||
className="w-full"
|
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</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>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
@ -97,8 +87,6 @@ export default function Register() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
Register.getLayout = function getLayout(page: ReactElement) {
|
Register.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user