feat(aep): carte AEP — push Gitea 2026-04-28

This commit is contained in:
Jules Neny
2026-04-28 14:00:05 +02:00
commit 21c44d8193
86 changed files with 31855 additions and 0 deletions

147
components/CommentForm.vue Normal file
View File

@@ -0,0 +1,147 @@
<template>
<section
class="rounded-2xl p-6"
style="background: var(--nav-bg-alt); border: 1px solid rgba(26,34,56,0.1);"
>
<h3 class="font-semibold mb-4" style="color: var(--nav-text);">Ajouter un commentaire</h3>
<!-- Succès -->
<div
v-if="success"
class="rounded-xl p-4 text-sm"
style="background: var(--nav-surface); color: var(--nav-text);"
>
<strong>Merci !</strong>
{{ successMessage }}
</div>
<!-- Formulaire -->
<form v-else @submit.prevent="submit" class="space-y-4" novalidate>
<!-- Commentaire -->
<div>
<label
for="comment-contenu"
class="block text-sm font-medium mb-1"
style="color: var(--nav-text);"
>
Commentaire <span aria-hidden="true">*</span>
</label>
<textarea
id="comment-contenu"
v-model="form.contenu"
required
rows="4"
minlength="10"
maxlength="500"
placeholder="Partage ton expérience avec cette organisation…"
class="w-full px-3 py-2 rounded-lg text-sm resize-none focus:outline-none focus:ring-2"
style="background: var(--nav-surface); color: var(--nav-text); border: 1px solid rgba(26,34,56,0.2); focus-ring-color: var(--nav-accent);"
:class="{ 'border-red-400': errors.contenu }"
/>
<div class="flex justify-between mt-1">
<span v-if="errors.contenu" class="text-xs text-red-500">{{ errors.contenu }}</span>
<span class="text-xs ml-auto" style="color: var(--nav-text-muted);">{{ form.contenu.length }}/500</span>
</div>
</div>
<!-- Pseudo (optionnel) -->
<div>
<label
for="comment-pseudo"
class="block text-sm font-medium mb-1"
style="color: var(--nav-text);"
>Pseudo <span class="font-normal" style="color: var(--nav-text-muted);">(optionnel)</span></label>
<input
id="comment-pseudo"
v-model="form.auteur_pseudo"
type="text"
maxlength="80"
placeholder="Marie A."
class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none focus:ring-2"
style="background: var(--nav-surface); color: var(--nav-text); border: 1px solid rgba(26,34,56,0.2);"
/>
</div>
<!-- Note modération -->
<p class="text-xs" style="color: var(--nav-text-muted);">
Vos commentaires sont filtrés par une IA avant publication.
Les critiques professionnelles factuelles sont les bienvenues.
</p>
<!-- Erreur serveur -->
<p v-if="serverError" class="text-xs text-red-500">{{ serverError }}</p>
<!-- Bouton -->
<button
type="submit"
:disabled="submitting"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
style="background: var(--nav-primary); color: var(--nav-text-on-primary);"
@mouseenter="(e: MouseEvent) => { if (!submitting) (e.currentTarget as HTMLElement).style.background = 'rgba(26,34,56,0.75)' }"
@mouseleave="(e: MouseEvent) => { if (!submitting) (e.currentTarget as HTMLElement).style.background = 'var(--nav-primary)' }"
>
<svg v-if="submitting" class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
{{ submitting ? 'Envoi…' : 'Envoyer' }}
</button>
</form>
</section>
</template>
<script setup lang="ts">
const props = defineProps<{ orgId: number }>()
const emit = defineEmits<{ submitted: [] }>()
const form = reactive({
contenu: '',
auteur_pseudo: '',
})
const submitting = ref(false)
const success = ref(false)
const successMessage = ref('')
const serverError = ref('')
const errors = reactive({ contenu: '' })
function validate(): boolean {
errors.contenu = ''
const c = form.contenu.trim()
if (!c) { errors.contenu = 'Le commentaire est requis.'; return false }
if (c.length < 10) { errors.contenu = 'Minimum 10 caractères.'; return false }
if (c.length > 500) { errors.contenu = 'Maximum 500 caractères.'; return false }
return true
}
async function submit() {
serverError.value = ''
if (!validate()) return
submitting.value = true
try {
const res = await $fetch<{ ok: boolean; status: string; message: string }>('/api/comment', {
method: 'POST',
body: {
orga_id: props.orgId,
contenu: form.contenu.trim(),
auteur_pseudo: form.auteur_pseudo.trim() || undefined,
},
})
success.value = true
successMessage.value = res.message || 'Commentaire reçu.'
emit('submitted')
} catch (err: any) {
const status = err?.response?.status
if (status === 429) {
serverError.value = 'Trop de commentaires aujourd\'hui. Réessaie demain.'
} else {
serverError.value = 'Erreur lors de l\'envoi. Réessaie dans un moment.'
}
} finally {
submitting.value = false
}
}
</script>