Files
Portfolio-Game/frontend/app/components/feature/CheminLibre.vue
skycel 64b1a33d10 feat(mobile): add BottomBar navigation and CheminLibre drawer (Story 3.7)
- Add ZoneCard component for zone display with status indicators
- Add CheminLibre drawer with vertical zone cards and path decoration
- Add BottomBar with Map, Progress, and Settings buttons
- Add ProgressDetail modal showing visited sections
- Add SettingsDrawer with language, consent, and reset options
- Add i18n translations for zone, cheminLibre, bottomBar, settings
- Add --bottom-bar-height CSS variable for spacing
- Modify layouts to include BottomBar on mobile (< 768px)
- Support safe-area-inset for iOS devices
- Touch targets minimum 48x48px for WCAG compliance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 04:29:55 +01:00

101 lines
2.9 KiB
Vue

<script setup lang="ts">
import { mapZones } from '~/data/mapZones'
const emit = defineEmits<{
close: []
navigate: [route: string]
}>()
const { locale, t } = useI18n()
const router = useRouter()
const route = useRoute()
const progressionStore = useProgressionStore()
function handleZoneSelect(zone: (typeof mapZones)[0]) {
if (zone.id === 'contact' && !progressionStore.contactUnlocked) {
return
}
const targetRoute = locale.value === 'fr' ? zone.route.fr : zone.route.en
router.push(targetRoute)
emit('navigate', targetRoute)
emit('close')
}
function isVisited(zoneId: string): boolean {
return zoneId !== 'contact' && progressionStore.visitedSections.includes(zoneId)
}
function isLocked(zoneId: string): boolean {
return zoneId === 'contact' && !progressionStore.contactUnlocked
}
function isCurrent(zoneId: string): boolean {
const path = route.path.toLowerCase()
const routeMap: Record<string, string[]> = {
projets: ['projets', 'projects'],
competences: ['competences', 'skills'],
temoignages: ['temoignages', 'testimonials'],
parcours: ['parcours', 'journey'],
contact: ['contact'],
}
return routeMap[zoneId]?.some((segment) => path.includes(segment)) ?? false
}
</script>
<template>
<div class="chemin-libre h-full overflow-y-auto pb-safe">
<!-- Header avec handle -->
<div class="sticky top-0 bg-sky-dark-50 pt-4 pb-2 z-10">
<div class="w-12 h-1 bg-sky-dark-100 rounded-full mx-auto mb-4" />
<h2 class="text-xl font-ui font-bold text-sky-text text-center mb-4">
{{ t('cheminLibre.title') }}
</h2>
</div>
<!-- Liste des zones avec ligne de connexion -->
<div class="relative px-4 pb-8">
<!-- Ligne de connexion verticale -->
<div class="absolute left-12 top-8 bottom-8 w-0.5 bg-sky-dark-100" />
<!-- Zones -->
<div class="space-y-4 relative z-10">
<div
v-for="zone in mapZones"
:key="zone.id"
class="relative"
>
<!-- Point sur la ligne -->
<div
class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 rounded-full border-2 z-10"
:class="[
isVisited(zone.id) ? 'bg-green-400 border-green-400' : '',
isLocked(zone.id) ? 'bg-gray-500 border-gray-500' : '',
!isVisited(zone.id) && !isLocked(zone.id) ? 'bg-sky-dark border-sky-accent' : '',
]"
/>
<!-- Card avec padding gauche pour la ligne -->
<div class="pl-12">
<ZoneCard
:zone="zone"
:is-visited="isVisited(zone.id)"
:is-locked="isLocked(zone.id)"
:is-current="isCurrent(zone.id)"
@select="handleZoneSelect(zone)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 1rem);
}
</style>