feat: implement candidate security honeypots and redesign authenticated layout
This commit is contained in:
84
resources/js/Pages/Public/Jobs/Index.vue
Normal file
84
resources/js/Pages/Public/Jobs/Index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
jobs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Offres d'emploi" />
|
||||
|
||||
<div class="min-h-screen bg-neutral text-anthracite font-sans">
|
||||
<!-- Navigation Bar -->
|
||||
<nav class="bg-primary shadow-lg p-6">
|
||||
<div class="max-w-4xl mx-auto flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white rounded-lg flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9l-.707.707M12 18v3m4.95-4.95l.707.707M12 3c-4.418 0-8 3.582-8 8 0 2.209.895 4.209 2.343 5.657L12 21l5.657-5.343A7.994 7.994 0 0020 11c0-4.418-3.582-8-8-8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-2xl font-serif font-bold text-white">RECRU<span class="text-highlight italic px-1">IT</span></span>
|
||||
</div>
|
||||
<div>
|
||||
<Link :href="route('login')" class="text-sm font-bold text-white hover:text-highlight transition-colors">
|
||||
Espace Recruteur
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-4xl mx-auto py-12 px-6">
|
||||
<div class="mb-10">
|
||||
<h1 class="text-4xl font-serif font-bold text-primary mb-4">Offres d'emploi disponibles</h1>
|
||||
<p class="text-lg text-anthracite/70">Découvrez nos opportunités et rejoignez-nous.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="jobs.length === 0" class="bg-white rounded-2xl shadow-sm p-12 text-center border border-anthracite/10">
|
||||
<div class="w-16 h-16 bg-neutral rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-anthracite/40" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-anthracite mb-2">Aucune offre pour le moment</h3>
|
||||
<p class="text-anthracite/60">Revenez plus tard pour découvrir nos futures opportunités.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 gap-6">
|
||||
<div v-for="job in jobs" :key="job.id" class="bg-white rounded-2xl shadow-sm hover:shadow-xl transition-all duration-300 overflow-hidden border border-anthracite/10 group flex flex-col sm:flex-row">
|
||||
<div class="p-8 flex-1">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span v-if="job.tenant" class="px-3 py-1 bg-highlight/20 text-[#3a2800] rounded-full text-xs font-bold uppercase tracking-wider">
|
||||
{{ job.tenant.name }}
|
||||
</span>
|
||||
<span class="text-xs font-bold text-anthracite/50 uppercase tracking-widest">Temps plein</span>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold font-serif text-primary group-hover:text-highlight transition-colors mb-4">
|
||||
{{ job.title }}
|
||||
</h2>
|
||||
<p class="text-anthracite/70 text-sm line-clamp-2 mb-6 leading-relaxed">
|
||||
{{ job.description }}
|
||||
</p>
|
||||
|
||||
<div v-if="job.requirements && job.requirements.length > 0" class="flex flex-wrap gap-2 mb-6">
|
||||
<span v-for="(req, i) in job.requirements.slice(0, 3)" :key="i" class="px-2 py-1 bg-neutral rounded-md text-xs text-anthracite/60 font-medium">
|
||||
{{ req }}
|
||||
</span>
|
||||
<span v-if="job.requirements.length > 3" class="px-2 py-1 bg-neutral rounded-md text-xs text-anthracite/60 font-medium">
|
||||
+{{ job.requirements.length - 3 }} autres
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-neutral/50 p-6 sm:w-48 flex items-center justify-center border-t sm:border-t-0 sm:border-l border-anthracite/10">
|
||||
<Link :href="route('jobs.show', job.id)" class="w-full text-center py-3 px-4 bg-primary text-white rounded-xl font-bold font-subtitle uppercase tracking-wider text-xs hover:bg-primary/90 hover:shadow-lg transition-all">
|
||||
Voir l'offre
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
134
resources/js/Pages/Public/Jobs/Show.vue
Normal file
134
resources/js/Pages/Public/Jobs/Show.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup>
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
jobPosition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
linkedin_url: '',
|
||||
city: '',
|
||||
cv: null,
|
||||
cover_letter: null,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('jobs.apply', props.jobPosition.id), {
|
||||
onSuccess: () => {
|
||||
// Success is handled by a redirect to dashboard and a flash message
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="'Postuler: ' + jobPosition.title" />
|
||||
|
||||
<div class="min-h-screen bg-neutral text-anthracite font-sans">
|
||||
<!-- Navigation Bar -->
|
||||
<nav class="bg-primary shadow-lg p-6">
|
||||
<div class="max-w-4xl mx-auto flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-white rounded-lg flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9l-.707.707M12 18v3m4.95-4.95l.707.707M12 3c-4.418 0-8 3.582-8 8 0 2.209.895 4.209 2.343 5.657L12 21l5.657-5.343A7.994 7.994 0 0020 11c0-4.418-3.582-8-8-8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-2xl font-serif font-bold text-white">RECRU<span class="text-highlight italic px-1">IT</span></span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-4xl mx-auto py-12 px-6">
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="bg-primary/5 border-b border-primary/10 px-8 py-10">
|
||||
<h1 class="text-3xl font-serif font-bold text-primary mb-2">{{ jobPosition.title }}</h1>
|
||||
<div class="flex items-center gap-4 text-sm text-anthracite/70">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
Offre d'emploi
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-8 grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<!-- Job Details -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold font-subtitle text-anthracite mb-3 border-b pb-2">Description du poste</h2>
|
||||
<div class="prose prose-sm prose-neutral text-anthracite/80 whitespace-pre-line">{{ jobPosition.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="jobPosition.requirements && jobPosition.requirements.length > 0">
|
||||
<h2 class="text-xl font-bold font-subtitle text-anthracite mb-3 border-b pb-2">Prérequis</h2>
|
||||
<ul class="list-disc list-inside text-anthracite/80 space-y-1">
|
||||
<li v-for="(req, i) in jobPosition.requirements" :key="i">{{ req }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Form -->
|
||||
<div class="bg-neutral/50 p-6 rounded-xl border border-anthracite/10">
|
||||
<h2 class="text-xl font-bold font-subtitle text-primary mb-6">Soumettre votre candidature</h2>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">Nom complet <span class="text-red-500">*</span></label>
|
||||
<input type="text" v-model="form.name" required class="w-full rounded-lg border-anthracite/20 focus:border-primary focus:ring-primary text-sm p-2.5" />
|
||||
<div v-if="form.errors.name" class="text-red-500 text-xs mt-1">{{ form.errors.name }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">Adresse Email <span class="text-red-500">*</span></label>
|
||||
<input type="email" v-model="form.email" required class="w-full rounded-lg border-anthracite/20 focus:border-primary focus:ring-primary text-sm p-2.5" />
|
||||
<div v-if="form.errors.email" class="text-red-500 text-xs mt-1">{{ form.errors.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">Téléphone</label>
|
||||
<input type="text" v-model="form.phone" class="w-full rounded-lg border-anthracite/20 focus:border-primary focus:ring-primary text-sm p-2.5" />
|
||||
<div v-if="form.errors.phone" class="text-red-500 text-xs mt-1">{{ form.errors.phone }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">Ville</label>
|
||||
<input type="text" v-model="form.city" class="w-full rounded-lg border-anthracite/20 focus:border-primary focus:ring-primary text-sm p-2.5" />
|
||||
<div v-if="form.errors.city" class="text-red-500 text-xs mt-1">{{ form.errors.city }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">URL LinkedIn</label>
|
||||
<input type="url" v-model="form.linkedin_url" placeholder="https://linkedin.com/in/..." class="w-full rounded-lg border-anthracite/20 focus:border-primary focus:ring-primary text-sm p-2.5" />
|
||||
<div v-if="form.errors.linkedin_url" class="text-red-500 text-xs mt-1">{{ form.errors.linkedin_url }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">CV (PDF) <span class="text-red-500">*</span></label>
|
||||
<input type="file" @input="form.cv = $event.target.files[0]" accept=".pdf" required class="w-full text-sm text-anthracite/70 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20" />
|
||||
<div v-if="form.errors.cv" class="text-red-500 text-xs mt-1">{{ form.errors.cv }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-anthracite mb-1">Lettre de motivation (PDF)</label>
|
||||
<input type="file" @input="form.cover_letter = $event.target.files[0]" accept=".pdf" class="w-full text-sm text-anthracite/70 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20" />
|
||||
<div v-if="form.errors.cover_letter" class="text-red-500 text-xs mt-1">{{ form.errors.cover_letter }}</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="form.processing" class="w-full mt-6 py-3 px-4 bg-highlight text-[#3a2800] rounded-xl font-bold font-subtitle uppercase tracking-wider text-sm hover:brightness-110 shadow-lg shadow-highlight/30 transition-all disabled:opacity-50 flex justify-center items-center">
|
||||
<span v-if="form.processing">Envoi en cours...</span>
|
||||
<span v-else>Postuler maintenant</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user