App Nuxt 3 + Nuxt UI qui lit les projets du CMS Grav et compose des portfolios A3 paysage exportés en PDF (gabarit book_v3) : composeur WYSIWYG, templates cover/toc/projet/grille/page libre, pipeline Playwright + Ghostscript, gel du contenu à la génération, partage public opt-in. Dépôt autonome : Dockerfile (base Playwright + Ghostscript + ffmpeg + rsync) inclus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.7 KiB
Vue
71 lines
2.7 KiB
Vue
<script setup lang="ts">
|
|
// Sélecteur d'un média (image) parmi tous les projets — utilisé pour l'image
|
|
// de couverture et les médias des pages libres. Renvoie "projectId/filename".
|
|
import type { Project } from '~~/types'
|
|
|
|
const open = defineModel<boolean>('open', { required: true })
|
|
const emit = defineEmits<{ pick: [ref: string] }>()
|
|
|
|
const { data } = await useFetch<{ projects: Project[] }>('/api/projects')
|
|
const search = ref('')
|
|
const selectedProject = ref<Project | null>(null)
|
|
|
|
const filtered = computed(() =>
|
|
(data.value?.projects ?? []).filter(
|
|
p => p.published && (!search.value || p.title.toLowerCase().includes(search.value.toLowerCase())),
|
|
),
|
|
)
|
|
|
|
function pick(project: Project, filename: string) {
|
|
emit('pick', `${project.id}/${filename}`)
|
|
open.value = false
|
|
selectedProject.value = null
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UModal v-model:open="open" :title="selectedProject ? selectedProject.title : 'Choisir une image'" :ui="{ content: 'max-w-3xl' }">
|
|
<template #body>
|
|
<template v-if="!selectedProject">
|
|
<UInput v-model="search" icon="i-lucide-search" placeholder="Rechercher un projet…" class="mb-4 w-full" autofocus />
|
|
<div class="grid max-h-[55vh] grid-cols-4 gap-3 overflow-y-auto pr-1">
|
|
<button
|
|
v-for="p in filtered"
|
|
:key="p.id"
|
|
type="button"
|
|
class="overflow-hidden rounded-lg border border-neutral-200 text-left hover:shadow-md dark:border-neutral-700"
|
|
@click="selectedProject = p"
|
|
>
|
|
<div class="aspect-video bg-neutral-100 dark:bg-neutral-800">
|
|
<img v-if="projectThumb(p, 320)" :src="projectThumb(p, 320)!" class="size-full object-cover" :alt="p.title" loading="lazy">
|
|
</div>
|
|
<p class="truncate p-2 text-xs font-medium">{{ p.title }}</p>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<UButton
|
|
icon="i-lucide-arrow-left"
|
|
label="Retour aux projets"
|
|
variant="ghost"
|
|
color="neutral"
|
|
size="sm"
|
|
class="mb-3"
|
|
@click="selectedProject = null"
|
|
/>
|
|
<div class="grid max-h-[55vh] grid-cols-4 gap-3 overflow-y-auto pr-1">
|
|
<button
|
|
v-for="m in selectedProject.media.filter(m => m.type === 'image')"
|
|
:key="m.filename"
|
|
type="button"
|
|
class="aspect-video overflow-hidden rounded-lg border border-neutral-200 hover:shadow-md dark:border-neutral-700"
|
|
@click="pick(selectedProject, m.filename)"
|
|
>
|
|
<img :src="mediaUrl(selectedProject.id, m.filename, 320)" class="size-full object-cover" :alt="m.filename" loading="lazy">
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</UModal>
|
|
</template>
|