Le contributeur colle un lien (ou plusieurs) et explique en texte libre pourquoi c'est pertinent, sans template ni minimum de caractères. Le type devient une suggestion optionnelle (chips). Les 5 formulaires typés restent disponibles derrière « Mode détaillé » (replié par défaut). - utils/submitLibre.ts : schéma Zod partagé client/serveur + mapping vers les colonnes NocoDB existantes (aucun changement de schéma) — table orgas par défaut, table ressources_references si le chip « Références » est choisi. - components/FormLibre.vue : liens dynamiques (1-10), texte libre, chips, email optionnel, honeypot. - server/api/submit/index.post.ts : branche mode=libre en amont du schéma typé existant ; email Jules remplacé par ntfy (server/utils/notify.ts) — jamais l'email ni le texte du contributeur dans la notif. - scripts/test-submit-libre.mjs : 6 cas contre le vrai schéma Zod (via jiti). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
6.8 KiB
Vue
249 lines
6.8 KiB
Vue
<template>
|
||
<div class="proposer-page">
|
||
<div class="proposer-inner">
|
||
<NuxtLink to="/" class="back-link">← Retour à la carte</NuxtLink>
|
||
|
||
<div class="proposer-header">
|
||
<h1>Proposer une ressource</h1>
|
||
<p class="proposer-subtitle">
|
||
Colle un lien (ou plusieurs), dis en quelques mots pourquoi c'est utile — on s'occupe du reste.
|
||
On valide manuellement avant publication.
|
||
</p>
|
||
</div>
|
||
|
||
<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 autre chose</button>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<FormLibre :preset-type="presetType" @success="onSuccess" />
|
||
|
||
<div class="mode-detaille">
|
||
<button
|
||
type="button"
|
||
class="mode-detaille-toggle"
|
||
:aria-expanded="detailleOuvert"
|
||
aria-controls="mode-detaille-panel"
|
||
@click="detailleOuvert = !detailleOuvert"
|
||
>
|
||
{{ detailleOuvert ? '− Fermer le mode détaillé' : '+ Mode détaillé (formulaires par catégorie)' }}
|
||
</button>
|
||
|
||
<div v-if="detailleOuvert" id="mode-detaille-panel" class="mode-detaille-panel">
|
||
<p class="mode-detaille-hint">
|
||
Pour aller plus loin qu'un lien + quelques mots : échelle, fonctions, ville... un formulaire par catégorie.
|
||
</p>
|
||
<div class="tabs-bar">
|
||
<button
|
||
v-for="tab in TABS"
|
||
:key="tab.id"
|
||
type="button"
|
||
class="tab-btn"
|
||
:class="{ 'tab-btn--active': activeTab === tab.id }"
|
||
@click="activeTab = tab.id"
|
||
>{{ tab.label }}</button>
|
||
</div>
|
||
|
||
<div class="tab-content">
|
||
<FormEcosysteme v-if="activeTab === 'ecosysteme'" @success="onSuccess" />
|
||
<FormReseau v-else-if="activeTab === 'reseau'" @success="onSuccess" />
|
||
<FormJobs v-else-if="activeTab === 'jobs'" @success="onSuccess" />
|
||
<FormOutils v-else-if="activeTab === 'outils'" @success="onSuccess" />
|
||
<FormReferences v-else-if="activeTab === 'references'" @success="onSuccess" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { TYPES_SUGGERES, type TypeSuggere } from '~/utils/submitLibre'
|
||
|
||
const TABS = [
|
||
{ id: 'ecosysteme', label: 'Écosystème / Entraide' },
|
||
{ id: 'reseau', label: 'Réseau AEP' },
|
||
{ id: 'jobs', label: 'Jobs' },
|
||
{ id: 'outils', label: 'Outils' },
|
||
{ id: 'references', label: 'Références (bibliothèque)' },
|
||
] as const
|
||
|
||
// ?categorie=outils (lien « Proposer une ressource » des têtes d'échelle) présélectionne
|
||
// le chip du formulaire assoupli ET l'onglet du mode détaillé si l'un ou l'autre est ouvert.
|
||
const route = useRoute()
|
||
const categorie = String(route.query.categorie ?? '')
|
||
const presetType = computed<TypeSuggere | undefined>(() =>
|
||
(TYPES_SUGGERES as readonly string[]).includes(categorie) ? (categorie as TypeSuggere) : undefined,
|
||
)
|
||
const activeTab = ref<string>(TABS.some(t => t.id === categorie) ? categorie : 'ecosysteme')
|
||
const detailleOuvert = ref(false)
|
||
const success = ref(false)
|
||
|
||
function onSuccess() {
|
||
success.value = true
|
||
}
|
||
|
||
function reset() {
|
||
success.value = false
|
||
activeTab.value = 'ecosysteme'
|
||
detailleOuvert.value = false
|
||
}
|
||
|
||
useHead({ title: 'Proposer une ressource — AEP' })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.proposer-page {
|
||
min-height: 100vh;
|
||
background: var(--nav-bg);
|
||
padding: 1.5rem 1rem 4rem;
|
||
}
|
||
.proposer-inner {
|
||
max-width: 680px;
|
||
margin: 0 auto;
|
||
}
|
||
.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; }
|
||
.proposer-header { margin-bottom: 1.5rem; }
|
||
.proposer-header h1 {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: var(--nav-text);
|
||
margin: 0 0 0.5rem;
|
||
}
|
||
.proposer-subtitle {
|
||
font-size: 0.9rem;
|
||
color: var(--nav-text-muted);
|
||
line-height: 1.5;
|
||
margin: 0;
|
||
}
|
||
|
||
.mode-detaille {
|
||
margin-top: 2rem;
|
||
border-top: 1px solid var(--nav-bg-alt);
|
||
padding-top: 1.25rem;
|
||
}
|
||
.mode-detaille-toggle {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--nav-text-muted);
|
||
font-size: 0.85rem;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
padding: 0.25rem 0;
|
||
text-decoration: underline;
|
||
text-underline-offset: 2px;
|
||
}
|
||
.mode-detaille-toggle:hover { color: var(--nav-text); }
|
||
.mode-detaille-panel { margin-top: 1rem; }
|
||
.mode-detaille-hint {
|
||
font-size: 0.8rem;
|
||
color: var(--nav-text-muted);
|
||
margin: 0 0 1rem;
|
||
}
|
||
|
||
.tabs-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.25rem;
|
||
margin-bottom: 1.5rem;
|
||
border-bottom: 1px solid var(--nav-bg-alt);
|
||
padding-bottom: 0;
|
||
}
|
||
.tab-btn {
|
||
padding: 0.5rem 0.9rem;
|
||
font-size: 0.8rem;
|
||
font-weight: 500;
|
||
color: var(--nav-text-muted);
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: color 0.15s, border-color 0.15s;
|
||
white-space: nowrap;
|
||
}
|
||
.tab-btn:hover { color: var(--nav-text); }
|
||
.tab-btn--active {
|
||
color: var(--nav-text);
|
||
border-bottom-color: var(--nav-primary-solid);
|
||
font-weight: 600;
|
||
}
|
||
.tab-content {
|
||
min-height: 300px;
|
||
}
|
||
|
||
.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; }
|
||
|
||
.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;
|
||
margin-top: 0.5rem;
|
||
transition: border-color 0.15s, color 0.15s;
|
||
}
|
||
.btn-secondary:hover {
|
||
border-color: var(--nav-primary-solid);
|
||
color: var(--nav-text);
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.proposer-page { padding: 1rem 0.75rem 3rem; }
|
||
.tabs-bar { overflow-x: auto; flex-wrap: nowrap; }
|
||
}
|
||
</style>
|