feat(pratiques): page /proposer-pratique — formulaire contribution Pratique
Formulaire complet : nom, URL, description (50-500c), critères régé (checkboxes min 3/8), type entité (radio), pays (dropdown Europe + DOM-TOM + autre), ville, tags (virgule-séparé, chips preview), email optionnel. Validation Zod client-side champ par champ + submit, gestion 422/429. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
833
pages/proposer-pratique.vue
Normal file
833
pages/proposer-pratique.vue
Normal file
@@ -0,0 +1,833 @@
|
||||
<template>
|
||||
<div class="contribuer-page">
|
||||
<div class="contribuer-inner">
|
||||
<!-- Retour -->
|
||||
<NuxtLink to="/pratiques-regeneratives" class="back-link">
|
||||
← Retour à la carte
|
||||
</NuxtLink>
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="contribuer-header">
|
||||
<h1>Proposer une pratique</h1>
|
||||
<p class="contribuer-subtitle">
|
||||
Tu connais une agence, un collectif ou un réseau qui incarne l'architecture régénérative ?
|
||||
Soumets-le ici — Jules valide manuellement les nouvelles entrées.
|
||||
</p>
|
||||
<p class="contribuer-hint">
|
||||
Si tu n'as pas le temps de tout remplir, laisse-nous juste le lien.
|
||||
Mais une description de ta main, c'est toujours plus vivant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Message succès -->
|
||||
<div v-if="success" class="success-block" role="status" aria-live="polite">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2>Merci !</h2>
|
||||
<p>Ta proposition est en attente de modération.</p>
|
||||
<p class="success-detail">
|
||||
Jules valide manuellement chaque entrée avant publication.
|
||||
</p>
|
||||
<button type="button" class="btn-secondary" @click="reset">
|
||||
Proposer une autre pratique
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire -->
|
||||
<form v-else @submit.prevent="submit" class="contribuer-form" novalidate>
|
||||
|
||||
<!-- Nom -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.nom }">
|
||||
<label for="nom">Nom de l'organisation <span class="required">*</span></label>
|
||||
<input
|
||||
id="nom"
|
||||
v-model="form.nom"
|
||||
type="text"
|
||||
placeholder="Ex : Lacaton & Vassal, Plateau Urbain..."
|
||||
autocomplete="organization"
|
||||
@blur="validateField('nom')"
|
||||
/>
|
||||
<span v-if="errors.nom" class="error-msg" role="alert">{{ errors.nom }}</span>
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.url }">
|
||||
<label for="url">
|
||||
Site web
|
||||
<span class="label-hint">(optionnel — recommandé)</span>
|
||||
</label>
|
||||
<input
|
||||
id="url"
|
||||
v-model="form.url"
|
||||
type="url"
|
||||
placeholder="https://..."
|
||||
@blur="validateField('url')"
|
||||
/>
|
||||
<span v-if="errors.url" class="error-msg" role="alert">{{ errors.url }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.description_user }">
|
||||
<label for="description_user">
|
||||
Description courte <span class="required">*</span>
|
||||
<span class="label-hint">(50 à 500 caractères)</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description_user"
|
||||
v-model="form.description_user"
|
||||
rows="4"
|
||||
placeholder="Décris cette pratique : approche, matériaux, posture, ce qui la rend régénérative..."
|
||||
@blur="validateField('description_user')"
|
||||
/>
|
||||
<div class="field-meta">
|
||||
<span v-if="errors.description_user" class="error-msg" role="alert">
|
||||
{{ errors.description_user }}
|
||||
</span>
|
||||
<span v-else class="char-count" :class="{ 'char-warn': form.description_user.length > 450 }">
|
||||
{{ form.description_user.length }}/500
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Critères régénératifs -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.criteres }">
|
||||
<fieldset>
|
||||
<legend>
|
||||
Critères régénératifs <span class="required">*</span>
|
||||
<span class="label-hint">(3 minimum, 8 maximum)</span>
|
||||
</legend>
|
||||
<div class="checkbox-grid">
|
||||
<label
|
||||
v-for="c in CRITERES"
|
||||
:key="c.id"
|
||||
class="checkbox-label"
|
||||
:class="{
|
||||
active: form.criteres.includes(c.id),
|
||||
disabled: !form.criteres.includes(c.id) && form.criteres.length >= 8,
|
||||
}"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="c.id"
|
||||
:checked="form.criteres.includes(c.id)"
|
||||
:disabled="!form.criteres.includes(c.id) && form.criteres.length >= 8"
|
||||
@change="toggleCritere(c.id)"
|
||||
/>
|
||||
{{ c.label }}
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<span v-if="errors.criteres" class="error-msg" role="alert">{{ errors.criteres }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Type d'entité -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.type }">
|
||||
<fieldset>
|
||||
<legend>
|
||||
Type d'entité <span class="required">*</span>
|
||||
</legend>
|
||||
<div class="radio-group">
|
||||
<label
|
||||
v-for="t in TYPES_ENTITE"
|
||||
:key="t"
|
||||
class="radio-label"
|
||||
:class="{ active: form.type === t }"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:value="t"
|
||||
v-model="form.type"
|
||||
name="type"
|
||||
@change="validateField('type')"
|
||||
/>
|
||||
{{ TYPES_ENTITE_LABELS[t] }}
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<span v-if="errors.type" class="error-msg" role="alert">{{ errors.type }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Pays -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.pays }">
|
||||
<label for="pays">
|
||||
Pays <span class="required">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="pays"
|
||||
v-model="form.pays"
|
||||
@change="validateField('pays')"
|
||||
>
|
||||
<option value="" disabled>Sélectionne un pays...</option>
|
||||
<optgroup label="Europe">
|
||||
<option v-for="code in EUROPE_CODES" :key="code" :value="code">
|
||||
{{ PAYS_LABELS[code] }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup label="DOM-TOM">
|
||||
<option v-for="code in OUTREMER_CODES" :key="code" :value="code">
|
||||
{{ PAYS_LABELS[code] }}
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup label="Autre">
|
||||
<option value="AUTRE">Autre pays...</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span v-if="errors.pays" class="error-msg" role="alert">{{ errors.pays }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Pays autre (conditionnel) -->
|
||||
<div v-if="form.pays === 'AUTRE'" class="field-group" :class="{ 'field-error': errors.pays_autre }">
|
||||
<label for="pays_autre">Précise le pays</label>
|
||||
<input
|
||||
id="pays_autre"
|
||||
v-model="form.pays_autre"
|
||||
type="text"
|
||||
placeholder="Ex : Maroc, Brésil..."
|
||||
maxlength="50"
|
||||
/>
|
||||
<span v-if="errors.pays_autre" class="error-msg" role="alert">{{ errors.pays_autre }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Ville -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.ville }">
|
||||
<label for="ville">
|
||||
Ville principale
|
||||
<span class="label-hint">(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
id="ville"
|
||||
v-model="form.ville"
|
||||
type="text"
|
||||
placeholder="Ex : Paris, Bordeaux, Bruxelles..."
|
||||
/>
|
||||
<span v-if="errors.ville" class="error-msg" role="alert">{{ errors.ville }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.tags }">
|
||||
<label for="tags">
|
||||
Tags
|
||||
<span class="label-hint">(optionnel — 3 à 6 mots-clés, séparés par des virgules)</span>
|
||||
</label>
|
||||
<input
|
||||
id="tags"
|
||||
v-model="tagsInput"
|
||||
type="text"
|
||||
placeholder="Ex : biosourcé, réhabilitation, circuit-court"
|
||||
@blur="parseTags"
|
||||
/>
|
||||
<span v-if="errors.tags" class="error-msg" role="alert">{{ errors.tags }}</span>
|
||||
<div v-if="form.tags && form.tags.length" class="tags-preview">
|
||||
<span v-for="tag in form.tags" :key="tag" class="tag-chip">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="field-group" :class="{ 'field-error': errors.submitted_by_email }">
|
||||
<label for="submitted_by_email">
|
||||
Ton email
|
||||
<span class="label-hint">(optionnel — pour le suivi)</span>
|
||||
</label>
|
||||
<input
|
||||
id="submitted_by_email"
|
||||
v-model="form.submitted_by_email"
|
||||
type="email"
|
||||
placeholder="ton@email.fr"
|
||||
autocomplete="email"
|
||||
@blur="validateField('submitted_by_email')"
|
||||
/>
|
||||
<span v-if="errors.submitted_by_email" class="error-msg" role="alert">
|
||||
{{ errors.submitted_by_email }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Erreur globale -->
|
||||
<div v-if="serverError" class="server-error" role="alert">
|
||||
<strong>Erreur :</strong> {{ serverError }}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NuxtLink to="/pratiques-regeneratives" class="btn-secondary">Annuler</NuxtLink>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary"
|
||||
:disabled="submitting"
|
||||
>
|
||||
{{ submitting ? 'Envoi en cours...' : 'Proposer la pratique →' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="form-note">
|
||||
Ta proposition sera examinée par Jules avant publication.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import { CRITERES, TYPES_ENTITE, TYPES_ENTITE_LABELS, EUROPE_CODES, OUTREMER_CODES, PAYS_LABELS } from '~/types/pratique'
|
||||
|
||||
// ── Schéma Zod (côté client — miroir du serveur) ──────────────────────────────
|
||||
|
||||
const PratiqueSubmitSchema = z.object({
|
||||
nom: z.string().min(3, 'Minimum 3 caractères').max(150, 'Maximum 150 caractères').trim(),
|
||||
url: z.string().url('URL invalide (commencer par https://)').optional().or(z.literal('')),
|
||||
description_user: z.string().min(50, 'Minimum 50 caractères').max(500, 'Maximum 500 caractères').trim(),
|
||||
criteres: z
|
||||
.array(z.number().int().min(1).max(8))
|
||||
.min(3, 'Sélectionne au moins 3 critères')
|
||||
.max(8, 'Maximum 8 critères'),
|
||||
pays: z.string().length(2, 'Sélectionne un pays').or(z.literal('AUTRE')),
|
||||
pays_autre: z.string().max(50).optional(),
|
||||
ville: z.string().max(100).optional(),
|
||||
type: z.enum(TYPES_ENTITE, { errorMap: () => ({ message: 'Sélectionne un type d\'entité' }) }),
|
||||
tags: z.array(z.string().max(30)).max(6).optional(),
|
||||
submitted_by_email: z.string().email('Email invalide').optional().or(z.literal('')),
|
||||
})
|
||||
|
||||
// ── État du formulaire ────────────────────────────────────────────────────────
|
||||
|
||||
const form = reactive({
|
||||
nom: '',
|
||||
url: '',
|
||||
description_user: '',
|
||||
criteres: [] as number[],
|
||||
pays: '' as string,
|
||||
pays_autre: '',
|
||||
ville: '',
|
||||
type: '' as typeof TYPES_ENTITE[number] | '',
|
||||
tags: [] as string[],
|
||||
submitted_by_email: '',
|
||||
})
|
||||
|
||||
const tagsInput = ref('')
|
||||
const errors = reactive<Record<string, string>>({})
|
||||
const submitting = ref(false)
|
||||
const success = ref(false)
|
||||
const serverError = ref('')
|
||||
|
||||
// ── Validation champ par champ ────────────────────────────────────────────────
|
||||
|
||||
function validateField(field: string) {
|
||||
const partial = PratiqueSubmitSchema.partial()
|
||||
const result = partial.safeParse({ [field]: (form as any)[field] })
|
||||
if (!result.success) {
|
||||
const fieldErrors = result.error.flatten().fieldErrors
|
||||
errors[field] = fieldErrors[field]?.[0] ?? ''
|
||||
} else {
|
||||
delete errors[field]
|
||||
}
|
||||
}
|
||||
|
||||
function validateAll(): boolean {
|
||||
const result = PratiqueSubmitSchema.safeParse(form)
|
||||
if (!result.success) {
|
||||
const flat = result.error.flatten().fieldErrors
|
||||
Object.assign(errors, Object.fromEntries(
|
||||
Object.entries(flat).map(([k, v]) => [k, v?.[0] ?? ''])
|
||||
))
|
||||
return false
|
||||
}
|
||||
Object.keys(errors).forEach(k => delete errors[k])
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Gestion critères ──────────────────────────────────────────────────────────
|
||||
|
||||
function toggleCritere(id: number) {
|
||||
const idx = form.criteres.indexOf(id)
|
||||
if (idx >= 0) {
|
||||
form.criteres.splice(idx, 1)
|
||||
} else if (form.criteres.length < 8) {
|
||||
form.criteres.push(id)
|
||||
}
|
||||
validateField('criteres')
|
||||
}
|
||||
|
||||
// ── Gestion tags ──────────────────────────────────────────────────────────────
|
||||
|
||||
function parseTags() {
|
||||
const raw = tagsInput.value
|
||||
.split(',')
|
||||
.map(t => t.trim().toLowerCase())
|
||||
.filter(t => t.length > 0 && t.length <= 30)
|
||||
.slice(0, 6)
|
||||
form.tags = raw
|
||||
}
|
||||
|
||||
// ── Soumission ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function submit() {
|
||||
serverError.value = ''
|
||||
parseTags()
|
||||
|
||||
if (!validateAll()) {
|
||||
await nextTick()
|
||||
const firstError = document.querySelector('.field-error')
|
||||
firstError?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
await $fetch('/api/submit-pratique', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
nom: form.nom,
|
||||
url: form.url || undefined,
|
||||
description_user: form.description_user,
|
||||
criteres: form.criteres,
|
||||
pays: form.pays,
|
||||
pays_autre: form.pays_autre || undefined,
|
||||
ville: form.ville || undefined,
|
||||
type: form.type,
|
||||
tags: form.tags.length ? form.tags : undefined,
|
||||
submitted_by_email: form.submitted_by_email || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
success.value = true
|
||||
} catch (e: any) {
|
||||
const status = e?.status ?? e?.statusCode
|
||||
if (status === 429) {
|
||||
serverError.value = 'Tu as déjà soumis 3 pratiques aujourd\'hui. Réessaie demain.'
|
||||
} else if (status === 422 && e?.data) {
|
||||
const fieldErrors = e.data
|
||||
Object.entries(fieldErrors).forEach(([k, v]) => {
|
||||
errors[k] = Array.isArray(v) ? v[0] : String(v)
|
||||
})
|
||||
serverError.value = 'Certains champs sont invalides — vérifie les erreurs ci-dessus.'
|
||||
} else {
|
||||
serverError.value = 'Une erreur s\'est produite. Réessaie dans quelques instants.'
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(form, {
|
||||
nom: '', url: '', description_user: '', criteres: [],
|
||||
pays: '', pays_autre: '', ville: '', type: '', tags: [], submitted_by_email: '',
|
||||
})
|
||||
tagsInput.value = ''
|
||||
Object.keys(errors).forEach(k => delete errors[k])
|
||||
success.value = false
|
||||
serverError.value = ''
|
||||
}
|
||||
|
||||
// ── Meta ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
useHead({ title: 'Proposer une pratique — AEP' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ── Layout ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.contribuer-page {
|
||||
min-height: 100vh;
|
||||
background: var(--nav-bg);
|
||||
padding: 1.5rem 1rem 4rem;
|
||||
}
|
||||
|
||||
.contribuer-inner {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ── Retour ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--nav-primary-solid);
|
||||
opacity: 0.7;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── En-tête ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.contribuer-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.contribuer-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--nav-text);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.contribuer-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--nav-text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.contribuer-hint {
|
||||
font-size: 0.82rem;
|
||||
color: var(--nav-text-muted);
|
||||
opacity: 0.75;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Succès ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.success-block {
|
||||
background: var(--nav-surface);
|
||||
border: 1px solid rgba(26, 34, 56, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 2rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(26, 34, 56, 0.1);
|
||||
color: var(--nav-text);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.success-block h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--nav-text);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.success-block p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--nav-text-muted);
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.success-detail {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
/* ── Formulaire ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.contribuer-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* ── Champ générique ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.field-group label,
|
||||
.field-group legend {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--nav-text);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.field-group fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.label-hint {
|
||||
font-weight: 400;
|
||||
color: var(--nav-text-muted);
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.field-group input[type="text"],
|
||||
.field-group input[type="url"],
|
||||
.field-group input[type="email"],
|
||||
.field-group select,
|
||||
.field-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid rgba(26, 34, 56, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--nav-text);
|
||||
background: var(--nav-surface);
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field-group select {
|
||||
cursor: pointer;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
.field-group input:focus,
|
||||
.field-group select:focus,
|
||||
.field-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--nav-primary-solid);
|
||||
box-shadow: 0 0 0 2px rgba(245, 179, 66, 0.4);
|
||||
}
|
||||
|
||||
.field-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Erreur champ */
|
||||
|
||||
.field-error input,
|
||||
.field-error select,
|
||||
.field-error textarea {
|
||||
border-color: #c0392b !important;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
font-size: 0.8rem;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.field-meta {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
font-size: 0.75rem;
|
||||
color: var(--nav-text-muted);
|
||||
}
|
||||
|
||||
.char-warn {
|
||||
color: #e67e22;
|
||||
}
|
||||
|
||||
/* ── Radio (Type entité) ─────────────────────────────────────────────────────── */
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid rgba(26, 34, 56, 0.2);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--nav-text);
|
||||
background: var(--nav-surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.radio-label input[type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.radio-label:hover {
|
||||
border-color: var(--nav-primary-solid);
|
||||
background: var(--nav-bg-alt);
|
||||
}
|
||||
|
||||
.radio-label.active {
|
||||
background: var(--nav-primary);
|
||||
border-color: transparent;
|
||||
color: var(--nav-text-on-primary);
|
||||
}
|
||||
|
||||
/* ── Checkboxes (Critères) ───────────────────────────────────────────────────── */
|
||||
|
||||
.checkbox-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.checkbox-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid rgba(26, 34, 56, 0.2);
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--nav-text);
|
||||
background: var(--nav-surface);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.checkbox-label:hover:not(.disabled) {
|
||||
border-color: var(--nav-primary-solid);
|
||||
background: var(--nav-bg-alt);
|
||||
}
|
||||
|
||||
.checkbox-label.active {
|
||||
background: var(--nav-primary);
|
||||
border-color: transparent;
|
||||
color: var(--nav-text-on-primary);
|
||||
}
|
||||
|
||||
.checkbox-label.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Tags preview ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.tags-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.6rem;
|
||||
background: var(--nav-bg-alt);
|
||||
border: 1px solid rgba(26, 34, 56, 0.15);
|
||||
border-radius: 100px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--nav-text-muted);
|
||||
}
|
||||
|
||||
/* ── Erreur serveur ──────────────────────────────────────────────────────────── */
|
||||
|
||||
.server-error {
|
||||
padding: 0.875rem 1rem;
|
||||
background: #fdf0ee;
|
||||
border: 1px solid #e74c3c;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
/* ── Actions ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--nav-primary);
|
||||
color: var(--nav-text-on-primary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s, opacity 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: rgba(26, 34, 56, 0.75);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: transparent;
|
||||
color: var(--nav-text-muted);
|
||||
border: 1px solid rgba(26, 34, 56, 0.2);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--nav-primary-solid);
|
||||
color: var(--nav-text);
|
||||
}
|
||||
|
||||
.form-note {
|
||||
font-size: 0.75rem;
|
||||
color: var(--nav-text-muted);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Responsive ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.contribuer-page {
|
||||
padding: 1rem 0.75rem 3rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user