feat(codev): M3 - CodevGraph D3 force-directed + page carto affichage

- Install d3@^7.9.0 (absent du projet, requis pour force simulation)
- components/codev/CodevGraph.vue : simulation forceLink/forceManyBody/forceCenter/forceCollide, drag D3, pastilles offre (vert) + besoin (orange), tooltip SVG natif, ResizeObserver, watch matches/mode pret pour M4, placeholder si 0 fiches
- pages/codev/carto.vue : useFetch /api/codev/fiches, mount CodevGraph, refs matches+mode vides (M4 les remplira)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jules Neny
2026-05-06 16:03:28 +02:00
parent 9c4f4b8e87
commit 3347b3f859
4 changed files with 955 additions and 0 deletions

123
pages/codev/carto.vue Normal file
View File

@@ -0,0 +1,123 @@
<template>
<div class="codev-carto">
<header class="carto-header">
<h1>Carto entraide</h1>
<p class="carto-subtitle">
<template v-if="pending">Chargement...</template>
<template v-else>
{{ fiches.length }} fiche{{ fiches.length !== 1 ? 's' : '' }} clique sur un nom pour voir le detail
</template>
</p>
</header>
<ClientOnly>
<CodevGraph
:fiches="fiches"
:matches="matches"
:mode="mode"
@select-fiche="onSelectFiche"
/>
<template #fallback>
<div class="graph-fallback">Chargement du graphe...</div>
</template>
</ClientOnly>
<!-- Boutons matching : arrivent en M4 -->
<div class="matching-controls placeholder">
<p>Boutons matching - arrivent bientot (M4)</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { CodevFiche, CodevMatch } from '~/types/codev'
useHead({ title: 'Carto — Co-developpement' })
const { data, pending } = await useFetch<{ list: CodevFiche[] }>('/api/codev/fiches')
const fiches = computed(() => data.value?.list ?? [])
// En M3 : pas de matching, tableau vide. M4 remplira ces refs.
const matches = ref<CodevMatch[]>([])
const mode = ref<'none' | 'solution' | 'alliance' | 'surprise'>('none')
function onSelectFiche(id: number) {
// M3 : simple navigation vers la fiche (ou futur drawer en M4)
navigateTo(`/codev/fiche?id=${id}`)
}
</script>
<style scoped>
.codev-carto {
min-height: 100vh;
background: var(--nav-bg, #fafafa);
display: flex;
flex-direction: column;
padding: 1.25rem 1rem 2rem;
gap: 1rem;
max-width: 100%;
box-sizing: border-box;
}
/* ── En-tete ── */
.carto-header {
text-align: center;
padding-bottom: 0.5rem;
}
.carto-header h1 {
font-size: 1.5rem;
font-weight: 700;
color: var(--nav-text, #1a1a2e);
margin: 0 0 0.375rem;
}
.carto-subtitle {
font-size: 0.9rem;
color: var(--nav-text-muted, #6b7280);
margin: 0;
}
/* ── Fallback ── */
.graph-fallback {
width: 100%;
height: 70vh;
min-height: 320px;
display: flex;
align-items: center;
justify-content: center;
color: var(--nav-text-muted, #6b7280);
font-size: 0.9rem;
background: var(--nav-bg-alt, #f3f4f6);
border-radius: 12px;
}
/* ── Placeholder matching (M4) ── */
.matching-controls.placeholder {
text-align: center;
padding: 0.75rem;
}
.matching-controls.placeholder p {
color: #999;
font-size: 0.85rem;
margin: 0;
}
/* ── Mobile ── */
@media (max-width: 600px) {
.codev-carto {
padding: 1rem 0.75rem 1.5rem;
}
.carto-header h1 {
font-size: 1.25rem;
}
}
</style>