Initial commit

This commit is contained in:
jeremy bayse
2026-03-20 08:25:58 +01:00
commit a55a33ae2a
143 changed files with 19599 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
<script setup>
import AdminLayout from '@/Layouts/AdminLayout.vue';
import { Head, useForm, Link, usePage, router } from '@inertiajs/vue3';
import { ref, computed } from 'vue';
import Modal from '@/Components/Modal.vue';
import InputLabel from '@/Components/InputLabel.vue';
import TextInput from '@/Components/TextInput.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DangerButton from '@/Components/DangerButton.vue';
import InputError from '@/Components/InputError.vue';
const props = defineProps({
quizzes: Array
});
const page = usePage();
const flashSuccess = computed(() => page.props.flash?.success);
const isModalOpen = ref(false);
const editingQuiz = ref(null);
const form = useForm({
title: '',
description: '',
duration_minutes: 60,
});
const openEditModal = (quiz) => {
editingQuiz.value = quiz;
form.title = quiz.title;
form.description = quiz.description;
form.duration_minutes = quiz.duration_minutes;
isModalOpen.value = true;
};
const deleteQuiz = (id) => {
if (confirm('Voulez-vous vraiment supprimer ce quiz ? Toutes les questions et tentatives associées seront supprimées.')) {
router.delete(route('admin.quizzes.destroy', id));
}
};
const closeModal = () => {
isModalOpen.value = false;
editingQuiz.value = null;
form.reset();
};
const submit = () => {
if (editingQuiz.value) {
form.put(route('admin.quizzes.update', editingQuiz.value.id), {
onSuccess: () => closeModal(),
});
} else {
form.post(route('admin.quizzes.store'), {
onSuccess: () => closeModal(),
});
}
};
</script>
<template>
<Head title="Gestion des Quiz" />
<AdminLayout>
<template #header>
Gestion des Quiz
</template>
<!-- Flash Messages -->
<div v-if="flashSuccess" class="mb-8 p-6 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-2xl flex items-center gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
<div class="p-2 bg-emerald-500 rounded-lg text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p class="font-bold text-emerald-800 dark:text-emerald-400">Succès !</p>
<p class="text-emerald-700 dark:text-emerald-500 text-sm">{{ flashSuccess }}</p>
</div>
</div>
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-bold">Liste des Quiz</h3>
<PrimaryButton @click="isModalOpen = true">
Créer un Quiz
</PrimaryButton>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="quiz in quizzes" :key="quiz.id" class="bg-white dark:bg-slate-800 rounded-3xl shadow-sm border border-slate-200 dark:border-slate-700 flex flex-col h-full group hover:shadow-xl transition-all duration-300">
<div class="p-8 pb-4">
<div class="flex justify-between items-start mb-4">
<h4 class="text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-indigo-600 transition-colors">{{ quiz.title }}</h4>
<span class="px-3 py-1 bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-400 text-[10px] font-black uppercase tracking-widest rounded-full">
{{ quiz.duration_minutes }} min
</span>
</div>
<p class="text-slate-500 dark:text-slate-400 text-sm line-clamp-3">
{{ quiz.description || 'Aucune description' }}
</p>
</div>
<div class="mt-auto p-4 bg-slate-50/50 dark:bg-slate-900/30 border-t border-slate-100 dark:border-slate-800 flex justify-between items-center rounded-b-3xl">
<Link :href="route('admin.quizzes.show', quiz.id)" class="px-4 py-2 text-indigo-600 dark:text-indigo-400 font-bold text-xs uppercase tracking-widest hover:text-indigo-900 dark:hover:text-white transition-colors">
Questions
</Link>
<div class="flex items-center gap-1">
<button @click="openEditModal(quiz)" class="p-2 text-amber-500 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-xl transition-colors" title="Modifier">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button @click="deleteQuiz(quiz.id)" class="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition-colors" title="Supprimer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
</div>
<div v-if="quizzes.length === 0" class="col-span-full py-12 text-center text-slate-500 italic">
Aucun quiz créé.
</div>
</div>
<!-- Quiz Modal (Create/Edit) -->
<Modal :show="isModalOpen" @close="closeModal">
<div class="p-8">
<h3 class="text-2xl font-black uppercase tracking-tight mb-8">
{{ editingQuiz ? 'Modifier le Quiz' : 'Créer un nouveau Quiz' }}
</h3>
<form @submit.prevent="submit" class="space-y-6">
<div>
<InputLabel for="title" value="Titre du Quiz" />
<TextInput id="title" type="text" class="mt-1 block w-full" v-model="form.title" required />
<InputError class="mt-2" :message="form.errors.title" />
</div>
<div>
<InputLabel for="description" value="Description" />
<textarea
id="description"
class="mt-1 block w-full border-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
rows="3"
v-model="form.description"
></textarea>
<InputError class="mt-2" :message="form.errors.description" />
</div>
<div>
<InputLabel for="duration" value="Durée (minutes)" />
<TextInput id="duration" type="number" class="mt-1 block w-full" v-model="form.duration_minutes" required />
<InputError class="mt-2" :message="form.errors.duration_minutes" />
</div>
<div class="flex justify-end gap-3 mt-8">
<SecondaryButton @click="isModalOpen = false" :disabled="form.processing">
Annuler
</SecondaryButton>
<PrimaryButton :disabled="form.processing">
{{ editingQuiz ? 'Modifier Quiz' : 'Créer Quiz' }}
</PrimaryButton>
</div>
</form>
</div>
</Modal>
</AdminLayout>
</template>

View File

@@ -0,0 +1,289 @@
<script setup>
import AdminLayout from '@/Layouts/AdminLayout.vue';
import { Head, useForm, Link, router, usePage } from '@inertiajs/vue3';
import { ref, computed } from 'vue';
import Modal from '@/Components/Modal.vue';
import InputLabel from '@/Components/InputLabel.vue';
import TextInput from '@/Components/TextInput.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DangerButton from '@/Components/DangerButton.vue';
import InputError from '@/Components/InputError.vue';
const props = defineProps({
quiz: Object
});
const page = usePage();
const flashSuccess = computed(() => page.props.flash?.success);
const isModalOpen = ref(false);
const editingQuestion = ref(null);
const form = useForm({
title: '',
context: '',
label: '',
points: 1,
type: 'qcm',
options: [
{ option_text: '', is_correct: false },
{ option_text: '', is_correct: false },
]
});
const openCreateModal = () => {
editingQuestion.value = null;
form.reset();
isModalOpen.value = true;
};
const openEditModal = (question) => {
editingQuestion.value = question;
form.title = question.title || '';
form.context = question.context || '';
form.label = question.label;
form.points = question.points;
form.type = question.type;
form.options = question.options.map(o => ({
option_text: o.option_text,
is_correct: !!o.is_correct
}));
if (form.options.length === 0 && form.type === 'qcm') {
form.options = [{ option_text: '', is_correct: false }, { option_text: '', is_correct: false }];
}
isModalOpen.value = true;
};
const deleteQuestion = (questionId) => {
if (confirm('Voulez-vous vraiment supprimer cette question ?')) {
router.delete(route('admin.quizzes.questions.destroy', [props.quiz.id, questionId]));
}
};
const addOption = () => {
form.options.push({ option_text: '', is_correct: false });
};
const removeOption = (index) => {
form.options.splice(index, 1);
};
const submit = () => {
if (editingQuestion.value) {
form.put(route('admin.quizzes.questions.update', [props.quiz.id, editingQuestion.value.id]), {
onSuccess: () => {
isModalOpen.value = false;
form.reset();
editingQuestion.value = null;
},
});
} else {
form.post(route('admin.quizzes.questions.store', [props.quiz.id]), {
onSuccess: () => {
isModalOpen.value = false;
form.reset();
},
});
}
};
</script>
<template>
<Head :title="'Gestion - ' + quiz.title" />
<AdminLayout>
<template #header>
<div class="flex items-center gap-4">
<Link :href="route('admin.quizzes.index')" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</Link>
<span>Quiz: {{ quiz.title }}</span>
</div>
</template>
<div class="flex justify-between items-center mb-8">
<h3 class="text-2xl font-bold">Banque de Questions</h3>
<PrimaryButton @click="openCreateModal">
Ajouter une Question
</PrimaryButton>
</div>
<!-- Flash Messages -->
<div v-if="flashSuccess" class="mb-8 p-6 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-2xl flex items-center gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
<div class="p-2 bg-emerald-500 rounded-lg text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<p class="font-bold text-emerald-800 dark:text-emerald-400">Succès !</p>
<p class="text-emerald-700 dark:text-emerald-500 text-sm">{{ flashSuccess }}</p>
</div>
</div>
<div class="space-y-6">
<div
v-for="(question, qIndex) in quiz.questions"
:key="question.id"
class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden"
>
<div class="p-6 bg-slate-50 dark:bg-slate-700/30 border-b border-slate-200 dark:border-slate-700 flex justify-between items-start">
<div>
<span class="text-xs font-bold text-slate-400 uppercase tracking-widest block mb-1">Question #{{ qIndex + 1 }} - {{ question.points }} pts</span>
<h4 class="text-lg font-bold">
<span v-if="question.title" class="text-indigo-600 mr-2">[{{ question.title }}]</span>
{{ question.label }}
</h4>
<div v-if="question.context" class="mt-2 p-3 bg-slate-100 dark:bg-slate-900/50 rounded-lg text-sm text-slate-600 dark:text-slate-400 border-l-4 border-slate-300">
{{ question.context }}
</div>
</div>
<div class="flex items-center gap-3">
<span class="px-2 py-1 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded text-xs font-bold uppercase tracking-tight mr-2">
{{ question.type }}
</span>
<button @click="openEditModal(question)" class="p-2 text-indigo-600 hover:bg-white dark:hover:bg-slate-700 rounded-lg transition-colors" title="Modifier">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button @click="deleteQuestion(question.id)" class="p-2 text-red-600 hover:bg-white dark:hover:bg-slate-700 rounded-lg transition-colors" title="Supprimer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
<div v-if="question.type === 'qcm'" class="p-6">
<ul class="space-y-3">
<li v-for="option in question.options" :key="option.id" class="flex items-center gap-3 text-sm p-3 rounded-lg border border-slate-100 dark:border-slate-700">
<div
class="w-2 h-2 rounded-full"
:class="[option.is_correct ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' : 'bg-slate-300 dark:bg-slate-600']"
></div>
<span :class="[option.is_correct ? 'font-bold text-emerald-600 dark:text-emerald-400' : 'text-slate-600 dark:text-slate-400']">
{{ option.option_text }}
</span>
</li>
</ul>
</div>
<div v-else class="p-6 text-slate-500 italic text-sm">
Question ouverte (reponse libre).
</div>
</div>
<div v-if="quiz.questions.length === 0" class="py-12 text-center text-slate-500 italic">
Aucune question dans ce quiz.
</div>
</div>
<!-- Add/Edit Question Modal -->
<Modal :show="isModalOpen" @close="isModalOpen = false">
<div class="p-6">
<h3 class="text-xl font-bold mb-6">
{{ editingQuestion ? 'Modifier la Question' : 'Ajouter une Question' }}
</h3>
<form @submit.prevent="submit" class="space-y-6">
<div v-if="Object.keys(form.errors).length > 0" class="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl text-red-600 dark:text-red-400 text-sm">
<p class="font-bold">Veuillez corriger les erreurs suivantes :</p>
<ul class="list-disc list-inside mt-1">
<li v-for="(error, field) in form.errors" :key="field">{{ error }}</li>
</ul>
</div>
<div class="space-y-4">
<div>
<InputLabel for="title" value="Titre de la question (optionnel)" />
<TextInput id="title" type="text" class="mt-1 block w-full" v-model="form.title" placeholder="Ex: Algorithmique, Debugging..." />
<InputError class="mt-2" :message="form.errors.title" />
</div>
<div>
<InputLabel for="context" value="Contexte ou Enoncé détaillé (optionnel)" />
<textarea
id="context"
v-model="form.context"
rows="3"
class="mt-1 block w-full border-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
placeholder="Donnez du contexte au candidat..."
></textarea>
<InputError class="mt-2" :message="form.errors.context" />
</div>
<div>
<InputLabel for="label" value="Question" />
<TextInput id="label" type="text" class="mt-1 block w-full" v-model="form.label" required placeholder="La question précise à poser" />
<InputError class="mt-2" :message="form.errors.label" />
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<InputLabel for="points" value="Points" />
<TextInput id="points" type="number" class="mt-1 block w-full" v-model="form.points" required />
<InputError class="mt-2" :message="form.errors.points" />
</div>
<div>
<InputLabel for="type" value="Type" />
<select
id="type"
v-model="form.type"
class="mt-1 block w-full border-slate-300 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
>
<option value="qcm">QCM</option>
<option value="open">Question Ouverte</option>
</select>
<InputError class="mt-2" :message="form.errors.type" />
</div>
</div>
<div v-if="form.type === 'qcm'" class="space-y-4">
<div class="flex justify-between items-center">
<InputLabel value="Options de réponse" />
<SecondaryButton type="button" @click="addOption" class="!py-1 !px-2 text-xs">
+ Ajouter
</SecondaryButton>
</div>
<div class="space-y-3">
<div v-for="(option, index) in form.options" :key="index" class="flex items-center gap-3">
<input
type="checkbox"
v-model="option.is_correct"
class="rounded dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 shadow-sm"
/>
<TextInput
type="text"
class="block w-full text-sm"
v-model="option.option_text"
placeholder="Libellé de l'option"
required
/>
<button v-if="form.options.length > 2" @click="removeOption(index)" type="button" class="text-slate-400 hover:text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
<p class="text-[11px] text-slate-500 italic">Cochez les cases pour indiquer les bonnes réponses.</p>
</div>
<div class="flex justify-end gap-3 mt-8 pt-6 border-t border-slate-100 dark:border-slate-700">
<SecondaryButton @click="isModalOpen = false" :disabled="form.processing">
Annuler
</SecondaryButton>
<PrimaryButton :disabled="form.processing">
{{ editingQuestion ? 'Mettre à jour' : 'Enregistrer Question' }}
</PrimaryButton>
</div>
</form>
</div>
</Modal>
</AdminLayout>
</template>