feat(aep): carte AEP — push Gitea 2026-04-28
This commit is contained in:
147
components/CommentForm.vue
Normal file
147
components/CommentForm.vue
Normal 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>
|
||||
Reference in New Issue
Block a user