Refactoring complet : contenu livre, config unique, routes, admin et light mode
- Source unique : supprime app/data/librodrome.config.yml, renomme site/ en bookplayer.config.yml - Morceaux : renommés avec slugs lisibles, fichiers audio renommés, inversion ch2↔ch3 corrigée - Chapitres : 11 fichiers .md réécrits avec le vrai contenu du livre (synthèse fidèle du PDF) - Routes : /lire → /modele-eco, /ecouter → /en-musique, redirections 301 - Admin chapitres : champs structurés (titre, description, temps lecture), compteur mots - Éditeur markdown : mode split, plein écran, support Tab, meilleur rendu aperçu - Admin morceaux : drag & drop, ajout/suppression, gestion playlist - Light mode : palettes printemps/été plus saturées et contrastées, teintes primary - Raccourcis clavier player : espace, flèches gauche/droite - Paroles : toggle supprimé, toujours visibles et scrollables - Nouvelles pages : autonomie, evenement Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,24 +6,36 @@
|
||||
← Chapitres
|
||||
</NuxtLink>
|
||||
<h1 class="font-display text-2xl font-bold text-white mt-1">
|
||||
{{ chapter?.slug }}
|
||||
{{ chapterTitle || slug }}
|
||||
</h1>
|
||||
<span class="text-xs text-white/30 font-mono">{{ slug }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span v-if="wordCount" class="text-xs text-white/30">{{ wordCount }} mots</span>
|
||||
<AdminSaveButton :saving="saving" :saved="saved" @save="save" />
|
||||
</div>
|
||||
<AdminSaveButton :saving="saving" :saved="saved" @save="save" />
|
||||
</div>
|
||||
|
||||
<template v-if="chapter">
|
||||
<AdminFormSection title="Frontmatter" open>
|
||||
<textarea
|
||||
v-model="frontmatter"
|
||||
class="fm-textarea"
|
||||
rows="6"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<AdminFormSection title="Métadonnées">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="field-label">Titre</label>
|
||||
<input v-model="title" class="field-input" placeholder="Titre du chapitre" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field-label">Temps de lecture</label>
|
||||
<input v-model="readingTime" class="field-input" placeholder="15 min" />
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="field-label">Description</label>
|
||||
<input v-model="description" class="field-input" placeholder="Description courte pour le SEO" />
|
||||
</div>
|
||||
</div>
|
||||
</AdminFormSection>
|
||||
|
||||
<AdminFormSection title="Contenu Markdown" open>
|
||||
<AdminMarkdownEditor v-model="body" :rows="30" />
|
||||
<AdminFormSection title="Contenu" open>
|
||||
<AdminMarkdownEditor v-model="body" :rows="35" />
|
||||
</AdminFormSection>
|
||||
</template>
|
||||
</div>
|
||||
@@ -40,16 +52,33 @@ const slug = computed(() => route.params.slug as string)
|
||||
|
||||
const { data: chapter } = await useFetch(() => `/api/admin/chapters/${slug.value}`)
|
||||
|
||||
const frontmatter = ref('')
|
||||
const title = ref('')
|
||||
const description = ref('')
|
||||
const readingTime = ref('')
|
||||
const body = ref('')
|
||||
|
||||
const chapterTitle = computed(() => title.value)
|
||||
const wordCount = computed(() => {
|
||||
if (!body.value) return 0
|
||||
return body.value.trim().split(/\s+/).filter(Boolean).length
|
||||
})
|
||||
|
||||
watch(chapter, (val) => {
|
||||
if (val) {
|
||||
frontmatter.value = val.frontmatter ?? ''
|
||||
// Parse frontmatter fields
|
||||
const fm = val.frontmatter ?? ''
|
||||
title.value = extractFmField(fm, 'title')
|
||||
description.value = extractFmField(fm, 'description')
|
||||
readingTime.value = extractFmField(fm, 'readingTime')
|
||||
body.value = val.body ?? ''
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function extractFmField(fm: string, field: string): string {
|
||||
const match = fm.match(new RegExp(`^${field}:\\s*"?([^"\\n]*)"?`, 'm'))
|
||||
return match ? match[1].trim() : ''
|
||||
}
|
||||
|
||||
const saving = ref(false)
|
||||
const saved = ref(false)
|
||||
|
||||
@@ -57,12 +86,17 @@ async function save() {
|
||||
saving.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
const order = chapter.value?.frontmatter?.match(/order:\s*(\d+)/)?.[1] ?? '1'
|
||||
const frontmatter = [
|
||||
`title: "${title.value}"`,
|
||||
`description: "${description.value}"`,
|
||||
`order: ${order}`,
|
||||
`readingTime: "${readingTime.value}"`,
|
||||
].join('\n')
|
||||
|
||||
await $fetch(`/api/admin/chapters/${slug.value}`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
frontmatter: frontmatter.value,
|
||||
body: body.value,
|
||||
},
|
||||
body: { frontmatter, body: body.value },
|
||||
})
|
||||
saved.value = true
|
||||
setTimeout(() => { saved.value = false }, 2000)
|
||||
@@ -74,20 +108,24 @@ async function save() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fm-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid hsl(20 8% 18%);
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(20 8% 4%);
|
||||
color: hsl(36 80% 76%);
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.7;
|
||||
resize: vertical;
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(20 8% 50%);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.fm-textarea:focus {
|
||||
.field-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid hsl(20 8% 18%);
|
||||
background: hsl(20 8% 6%);
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(12 76% 48% / 0.5);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="font-display text-2xl font-bold text-white mb-6">Chapitres</h1>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="font-display text-2xl font-bold text-white">Chapitres</h1>
|
||||
<AdminSaveButton :saving="saving" :saved="saved" @save="saveOrder" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<NuxtLink
|
||||
v-for="chapter in chapters"
|
||||
<div
|
||||
v-for="(chapter, i) in chapters"
|
||||
:key="chapter.slug"
|
||||
:to="`/admin/book/${chapter.slug}`"
|
||||
class="chapter-item"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(i, $event)"
|
||||
@dragover.prevent="onDragOver(i)"
|
||||
@dragend="onDragEnd"
|
||||
:class="{ 'chapter-item--dragging': dragIdx === i, 'chapter-item--over': dropIdx === i && dropIdx !== dragIdx }"
|
||||
>
|
||||
<span class="chapter-order">{{ String(chapter.order ?? 0).padStart(2, '0') }}</span>
|
||||
<span class="chapter-title">{{ chapter.title }}</span>
|
||||
<div class="i-lucide-chevron-right h-4 w-4 text-white/20" />
|
||||
</NuxtLink>
|
||||
<div class="drag-handle" aria-label="Réordonner">
|
||||
<div class="i-lucide-grip-vertical h-4 w-4" />
|
||||
</div>
|
||||
<span class="chapter-order">{{ String(i + 1).padStart(2, '0') }}</span>
|
||||
<NuxtLink
|
||||
:to="`/admin/book/${chapter.slug}`"
|
||||
class="chapter-title"
|
||||
>
|
||||
{{ chapter.title }}
|
||||
</NuxtLink>
|
||||
<button
|
||||
class="delete-btn"
|
||||
@click="removeChapter(chapter.slug)"
|
||||
aria-label="Supprimer"
|
||||
>
|
||||
<div class="i-lucide-trash-2 h-4 w-4" />
|
||||
</button>
|
||||
<NuxtLink :to="`/admin/book/${chapter.slug}`">
|
||||
<div class="i-lucide-chevron-right h-4 w-4 text-white/20" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add chapter -->
|
||||
<div class="mt-6 flex items-end gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs text-white/40 mb-1">Titre</label>
|
||||
<input v-model="newTitle" class="admin-input w-full" placeholder="Nouveau chapitre" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/40 mb-1">Slug</label>
|
||||
<input v-model="newSlug" class="admin-input w-full font-mono text-xs" placeholder="12-slug" />
|
||||
</div>
|
||||
<button class="add-btn" @click="addChapter" :disabled="!newTitle || !newSlug">
|
||||
<div class="i-lucide-plus h-4 w-4" />
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,7 +63,74 @@ definePageMeta({
|
||||
middleware: 'admin',
|
||||
})
|
||||
|
||||
const { data: chapters } = await useFetch('/api/admin/chapters')
|
||||
const { data: chapters, refresh } = await useFetch<any[]>('/api/admin/chapters')
|
||||
const saving = ref(false)
|
||||
const saved = ref(false)
|
||||
const newTitle = ref('')
|
||||
const newSlug = ref('')
|
||||
|
||||
// Drag & drop state
|
||||
const dragIdx = ref<number | null>(null)
|
||||
const dropIdx = ref<number | null>(null)
|
||||
|
||||
function onDragStart(i: number, e: DragEvent) {
|
||||
dragIdx.value = i
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(i: number) {
|
||||
dropIdx.value = i
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
if (dragIdx.value !== null && dropIdx.value !== null && dragIdx.value !== dropIdx.value && chapters.value) {
|
||||
const [moved] = chapters.value.splice(dragIdx.value, 1)
|
||||
chapters.value.splice(dropIdx.value, 0, moved)
|
||||
}
|
||||
dragIdx.value = null
|
||||
dropIdx.value = null
|
||||
}
|
||||
|
||||
async function saveOrder() {
|
||||
if (!chapters.value) return
|
||||
saving.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
const orderedChapters = chapters.value.map((ch: any, i: number) => ({
|
||||
slug: ch.slug,
|
||||
order: i + 1,
|
||||
}))
|
||||
await $fetch('/api/admin/chapters', {
|
||||
method: 'PUT',
|
||||
body: { chapters: orderedChapters },
|
||||
})
|
||||
saved.value = true
|
||||
setTimeout(() => { saved.value = false }, 2000)
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addChapter() {
|
||||
if (!newTitle.value || !newSlug.value) return
|
||||
const order = (chapters.value?.length ?? 0) + 1
|
||||
await $fetch('/api/admin/chapters', {
|
||||
method: 'POST',
|
||||
body: { slug: newSlug.value, title: newTitle.value, order },
|
||||
})
|
||||
newTitle.value = ''
|
||||
newSlug.value = ''
|
||||
await refresh()
|
||||
}
|
||||
|
||||
async function removeChapter(slug: string) {
|
||||
if (!confirm(`Supprimer le chapitre "${slug}" ?`)) return
|
||||
await $fetch(`/api/admin/chapters/${slug}`, { method: 'DELETE' })
|
||||
await refresh()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -34,7 +141,6 @@ const { data: chapters } = await useFetch('/api/admin/chapters')
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid hsl(20 8% 14%);
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@@ -43,6 +149,25 @@ const { data: chapters } = await useFetch('/api/admin/chapters')
|
||||
background: hsl(20 8% 6%);
|
||||
}
|
||||
|
||||
.chapter-item--dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.chapter-item--over {
|
||||
border-top: 2px solid hsl(12 76% 48%);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
padding: 0.25rem;
|
||||
color: hsl(20 8% 35%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.chapter-order {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.85rem;
|
||||
@@ -55,5 +180,64 @@ const { data: chapters } = await useFetch('/api/admin/chapters')
|
||||
flex: 1;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chapter-title:hover {
|
||||
color: hsl(12 76% 68%);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(0 60% 50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: hsl(0 60% 50% / 0.1);
|
||||
}
|
||||
|
||||
.admin-input {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid hsl(20 8% 18%);
|
||||
background: hsl(20 8% 6%);
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.admin-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(12 76% 48% / 0.5);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid hsl(20 8% 25%);
|
||||
background: none;
|
||||
color: hsl(20 8% 55%);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-btn:hover:not(:disabled) {
|
||||
border-color: hsl(12 76% 48% / 0.5);
|
||||
color: hsl(12 76% 68%);
|
||||
}
|
||||
|
||||
.add-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,14 +6,22 @@
|
||||
</div>
|
||||
|
||||
<template v-if="config">
|
||||
<AdminFormSection title="Métadonnées des chansons" open>
|
||||
<AdminFormSection title="Morceaux" open>
|
||||
<div
|
||||
v-for="(song, i) in config.songs"
|
||||
:key="i"
|
||||
:key="song.id"
|
||||
class="song-row"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(i, $event)"
|
||||
@dragover.prevent="onDragOver(i)"
|
||||
@dragend="onDragEnd"
|
||||
:class="{ 'song-row--dragging': dragIdx === i, 'song-row--over': dropIdx === i && dropIdx !== dragIdx }"
|
||||
>
|
||||
<div class="drag-handle" aria-label="Réordonner">
|
||||
<div class="i-lucide-grip-vertical h-4 w-4" />
|
||||
</div>
|
||||
<span class="song-num">{{ i + 1 }}</span>
|
||||
<div class="flex-1 grid gap-2 sm:grid-cols-2">
|
||||
<div class="flex-1 grid gap-2 sm:grid-cols-3">
|
||||
<input
|
||||
v-model="song.title"
|
||||
class="admin-input"
|
||||
@@ -24,8 +32,33 @@
|
||||
class="admin-input"
|
||||
placeholder="/audio/fichier.mp3"
|
||||
/>
|
||||
<input
|
||||
v-model="song.id"
|
||||
class="admin-input font-mono text-xs"
|
||||
placeholder="identifiant-slug"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<textarea
|
||||
v-model="song.lyrics"
|
||||
class="admin-input lyrics-textarea"
|
||||
placeholder="Paroles..."
|
||||
rows="2"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="delete-btn"
|
||||
@click="removeSong(i)"
|
||||
aria-label="Supprimer"
|
||||
>
|
||||
<div class="i-lucide-trash-2 h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="add-btn" @click="addSong">
|
||||
<div class="i-lucide-plus h-4 w-4" />
|
||||
Ajouter un morceau
|
||||
</button>
|
||||
</AdminFormSection>
|
||||
</template>
|
||||
</div>
|
||||
@@ -37,14 +70,80 @@ definePageMeta({
|
||||
middleware: 'admin',
|
||||
})
|
||||
|
||||
const { data: config } = await useFetch('/api/content/config')
|
||||
const { data: config } = await useFetch<any>('/api/content/config')
|
||||
const saving = ref(false)
|
||||
const saved = ref(false)
|
||||
|
||||
// Drag & drop state
|
||||
const dragIdx = ref<number | null>(null)
|
||||
const dropIdx = ref<number | null>(null)
|
||||
|
||||
function onDragStart(i: number, e: DragEvent) {
|
||||
dragIdx.value = i
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(i: number) {
|
||||
dropIdx.value = i
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
if (dragIdx.value !== null && dropIdx.value !== null && dragIdx.value !== dropIdx.value && config.value) {
|
||||
const songs = config.value.songs
|
||||
const [moved] = songs.splice(dragIdx.value, 1)
|
||||
songs.splice(dropIdx.value, 0, moved)
|
||||
// Sync defaultPlaylistOrder
|
||||
config.value.defaultPlaylistOrder = songs.map((s: any) => s.id)
|
||||
}
|
||||
dragIdx.value = null
|
||||
dropIdx.value = null
|
||||
}
|
||||
|
||||
function addSong() {
|
||||
if (!config.value) return
|
||||
const newSong = {
|
||||
id: `nouveau-morceau-${Date.now()}`,
|
||||
title: '',
|
||||
artist: 'Yvv',
|
||||
file: '/audio/',
|
||||
duration: 0,
|
||||
lyrics: '',
|
||||
tags: [],
|
||||
}
|
||||
config.value.songs.push(newSong)
|
||||
config.value.defaultPlaylistOrder.push(newSong.id)
|
||||
}
|
||||
|
||||
function removeSong(i: number) {
|
||||
if (!config.value) return
|
||||
const songId = config.value.songs[i].id
|
||||
config.value.songs.splice(i, 1)
|
||||
// Remove from defaultPlaylistOrder
|
||||
const orderIdx = config.value.defaultPlaylistOrder.indexOf(songId)
|
||||
if (orderIdx !== -1) config.value.defaultPlaylistOrder.splice(orderIdx, 1)
|
||||
// Clean chapterSongs
|
||||
config.value.chapterSongs = config.value.chapterSongs.filter((cs: any) => cs.songId !== songId)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
saved.value = false
|
||||
try {
|
||||
// Regenerate IDs from titles for new songs
|
||||
for (const song of config.value!.songs) {
|
||||
if (song.id.startsWith('nouveau-morceau-')) {
|
||||
song.id = song.title
|
||||
.toLowerCase()
|
||||
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
}
|
||||
// Sync playlist order
|
||||
config.value!.defaultPlaylistOrder = config.value!.songs.map((s: any) => s.id)
|
||||
|
||||
await $fetch('/api/admin/content/config', {
|
||||
method: 'PUT',
|
||||
body: config.value,
|
||||
@@ -61,10 +160,31 @@ async function save() {
|
||||
<style scoped>
|
||||
.song-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
padding: 0.75rem 0.5rem;
|
||||
border-bottom: 1px solid hsl(20 8% 10%);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.song-row--dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.song-row--over {
|
||||
border-top: 2px solid hsl(12 76% 48%);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
padding: 0.25rem;
|
||||
color: hsl(20 8% 35%);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.song-num {
|
||||
@@ -73,6 +193,8 @@ async function save() {
|
||||
color: hsl(20 8% 40%);
|
||||
width: 1.25rem;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.admin-input {
|
||||
@@ -89,4 +211,45 @@ async function save() {
|
||||
outline: none;
|
||||
border-color: hsl(12 76% 48% / 0.5);
|
||||
}
|
||||
|
||||
.lyrics-textarea {
|
||||
resize: vertical;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(0 60% 50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: hsl(0 60% 50% / 0.1);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px dashed hsl(20 8% 25%);
|
||||
background: none;
|
||||
color: hsl(20 8% 55%);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
border-color: hsl(12 76% 48% / 0.5);
|
||||
color: hsl(12 76% 68%);
|
||||
}
|
||||
</style>
|
||||
|
||||
168
app/pages/autonomie.vue
Normal file
168
app/pages/autonomie.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden section-padding">
|
||||
<!-- Shadok jardinier: character with watering can and plant -->
|
||||
<svg class="shadok-jardinier" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
||||
<!-- Body -->
|
||||
<ellipse cx="110" cy="160" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
|
||||
<!-- Head -->
|
||||
<circle cx="110" cy="96" r="25" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Straw hat -->
|
||||
<ellipse cx="110" cy="78" rx="35" ry="8" fill="currentColor" opacity="0.4"/>
|
||||
<path d="M85 78 Q110 60 135 78" fill="currentColor" opacity="0.35"/>
|
||||
<!-- Eyes (focused, looking down at plant) -->
|
||||
<circle cx="102" cy="94" r="4" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="120" cy="94" r="4" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="103" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
|
||||
<circle cx="121" cy="95" r="1.8" fill="currentColor" opacity="0.5"/>
|
||||
<!-- Smile -->
|
||||
<path d="M103 106 Q110 111 118 106" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<!-- Arm holding watering can -->
|
||||
<line x1="70" y1="150" x2="40" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Watering can -->
|
||||
<rect x="20" y="165" width="30" height="20" rx="3" fill="currentColor" opacity="0.4"/>
|
||||
<line x1="20" y1="168" x2="10" y2="160" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
|
||||
<!-- Water drops -->
|
||||
<circle cx="12" cy="165" r="1.5" fill="currentColor" opacity="0.25"/>
|
||||
<circle cx="8" cy="170" r="1.5" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="15" cy="172" r="1.5" fill="currentColor" opacity="0.2"/>
|
||||
<!-- Other arm -->
|
||||
<line x1="150" y1="150" x2="170" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Legs -->
|
||||
<line x1="95" y1="205" x2="85" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="125" y1="205" x2="135" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Plant -->
|
||||
<line x1="180" y1="220" x2="180" y2="180" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.4"/>
|
||||
<path d="M180 195 Q195 185 190 175" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
|
||||
<path d="M180 205 Q165 195 168 185" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
|
||||
<path d="M180 185 Q192 172 188 165" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<!-- Pot -->
|
||||
<path d="M170 220 L175 240 L185 240 L190 220 Z" fill="currentColor" opacity="0.35"/>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok bâtisseur: character with trowel building a wall -->
|
||||
<svg class="shadok-batisseur" viewBox="0 0 260 300" fill="none" aria-hidden="true">
|
||||
<!-- Body -->
|
||||
<ellipse cx="130" cy="150" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
|
||||
<!-- Head -->
|
||||
<circle cx="130" cy="86" r="25" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Hard hat -->
|
||||
<ellipse cx="130" cy="68" rx="28" ry="6" fill="currentColor" opacity="0.4"/>
|
||||
<rect x="108" y="60" width="44" height="10" rx="3" fill="currentColor" opacity="0.35"/>
|
||||
<!-- Eyes (determined) -->
|
||||
<circle cx="122" cy="84" r="4" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="140" cy="84" r="4" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="123" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
|
||||
<circle cx="141" cy="83" r="1.8" fill="currentColor" opacity="0.5"/>
|
||||
<!-- Grin -->
|
||||
<path d="M123 96 Q130 101 138 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<!-- Arm with trowel -->
|
||||
<line x1="170" y1="140" x2="210" y2="120" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Trowel -->
|
||||
<polygon points="210,115 230,110 225,120 210,122" fill="currentColor" opacity="0.45"/>
|
||||
<line x1="210" y1="118" x2="200" y2="125" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.4"/>
|
||||
<!-- Other arm -->
|
||||
<line x1="90" y1="145" x2="65" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Legs -->
|
||||
<line x1="115" y1="195" x2="105" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="145" y1="195" x2="155" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Wall (bricks) -->
|
||||
<rect x="40" y="200" width="50" height="16" rx="1" fill="currentColor" opacity="0.3"/>
|
||||
<rect x="45" y="183" width="40" height="16" rx="1" fill="currentColor" opacity="0.28"/>
|
||||
<rect x="50" y="166" width="30" height="16" rx="1" fill="currentColor" opacity="0.25"/>
|
||||
<!-- Brick lines -->
|
||||
<line x1="65" y1="200" x2="65" y2="216" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
||||
<line x1="55" y1="183" x2="55" y2="199" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
||||
</svg>
|
||||
|
||||
<div class="container-content">
|
||||
<header class="mb-12 text-center">
|
||||
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
|
||||
<h1 class="page-title font-display font-bold tracking-tight text-white">
|
||||
{{ content?.title }}
|
||||
</h1>
|
||||
<p class="mt-4 mx-auto max-w-2xl text-white/60">
|
||||
{{ content?.description }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="mx-auto max-w-3xl flex flex-col gap-6">
|
||||
<div
|
||||
v-for="(extract, i) in content?.extracts"
|
||||
:key="i"
|
||||
class="card-surface"
|
||||
>
|
||||
<p class="mb-2 font-mono text-xs tracking-widest text-accent uppercase">
|
||||
{{ extract.chapter }}
|
||||
</p>
|
||||
<blockquote class="border-l-2 border-primary/30 pl-4 text-white/70 italic leading-relaxed whitespace-pre-line">
|
||||
{{ extract.text }}
|
||||
</blockquote>
|
||||
<div class="mt-4">
|
||||
<NuxtLink
|
||||
:to="`/modele-eco/${extract.chapterSlug}`"
|
||||
class="inline-flex items-center gap-1 text-sm text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
Lire le chapitre
|
||||
<div class="i-lucide-arrow-right h-3.5 w-3.5" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'default',
|
||||
})
|
||||
|
||||
const { data: content } = await usePageContent('autonomie')
|
||||
|
||||
useHead({
|
||||
title: content.value?.meta?.title ?? 'Autonomie',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title {
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
}
|
||||
|
||||
.shadok-jardinier {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
top: 5%;
|
||||
width: clamp(100px, 14vw, 200px);
|
||||
opacity: 0.28;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-primary));
|
||||
animation: shadok-float-jardinier 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shadok-batisseur {
|
||||
position: absolute;
|
||||
right: 2%;
|
||||
bottom: 5%;
|
||||
width: clamp(110px, 15vw, 210px);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-accent));
|
||||
animation: shadok-float-batisseur 11s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shadok-float-jardinier {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes shadok-float-batisseur {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-8px) rotate(1deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shadok-jardinier { display: none; }
|
||||
.shadok-batisseur { display: none; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,35 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden section-padding">
|
||||
<!-- Shadok danseur: character dancing with music notes -->
|
||||
<svg class="shadok-danseur" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
||||
<!-- Body (dynamic pose, leaning) -->
|
||||
<ellipse cx="120" cy="155" rx="38" ry="46" fill="currentColor" opacity="0.85"/>
|
||||
<!-- Head (tilted with joy) -->
|
||||
<ellipse cx="125" cy="92" rx="24" ry="23" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Eyes (happy, squinted) -->
|
||||
<path d="M114 88 Q118 84 122 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<path d="M130 88 Q134 84 138 88" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<!-- Big smile -->
|
||||
<path d="M116 100 Q125 108 134 100" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.35"/>
|
||||
<!-- Arms thrown up (dancing) -->
|
||||
<line x1="85" y1="140" x2="50" y2="100" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="155" y1="140" x2="190" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Hands -->
|
||||
<circle cx="50" cy="98" r="4" fill="currentColor" opacity="0.4"/>
|
||||
<circle cx="190" cy="103" r="4" fill="currentColor" opacity="0.4"/>
|
||||
<!-- Legs (one kicked up) -->
|
||||
<line x1="105" y1="198" x2="80" y2="255" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="135" y1="198" x2="170" y2="240" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Feet -->
|
||||
<path d="M80 255 L68 258 M80 255 L75 261" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M170 240 L180 238 M170 240 L175 246" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
|
||||
<!-- Music notes floating around -->
|
||||
<text x="42" y="82" fill="currentColor" opacity="0.3" font-size="18">♪</text>
|
||||
<text x="195" y="88" fill="currentColor" opacity="0.25" font-size="16">♫</text>
|
||||
<text x="60" y="65" fill="currentColor" opacity="0.2" font-size="14">♩</text>
|
||||
<text x="180" y="72" fill="currentColor" opacity="0.2" font-size="20">♪</text>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok DJ: character with headphones behind a turntable -->
|
||||
<svg class="shadok-dj" viewBox="0 0 260 300" fill="none" aria-hidden="true">
|
||||
<!-- Body -->
|
||||
@@ -23,7 +53,7 @@
|
||||
<line x1="170" y1="150" x2="205" y2="195" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Turntable body -->
|
||||
<rect x="30" y="200" width="200" height="18" rx="4" fill="currentColor" opacity="0.4"/>
|
||||
<!-- Turntable platter (ellipse for perspective) -->
|
||||
<!-- Turntable platter -->
|
||||
<ellipse cx="130" cy="200" rx="55" ry="15" fill="currentColor" opacity="0.25"/>
|
||||
<ellipse cx="130" cy="200" rx="55" ry="15" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.35"/>
|
||||
<!-- Record center -->
|
||||
@@ -37,15 +67,38 @@
|
||||
</svg>
|
||||
|
||||
<div class="container-content">
|
||||
<header class="mb-12 text-center">
|
||||
<p class="mb-2 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
|
||||
<h1 class="page-title font-display font-bold tracking-tight text-white">
|
||||
{{ content?.title }}
|
||||
</h1>
|
||||
<p class="mt-4 mx-auto max-w-2xl text-white/60">
|
||||
{{ content?.description }}
|
||||
</p>
|
||||
</header>
|
||||
<!-- Hero section with book cover -->
|
||||
<div class="mb-12 grid items-center gap-8 md:grid-cols-2">
|
||||
<div class="book-cover-wrapper">
|
||||
<div class="book-cover-3d">
|
||||
<img
|
||||
:src="homeContent?.book.coverImage"
|
||||
:alt="homeContent?.book.coverAlt"
|
||||
class="book-cover-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center md:text-left">
|
||||
<p class="mb-2 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
|
||||
<h1 class="page-title font-display font-bold tracking-tight text-white">
|
||||
{{ content?.title }}
|
||||
</h1>
|
||||
<p class="mt-4 text-white/60">
|
||||
{{ content?.description }}
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:gap-4 justify-center md:justify-start">
|
||||
<UiBaseButton @click="showBookPlayer = true">
|
||||
<div class="i-lucide-play mr-2 h-5 w-5" />
|
||||
Présentation musicale
|
||||
</UiBaseButton>
|
||||
<UiBaseButton variant="accent" @click="showPdfReader = true">
|
||||
<div class="i-lucide-book-open mr-2 h-5 w-5" />
|
||||
Lire le livre
|
||||
</UiBaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + view toggle -->
|
||||
<div class="mb-6 flex items-center justify-between gap-4">
|
||||
@@ -99,6 +152,9 @@
|
||||
{{ content?.noResults }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BookPlayer v-model="showBookPlayer" />
|
||||
<BookPdfReader v-model="showPdfReader" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -107,10 +163,11 @@ definePageMeta({
|
||||
layout: 'default',
|
||||
})
|
||||
|
||||
const { data: content } = await usePageContent('ecouter')
|
||||
const { data: content } = await usePageContent('en-musique')
|
||||
const { data: homeContent } = await usePageContent('home')
|
||||
|
||||
useHead({
|
||||
title: content.value?.meta?.title ?? 'Écouter',
|
||||
title: content.value?.meta?.title ?? 'En musique',
|
||||
})
|
||||
|
||||
const store = usePlayerStore()
|
||||
@@ -125,6 +182,8 @@ await loadFullPlaylist()
|
||||
|
||||
const search = ref('')
|
||||
const viewMode = ref<'list' | 'grid'>('list')
|
||||
const showBookPlayer = ref(false)
|
||||
const showPdfReader = ref(false)
|
||||
|
||||
const filteredSongs = computed(() => {
|
||||
const songs = bookData.getSongs()
|
||||
@@ -144,6 +203,50 @@ const filteredSongs = computed(() => {
|
||||
font-size: clamp(2rem, 5vw, 2.75rem);
|
||||
}
|
||||
|
||||
.book-cover-wrapper {
|
||||
perspective: 800px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.book-cover-3d {
|
||||
aspect-ratio: 3 / 4;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--color-text) / 0.1);
|
||||
box-shadow:
|
||||
0 12px 40px hsl(var(--color-text) / 0.15),
|
||||
0 0 0 1px hsl(var(--color-text) / 0.08);
|
||||
transition: transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
|
||||
box-shadow 0.5s ease;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.book-cover-3d:hover {
|
||||
transform: rotateY(-8deg) rotateX(3deg) scale(1.02);
|
||||
box-shadow:
|
||||
12px 16px 48px hsl(var(--color-text) / 0.2),
|
||||
0 0 0 1px hsl(var(--color-primary) / 0.2);
|
||||
}
|
||||
|
||||
.book-cover-img {
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.shadok-danseur {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
top: 3%;
|
||||
width: clamp(100px, 14vw, 200px);
|
||||
opacity: 0.28;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-primary));
|
||||
animation: shadok-float-danseur 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shadok-dj {
|
||||
position: absolute;
|
||||
right: 2%;
|
||||
@@ -155,12 +258,19 @@ const filteredSongs = computed(() => {
|
||||
animation: shadok-float-dj 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shadok-float-danseur {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
25% { transform: translateY(-8px) rotate(2deg); }
|
||||
75% { transform: translateY(-4px) rotate(-2deg); }
|
||||
}
|
||||
|
||||
@keyframes shadok-float-dj {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shadok-danseur { display: none; }
|
||||
.shadok-dj { display: none; }
|
||||
}
|
||||
</style>
|
||||
267
app/pages/evenement.vue
Normal file
267
app/pages/evenement.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden section-padding min-h-[70vh] flex items-center justify-center">
|
||||
<!-- Shadok jongleur: juggling coins (top-left) -->
|
||||
<svg class="shadok-juggler" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
||||
<!-- Body -->
|
||||
<ellipse cx="120" cy="160" rx="38" ry="46" fill="currentColor" opacity="0.85"/>
|
||||
<!-- Head -->
|
||||
<circle cx="120" cy="98" r="24" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Eyes (looking up at coins) -->
|
||||
<circle cx="112" cy="92" r="3.5" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="130" cy="92" r="3.5" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="113" cy="91" r="1.5" fill="currentColor" opacity="0.5"/>
|
||||
<circle cx="131" cy="91" r="1.5" fill="currentColor" opacity="0.5"/>
|
||||
<!-- Smile -->
|
||||
<path d="M112 108 Q120 114 128 108" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.35"/>
|
||||
<!-- Arms up (juggling) -->
|
||||
<line x1="85" y1="145" x2="55" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="155" y1="145" x2="185" y2="105" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Hands -->
|
||||
<circle cx="55" cy="103" r="4" fill="currentColor" opacity="0.4"/>
|
||||
<circle cx="185" cy="103" r="4" fill="currentColor" opacity="0.4"/>
|
||||
<!-- Juggling coins -->
|
||||
<circle cx="90" cy="55" r="8" fill="currentColor" opacity="0.35"/>
|
||||
<text x="86" y="59" fill="currentColor" opacity="0.5" font-size="10" font-weight="bold">$</text>
|
||||
<circle cx="120" cy="40" r="8" fill="currentColor" opacity="0.3"/>
|
||||
<text x="116" y="44" fill="currentColor" opacity="0.45" font-size="10" font-weight="bold">$</text>
|
||||
<circle cx="150" cy="50" r="8" fill="currentColor" opacity="0.32"/>
|
||||
<text x="146" y="54" fill="currentColor" opacity="0.48" font-size="10" font-weight="bold">$</text>
|
||||
<!-- Legs -->
|
||||
<line x1="105" y1="203" x2="95" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="135" y1="203" x2="145" y2="260" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok échelle: on a wobbly ladder (top-right) -->
|
||||
<svg class="shadok-ladder" viewBox="0 0 220 320" fill="none" aria-hidden="true">
|
||||
<!-- Ladder (tilting) -->
|
||||
<line x1="80" y1="50" x2="70" y2="300" stroke="currentColor" stroke-width="3" opacity="0.35"/>
|
||||
<line x1="150" y1="50" x2="140" y2="300" stroke="currentColor" stroke-width="3" opacity="0.35"/>
|
||||
<!-- Rungs -->
|
||||
<line x1="82" y1="80" x2="148" y2="80" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
||||
<line x1="83" y1="120" x2="147" y2="120" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
||||
<line x1="84" y1="160" x2="146" y2="160" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
||||
<line x1="85" y1="200" x2="145" y2="200" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
||||
<line x1="86" y1="240" x2="144" y2="240" stroke="currentColor" stroke-width="2.5" opacity="0.3"/>
|
||||
<!-- Shadok on top (arms out for balance) -->
|
||||
<ellipse cx="115" cy="68" rx="18" ry="14" fill="currentColor" opacity="0.85"/>
|
||||
<circle cx="115" cy="46" r="14" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Eyes (worried) -->
|
||||
<circle cx="110" cy="43" r="3" fill="currentColor" opacity="0.25"/>
|
||||
<circle cx="122" cy="43" r="3" fill="currentColor" opacity="0.25"/>
|
||||
<circle cx="110" cy="44" r="1.2" fill="currentColor" opacity="0.5"/>
|
||||
<circle cx="122" cy="44" r="1.2" fill="currentColor" opacity="0.5"/>
|
||||
<!-- Worried mouth -->
|
||||
<path d="M108 52 Q115 49 122 52" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<!-- Arms out (balancing) -->
|
||||
<line x1="97" y1="62" x2="60" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="133" y1="62" x2="170" y2="55" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok acrobate: doing a cartwheel (center) -->
|
||||
<svg class="shadok-acrobat" viewBox="0 0 260 240" fill="none" aria-hidden="true">
|
||||
<!-- Body (sideways, mid-cartwheel) -->
|
||||
<ellipse cx="130" cy="120" rx="30" ry="38" fill="currentColor" opacity="0.85" transform="rotate(45 130 120)"/>
|
||||
<!-- Head -->
|
||||
<circle cx="155" cy="82" r="20" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Eyes (dizzy/happy) -->
|
||||
<path d="M148 78 Q152 74 156 78" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<path d="M160 78 Q164 74 168 78" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<!-- Smile -->
|
||||
<path d="M150 90 Q158 95 165 90" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<!-- Arms (one touching ground, one up) -->
|
||||
<line x1="110" y1="100" x2="80" y2="130" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="150" y1="105" x2="185" y2="70" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Hand on ground -->
|
||||
<circle cx="78" cy="132" r="4" fill="currentColor" opacity="0.4"/>
|
||||
<!-- Legs (splayed in cartwheel) -->
|
||||
<line x1="125" y1="155" x2="100" y2="195" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="140" y1="150" x2="175" y2="175" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Motion lines -->
|
||||
<path d="M70 110 Q60 105 55 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||
<path d="M190 60 Q200 55 205 65" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok dormeur: sleeping on a cloud (bottom-left) -->
|
||||
<svg class="shadok-sleeper" viewBox="0 0 260 220" fill="none" aria-hidden="true">
|
||||
<!-- Cloud -->
|
||||
<ellipse cx="130" cy="150" rx="80" ry="25" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="80" cy="140" r="25" fill="currentColor" opacity="0.18"/>
|
||||
<circle cx="120" cy="130" r="30" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="165" cy="135" r="22" fill="currentColor" opacity="0.18"/>
|
||||
<circle cx="190" cy="142" r="18" fill="currentColor" opacity="0.15"/>
|
||||
<!-- Shadok body (lying down) -->
|
||||
<ellipse cx="130" cy="125" rx="35" ry="18" fill="currentColor" opacity="0.85"/>
|
||||
<!-- Head (on cloud, sideways) -->
|
||||
<ellipse cx="85" cy="118" rx="18" ry="16" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Closed eyes (sleeping) -->
|
||||
<path d="M76 115 Q80 112 84 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<path d="M88 115 Q92 112 96 115" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.4"/>
|
||||
<!-- Snooze bubbles -->
|
||||
<text x="70" y="100" fill="currentColor" opacity="0.3" font-size="12" font-weight="bold">z</text>
|
||||
<text x="60" y="85" fill="currentColor" opacity="0.25" font-size="16" font-weight="bold">z</text>
|
||||
<text x="48" y="68" fill="currentColor" opacity="0.2" font-size="20" font-weight="bold">z</text>
|
||||
<!-- Legs (curled) -->
|
||||
<path d="M165 125 Q180 130 175 140" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.5"/>
|
||||
<path d="M160 130 Q172 138 168 148" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" opacity="0.5"/>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok cuisinier: cooking in a cauldron (bottom-right) -->
|
||||
<svg class="shadok-cook" viewBox="0 0 240 300" fill="none" aria-hidden="true">
|
||||
<!-- Body -->
|
||||
<ellipse cx="120" cy="145" rx="38" ry="45" fill="currentColor" opacity="0.85"/>
|
||||
<!-- Head -->
|
||||
<circle cx="120" cy="85" r="24" fill="currentColor" opacity="0.8"/>
|
||||
<!-- Chef hat -->
|
||||
<ellipse cx="120" cy="62" rx="22" ry="18" fill="currentColor" opacity="0.35"/>
|
||||
<rect x="105" y="68" width="30" height="6" rx="1" fill="currentColor" opacity="0.4"/>
|
||||
<!-- Eyes (focused on cooking) -->
|
||||
<circle cx="112" cy="82" r="3.5" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="130" cy="82" r="3.5" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="113" cy="83" r="1.5" fill="currentColor" opacity="0.5"/>
|
||||
<circle cx="131" cy="83" r="1.5" fill="currentColor" opacity="0.5"/>
|
||||
<!-- Tongue out (concentrating) -->
|
||||
<path d="M115 96 Q120 100 125 96" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.3"/>
|
||||
<!-- Arm with ladle -->
|
||||
<line x1="155" y1="135" x2="185" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Ladle -->
|
||||
<line x1="185" y1="175" x2="175" y2="200" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" opacity="0.5"/>
|
||||
<ellipse cx="175" cy="205" rx="8" ry="5" fill="currentColor" opacity="0.35"/>
|
||||
<!-- Other arm -->
|
||||
<line x1="85" y1="140" x2="60" y2="175" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Legs -->
|
||||
<line x1="105" y1="188" x2="95" y2="250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="135" y1="188" x2="145" y2="250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Cauldron -->
|
||||
<path d="M55 220 Q55 260 120 260 Q185 260 185 220" fill="currentColor" opacity="0.3"/>
|
||||
<ellipse cx="120" cy="220" rx="65" ry="12" fill="currentColor" opacity="0.25"/>
|
||||
<ellipse cx="120" cy="220" rx="65" ry="12" stroke="currentColor" stroke-width="2" fill="none" opacity="0.35"/>
|
||||
<!-- Steam -->
|
||||
<path d="M95 210 Q90 195 95 185" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||
<path d="M120 208 Q118 190 122 180" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||
<path d="M145 210 Q148 195 143 185" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.2"/>
|
||||
</svg>
|
||||
|
||||
<div class="container-content relative z-10 text-center">
|
||||
<p class="mb-3 font-mono text-sm tracking-widest text-accent uppercase">{{ content?.kicker }}</p>
|
||||
<h1 class="page-title font-display font-extrabold tracking-tight text-white">
|
||||
{{ content?.title }}
|
||||
</h1>
|
||||
<p class="mt-4 text-lg text-white/50">
|
||||
{{ content?.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'default',
|
||||
})
|
||||
|
||||
const { data: content } = await usePageContent('evenement')
|
||||
|
||||
useHead({
|
||||
title: content.value?.meta?.title ?? 'Évènement',
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-title {
|
||||
font-size: clamp(2.5rem, 6vw, 3.5rem);
|
||||
}
|
||||
|
||||
.shadok-juggler {
|
||||
position: absolute;
|
||||
left: 4%;
|
||||
top: 5%;
|
||||
width: clamp(100px, 14vw, 190px);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-primary));
|
||||
animation: shadok-bounce-juggler 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shadok-ladder {
|
||||
position: absolute;
|
||||
right: 4%;
|
||||
top: 3%;
|
||||
width: clamp(90px, 12vw, 170px);
|
||||
opacity: 0.28;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-accent));
|
||||
animation: shadok-wobble-ladder 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shadok-acrobat {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 55%;
|
||||
transform: translateX(-50%);
|
||||
width: clamp(100px, 13vw, 180px);
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-primary));
|
||||
animation: shadok-spin-acrobat 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shadok-sleeper {
|
||||
position: absolute;
|
||||
left: 3%;
|
||||
bottom: 5%;
|
||||
width: clamp(110px, 15vw, 210px);
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-accent));
|
||||
animation: shadok-float-sleeper 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shadok-cook {
|
||||
position: absolute;
|
||||
right: 3%;
|
||||
bottom: 4%;
|
||||
width: clamp(100px, 14vw, 200px);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-primary));
|
||||
animation: shadok-bounce-cook 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shadok-bounce-juggler {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-12px); }
|
||||
60% { transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
@keyframes shadok-wobble-ladder {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(3deg); }
|
||||
75% { transform: rotate(-3deg); }
|
||||
}
|
||||
|
||||
@keyframes shadok-spin-acrobat {
|
||||
0% { transform: translateX(-50%) rotate(0deg); }
|
||||
25% { transform: translateX(-50%) rotate(15deg); }
|
||||
50% { transform: translateX(-50%) rotate(0deg); }
|
||||
75% { transform: translateX(-50%) rotate(-15deg); }
|
||||
100% { transform: translateX(-50%) rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes shadok-float-sleeper {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
@keyframes shadok-bounce-cook {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-10px); }
|
||||
70% { transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shadok-juggler { display: none; }
|
||||
.shadok-ladder { display: none; }
|
||||
.shadok-acrobat { display: none; }
|
||||
.shadok-sleeper { display: none; }
|
||||
.shadok-cook { display: none; }
|
||||
}
|
||||
</style>
|
||||
@@ -14,7 +14,7 @@
|
||||
<nav class="mt-16 flex items-center justify-between border-t border-white/8 pt-8">
|
||||
<NuxtLink
|
||||
v-if="prevChapter"
|
||||
:to="`/lire/${prevChapter.stem}`"
|
||||
:to="`/modele-eco/${prevChapter.stem}`"
|
||||
class="btn-ghost gap-2"
|
||||
>
|
||||
<div class="i-lucide-arrow-left h-4 w-4" />
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<NuxtLink
|
||||
v-if="nextChapter"
|
||||
:to="`/lire/${nextChapter.stem}`"
|
||||
:to="`/modele-eco/${nextChapter.stem}`"
|
||||
class="btn-ghost gap-2"
|
||||
>
|
||||
<span class="text-sm">{{ nextChapter.title }}</span>
|
||||
@@ -49,6 +49,30 @@
|
||||
<circle cx="87" cy="29" r="1.5" fill="currentColor" opacity="0.3"/>
|
||||
</svg>
|
||||
|
||||
<!-- Shadok scribe: character with quill and inkwell -->
|
||||
<svg class="shadok-scribe" viewBox="0 0 240 280" fill="none" aria-hidden="true">
|
||||
<ellipse cx="120" cy="155" rx="40" ry="48" fill="currentColor" opacity="0.85"/>
|
||||
<circle cx="120" cy="92" r="25" fill="currentColor" opacity="0.8"/>
|
||||
<ellipse cx="120" cy="72" rx="20" ry="8" fill="currentColor" opacity="0.35"/>
|
||||
<circle cx="112" cy="90" r="4" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="130" cy="90" r="4" fill="currentColor" opacity="0.2"/>
|
||||
<circle cx="113" cy="91" r="1.8" fill="currentColor" opacity="0.5"/>
|
||||
<circle cx="131" cy="91" r="1.8" fill="currentColor" opacity="0.5"/>
|
||||
<path d="M118 104 Q120 108 122 104" fill="currentColor" opacity="0.3"/>
|
||||
<line x1="160" y1="140" x2="190" y2="170" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<line x1="190" y1="170" x2="210" y2="145" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
|
||||
<path d="M210 145 Q215 140 212 135 Q208 138 210 145" fill="currentColor" opacity="0.3"/>
|
||||
<line x1="80" y1="148" x2="55" y2="180" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" opacity="0.6"/>
|
||||
<path d="M100 200 Q90 230 95 250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
|
||||
<path d="M140 200 Q150 230 145 250" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none" opacity="0.6"/>
|
||||
<rect x="45" y="185" width="20" height="18" rx="3" fill="currentColor" opacity="0.35"/>
|
||||
<ellipse cx="55" cy="185" rx="12" ry="4" fill="currentColor" opacity="0.3"/>
|
||||
<rect x="170" y="175" width="40" height="50" rx="2" fill="currentColor" opacity="0.2"/>
|
||||
<line x1="178" y1="188" x2="202" y2="188" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
||||
<line x1="178" y1="195" x2="200" y2="195" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
||||
<line x1="178" y1="202" x2="198" y2="202" stroke="currentColor" stroke-width="1" opacity="0.15"/>
|
||||
</svg>
|
||||
|
||||
<div class="container-content">
|
||||
<header class="mb-12 text-center">
|
||||
<p class="mb-2 font-mono text-sm tracking-widest text-primary uppercase">{{ content?.kicker }}</p>
|
||||
@@ -67,7 +91,7 @@
|
||||
:key="chapter.path"
|
||||
>
|
||||
<NuxtLink
|
||||
:to="`/lire/${chapter.stem}`"
|
||||
:to="`/modele-eco/${chapter.stem}`"
|
||||
class="card-surface flex items-start gap-4 group"
|
||||
>
|
||||
<span class="font-mono text-2xl font-bold text-primary/30 leading-none mt-1 w-10 text-right flex-shrink-0">
|
||||
@@ -102,7 +126,7 @@ definePageMeta({
|
||||
layout: 'default',
|
||||
})
|
||||
|
||||
const { data: content } = await usePageContent('lire')
|
||||
const { data: content } = await usePageContent('modele-eco')
|
||||
|
||||
useHead({
|
||||
title: content.value?.meta?.title ?? 'Table des matières',
|
||||
@@ -150,8 +174,26 @@ const { data: chapters } = await useAsyncData('book-toc', () =>
|
||||
50% { transform: translateY(-8px) rotate(-2deg); }
|
||||
}
|
||||
|
||||
.shadok-scribe {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 3%;
|
||||
transform: translateX(-50%);
|
||||
width: clamp(100px, 13vw, 180px);
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
color: hsl(var(--color-primary));
|
||||
animation: shadok-float-scribe 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shadok-float-scribe {
|
||||
0%, 100% { transform: translateX(-50%) translateY(0); }
|
||||
50% { transform: translateX(-50%) translateY(-8px); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.shadok-reader { display: none; }
|
||||
.shadok-stack { display: none; }
|
||||
.shadok-scribe { display: none; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user