ajout pro et tailwind

This commit is contained in:
2026-04-20 11:37:03 +02:00
parent c3f236eac9
commit 9e421b1d08
13 changed files with 2732 additions and 82 deletions

View File

@@ -1,13 +1,656 @@
<template>
<div>
<div class="programmer-orchestre-page bg-surface text-on-surface">
<div class="px-12 py-8 max-w-7xl mx-auto">
<!-- L'ORCHESTRE -->
<section class="mb-16">
<div class="flex items-center gap-4 mb-8">
<h3 class="text-2xl font-bold tracking-tight">L'ORCHESTRE</h3>
<div class="h-[2px] flex-1 bg-surface-container"></div>
</div>
<div class="grid grid-cols-12 gap-6">
<!-- Bio Card -->
<div class="col-span-12 md:col-span-4 bg-surface-container-lowest rounded-xl p-6 shadow-sm flex flex-col justify-between hover:bg-surface-container-low transition-colors group border border-outline-variant/10">
<div>
<div class="flex justify-between items-start mb-6">
<span class="material-symbols-outlined text-4xl text-primary/40 group-hover:text-primary transition-colors">description</span>
</div>
<h4 class="text-xl font-bold mb-2">Biographies de l'Orchestre</h4>
</div>
<div class="mt-8 space-y-2">
<div class="flex items-center justify-between p-3 bg-surface rounded-lg">
<span class="text-sm font-medium">
Bio<span v-if="programmer_content?.bios?.[0]?.ext">
({{ programmer_content.bios[0].ext.replace('.', '') }})
</span>
</span>
<span class="text-xs text-outline">
<template v-if="programmer_content?.bios?.[0]?.size">
{{ formatKoToMo(programmer_content.bios[0].size) }}
</template>
</span>
<a
v-if="programmer_content?.bios?.[0]?.url"
:href="programmer_content.bios[0].url"
:download="programmer_content.bios[0].name || true"
class="inline-flex cursor-pointer"
target="_blank"
rel="noopener noreferrer"
>
<span class="material-symbols-outlined text-primary text-xl">download</span>
</a>
</div>
<div class="flex items-center justify-between p-3 bg-surface rounded-lg">
<span class="text-sm font-medium">
Bio courte<span v-if="programmer_content?.bios?.[1]?.ext">
({{ programmer_content.bios[1].ext.replace('.', '') }})
</span>
</span>
<span class="text-xs text-outline">
<template v-if="programmer_content?.bios?.[1]?.size">
{{ formatKoToMo(programmer_content.bios[1].size) }}
</template>
</span>
<a
v-if="programmer_content?.bios?.[1]?.url"
:href="programmer_content.bios[1].url"
:download="programmer_content.bios[1].name || true"
class="inline-flex cursor-pointer"
target="_blank"
rel="noopener noreferrer"
>
<span class="material-symbols-outlined text-primary text-xl">download</span>
</a>
</div>
</div>
</div>
<!-- Contacts -->
<div class="col-span-12 md:col-span-4 lg:col-span-4 rounded-xl p-8 shadow-sm">
<h4 class="text-xl font-bold mb-6">Contacts</h4>
<div class="space-y-6">
<div
v-for="(contact, index) in programmer_content?.Contacts || []"
:key="contact.id"
class="flex items-start gap-4"
>
<div
:class="index % 2 === 0
? 'bg-secondary-container text-on-secondary-container'
: 'bg-tertiary-container text-on-tertiary-container'"
class="w-12 h-12 shrink-0 rounded-full flex items-center justify-center font-bold"
>
{{ getInitials(contact.nom_complet) }}
</div>
<div>
<p v-if="contact.nom_complet" class="font-bold">{{ contact.nom_complet }}</p>
<p v-if="contact.poste" class="font-regular">{{ contact.poste }}</p>
<p v-if="contact.email" class="text-sm text-primary font-medium">{{ contact.email }}</p>
<p v-if="contact.telephone" class="text-sm text-primary font-medium">{{ contact.telephone }}</p>
</div>
</div>
</div>
</div>
<!-- Réseaux sociaux -->
<div class="col-span-12 md:col-span-4 lg:col-span-4 rounded-xl p-8 shadow-sm">
<h4 class="text-xl font-bold mb-6">Mentionner l'Orchestre sur les réseaux</h4>
<div class="space-y-6">
<div
v-for="(reseau, index) in parametres?.reseaux_sociaux || []"
:key="reseau.id"
class="flex items-start gap-4"
>
<div
:class="index % 2 === 0
? 'bg-secondary-container text-on-secondary-container'
: 'bg-tertiary-container text-on-tertiary-container'"
class="w-12 h-12 shrink-0 rounded-full flex items-center justify-center font-bold"
>
{{ getInitials(reseau.nom_reseau) }}
</div>
<div>
<p v-if="reseau.nom_reseau" class="font-bold">{{ reseau.nom_reseau }}</p>
<p v-if="reseau.nom_compte" class="text-sm text-primary font-medium">{{ reseau.nom_compte }}</p>
</div>
</div>
</div>
</div>
<!-- Logos -->
<div class="col-span-12 md:col-span-6 lg:col-span-7 bg-surface-container-highest/40 rounded-xl p-8 border border-outline-variant/20">
<div class="flex justify-between items-center gap-x-16 mb-8">
<h4 class="text-xl font-bold">Logos</h4>
<span class="text-xs text-on-surface-variant font-medium">LOrchestre national dÎle-de-France est financé par la Région Île-de-France et la DRAC Île-de-France.</span>
</div>
<div class="flex gap-12 items-start">
<div
v-for="logo in programmer_content?.logos || []"
:key="logo.id"
class="text-center group cursor-pointer"
>
<a
v-if="logo.url"
:href="logo.url"
:download="logo.name || true"
class="block w-32 h-16 bg-white rounded-lg flex items-center justify-center p-2 mb-2 group-hover:shadow-md transition-shadow"
target="_blank"
rel="noopener noreferrer"
>
<span class="text-xs font-bold text-slate-400">
{{ logo.caption }}
</span>
</a>
<p class="text-[10px] text-outline">
<span v-if="logo.ext">
{{ logo.ext.replace('.', '').toUpperCase() }}
</span>
<span v-if="logo.ext && logo.size"> </span>
<span v-if="logo.size">
{{ `${logo.size} Ko` }}
</span>
</p>
</div>
</div>
</div>
<!-- Media Assets -->
<div class="col-span-12 md:col-span-12 bg-surface-container-low rounded-xl overflow-hidden flex flex-col">
<div class="p-6">
<h4 class="text-xl font-bold mb-1">Photothèque de l'Orchestre</h4>
<p class="text-sm text-on-surface-variant"></p>
</div>
<div class="grid grid-cols-4 h-full border-t border-white/20">
<a
v-for="photo in programmer_content?.photos_orchestre || []"
:key="photo.id"
:href="photo.url"
:download="photo.name || true"
class="relative group overflow-hidden h-64 block"
target="_blank"
rel="noopener noreferrer"
>
<img
v-if="photo.formats?.thumbnail?.url"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
:src="photo.formats.thumbnail.url"
:alt="photo.alternativeText || photo.caption || photo.name || ''"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent flex flex-col justify-end p-6">
<span class="text-white font-bold text-lg">{{ photo.caption }}</span>
<div class="flex justify-between items-center text-white/70 text-xs mt-1">
<span>
<span v-if="photo.ext">{{ photo.ext.replace('.', '').toUpperCase() }}</span>
<span v-if="photo.ext && photo.size"> • </span>
<span v-if="photo.size">{{ formatKoToMo(photo.size) }}</span>
</span>
<span class="material-symbols-outlined">download</span>
</div>
</div>
</a>
</div>
</div>
</div>
<!-- MUSIQUE DE CHAMBRE -->
<div class="space-y-4 mt-8">
<div
v-for="programme in programmer_content?.musique_chambre || []"
:key="programme.id"
class="bg-surface-container-lowest rounded-xl p-8 hover:shadow-xl transition-shadow border border-outline-variant/10"
>
<div class="flex flex-col lg:flex-row gap-8">
<div class="lg:w-1/3">
<div class="flex items-center gap-2 mb-4">
<span
v-if="programme.periode"
class="bg-primary/10 text-primary px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest"
>
{{ programme.periode }}
</span>
</div>
<h4 v-if="programme.nom" class="text-3xl font-extrabold tracking-tighter mb-2">
{{ programme.nom }}
</h4>
<div v-if="programme.description">
<StrapiBlocksConvert :blocks="programme.description" />
</div>
</div>
<div class="lg:w-2/3 border-l border-surface-container pl-8 grid grid-cols-1 md:grid-cols-3 gap-6 items-start">
<div
v-for="video in programme.videos || []"
:key="`video-${video.id}`"
class="flex flex-col gap-3"
>
<div class="w-full aspect-[4/3] bg-surface-container-low rounded-lg overflow-hidden">
<iframe
v-if="getYoutubeEmbedUrl(video.lien_youtube)"
:src="getYoutubeEmbedUrl(video.lien_youtube)"
title="Vidéo YouTube"
class="w-full h-full rounded-lg"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
<div class="text-center">
<p class="text-xs font-bold uppercase tracking-tighter">{{ getVideoTitle(video.lien_youtube) }}</p>
<p class="text-[10px] text-outline">YOUTUBE</p>
</div>
</div>
<a
v-for="image in programme.images || []"
:key="`image-${image.id}`"
:href="image.url"
:download="image.name || true"
class="flex flex-col gap-3 group"
target="_blank"
rel="noopener noreferrer"
>
<div class="w-full aspect-[4/3] bg-surface-container-low rounded-lg flex items-center justify-center group-hover:bg-primary-container transition-colors">
<img
v-if="image.formats?.thumbnail?.url"
:src="image.formats.thumbnail.url"
:alt="image.alternativeText || image.caption || image.name || ''"
class="w-full h-[180px] object-contain rounded-lg"
>
</div>
<div class="text-center">
<p v-if="image.caption" class="text-xs font-bold uppercase tracking-tighter">{{ image.caption }}</p>
<p class="text-[10px] text-outline">
<span v-if="image.ext">{{ image.ext.replace('.', '').toUpperCase() }}</span>
<span v-if="image.ext && image.size"> • </span>
<span v-if="image.size">{{ formatKoToMo(image.size) }}</span>
</p>
</div>
</a>
<a
v-for="document in programme.documents || []"
:key="`document-${document.id}`"
:href="document.url"
:download="document.name || true"
class="flex flex-col gap-3 group"
target="_blank"
rel="noopener noreferrer"
>
<div class="w-full aspect-[4/3] bg-surface-container-low rounded-lg flex items-center justify-center group-hover:bg-primary-container transition-colors">
<span class="material-symbols-outlined text-3xl text-outline group-hover:text-primary">insert_drive_file</span>
</div>
<div class="text-center">
<p v-if="document.caption" class="text-xs font-bold uppercase tracking-tighter">{{ document.caption }}</p>
<p class="text-[10px] text-outline">
<span v-if="document.ext">{{ document.ext.replace('.', '').toUpperCase() }}</span>
<span v-if="document.ext && document.size"> • </span>
<span v-if="document.size">{{ formatKoToMo(document.size) }}</span>
</p>
</div>
</a>
</div>
</div>
</div>
</div>
</section>
<!-- AUTRE CATEGORIE -->
<section class="space-y-12">
<div
v-for="categorie in programmer_content?.categorie || []"
:key="categorie.id"
class="space-y-4"
>
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-4">
<h3 v-if="categorie.nom_categorie" class="text-2xl font-bold tracking-tight">
{{ categorie.nom_categorie }}
</h3>
<div class="h-[2px] w-24 bg-surface-container"></div>
</div>
<!-- PDF -->
<a
v-if="categorie.programme_categorie?.url"
:href="categorie.programme_categorie.url"
:download="categorie.programme_categorie.name || true"
class="flex items-center gap-2 px-6 py-2 bg-primary-container text-on-primary-container rounded-full font-bold text-sm hover:bg-primary-fixed transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<span class="material-symbols-outlined text-lg">picture_as_pdf</span>
BROCHURE COMPLÈTE
<span v-if="categorie.programme_categorie.size">
({{ formatKoToMo(categorie.programme_categorie.size) }})
</span>
</a>
</div>
<!-- LISTE DES EVENEMENTS -->
<div class="space-y-4">
<div
v-for="programme in categorie.evenement || []"
:key="programme.id"
class="bg-surface-container-lowest rounded-xl p-8 hover:shadow-xl transition-shadow border border-outline-variant/10"
>
<div class="flex flex-col lg:flex-row gap-8">
<div class="lg:w-1/3">
<div class="flex items-center gap-2 mb-4">
<span
v-if="programme.periode"
class="bg-primary/10 text-primary px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest"
>
{{ programme.periode }}
</span>
</div>
<h4 v-if="programme.nom" class="text-3xl font-extrabold tracking-tighter mb-2">
{{ programme.nom }}
</h4>
<div v-if="programme.description">
<StrapiBlocksConvert :blocks="programme.description" />
</div>
</div>
<div class="lg:w-2/3 border-l border-surface-container pl-8 grid grid-cols-1 md:grid-cols-3 gap-6 items-start">
<div
v-for="video in programme.videos || []"
:key="`video-${video.id}`"
class="flex flex-col gap-3"
>
<div class="w-full aspect-[4/3] bg-surface-container-low rounded-lg overflow-hidden">
<iframe
v-if="getYoutubeEmbedUrl(video.lien_youtube)"
:src="getYoutubeEmbedUrl(video.lien_youtube)"
title="Vidéo YouTube"
class="w-full h-full rounded-lg"
loading="lazy"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>
</div>
<div class="text-center">
<p class="text-xs font-bold uppercase tracking-tighter">{{ getVideoTitle(video.lien_youtube) }}</p>
<p class="text-[10px] text-outline">YOUTUBE</p>
</div>
</div>
<a
v-for="document in programme.documents || []"
:key="`document-${document.id}`"
:href="document.url"
:download="document.name || true"
class="flex flex-col gap-3 group"
target="_blank"
rel="noopener noreferrer"
>
<div class="w-full aspect-[4/3] bg-surface-container-low rounded-lg flex items-center justify-center group-hover:bg-primary-container transition-colors">
<span class="material-symbols-outlined text-3xl text-outline group-hover:text-primary">insert_drive_file</span>
</div>
<div class="text-center">
<p v-if="document.caption" class="text-xs font-bold uppercase tracking-tighter">{{ document.caption }}</p>
<p class="text-[10px] text-outline">
<span v-if="document.ext">{{ document.ext.replace('.', '').toUpperCase() }}</span>
<span v-if="document.ext && document.size"> • </span>
<span v-if="document.size">{{ formatKoToMo(document.size) }}</span>
</p>
</div>
</a>
<a
v-for="image in programme.images || []"
:key="`image-${image.id}`"
:href="image.url"
:download="image.name || true"
class="flex flex-col gap-3 group"
target="_blank"
rel="noopener noreferrer"
>
<div class="w-full aspect-[4/3] bg-surface-container-low rounded-lg flex items-center justify-center group-hover:bg-primary-container transition-colors">
<img
v-if="image.formats?.thumbnail?.url"
:src="image.formats.thumbnail.url"
:alt="image.alternativeText || image.caption || image.name || ''"
class="w-full h-[180px] object-contain rounded-lg"
>
</div>
<div class="text-center">
<p v-if="image.caption" class="text-xs font-bold uppercase tracking-tighter">{{ image.caption }}</p>
<p class="text-[10px] text-outline">
<span v-if="image.ext">{{ image.ext.replace('.', '').toUpperCase() }}</span>
<span v-if="image.ext && image.size"> • </span>
<span v-if="image.size">{{ formatKoToMo(image.size) }}</span>
</p>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
useHead({
link: [
{
rel: 'preconnect',
href: 'https://fonts.googleapis.com',
},
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossorigin: '',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap',
},
],
})
//--------------------
// RÉCUPÉRATION DES DONNÉES STRAPI
//--------------------
const { items: programmer_content_strapi, pending, error, refresh } = useStrapi(
"/api/__strapi__/pro-programmer",
{ locale: "fr-FR",
populate: {
bios: true,
logos: true,
Contacts: true,
photos_orchestre: true,
musique_chambre: {
images: true,
documents: true,
videos: true,
},
categorie: {
programme_categorie: true,
evenement: {
images: true,
documents: true,
videos: true,
}
},
},
}
)
const { items: parametresItems, pending: pendingParametres, error: errorParametres, refresh: refreshParametres } = useStrapi(
"/api/__strapi__/parametres",
{ locale: "fr-FR",
populate: {
reseaux_sociaux: true,
},
}
)
const programmer_content = computed(() => programmer_content_strapi.value?.[0] || null)
const parametres = computed(() => parametresItems.value?.[0] || null)
const allVideoUrls = computed(() => {
const urls = []
for (const programme of programmer_content.value?.musique_chambre || []) {
for (const video of programme.videos || []) {
if (video?.lien_youtube) urls.push(video.lien_youtube)
}
}
for (const categorie of programmer_content.value?.categorie || []) {
for (const programme of categorie.evenement || []) {
for (const video of programme.videos || []) {
if (video?.lien_youtube) urls.push(video.lien_youtube)
}
}
}
return [...new Set(urls)]
})
const { data: youtubeOembedData } = await useFetch("/api/youtube/oembed", {
method: "POST",
body: computed(() => ({
urls: allVideoUrls.value,
})),
watch: [allVideoUrls],
default: () => ({ results: [] }),
})
const youtubeTitlesByUrl = computed(() => {
const entries =
youtubeOembedData.value?.results
?.filter((result) => result?.ok && result?.url && result?.title)
.map((result) => [result.url, result.title]) || []
return Object.fromEntries(entries)
})
function toMediaDebugItem(file) {
if (!file) return null
return {
url: file.url ?? null,
vignette: file.formats?.thumbnail?.url ?? file.previewUrl ?? null,
ext: file.ext ?? null,
size: file.size ?? null,
}
}
function formatKoToMo(sizeInKo) {
if (typeof sizeInKo !== "number") return ""
return `${(sizeInKo / 1024).toFixed(1)} Mo`
}
function getInitials(fullName) {
if (!fullName) return ""
return fullName
.split(" ")
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? "")
.join("")
}
function getYoutubeEmbedUrl(url = "") {
try {
const parsedUrl = new URL(url)
let videoId = ""
if (parsedUrl.hostname.includes("youtu.be")) {
videoId = parsedUrl.pathname.slice(1)
} else {
videoId = parsedUrl.searchParams.get("v") || ""
}
if (!videoId) return ""
return `https://www.youtube-nocookie.com/embed/${videoId}?rel=0&modestbranding=1&iv_load_policy=3&playsinline=1`
} catch {
return ""
}
}
function getVideoTitle(url = "") {
return youtubeTitlesByUrl.value[url] || "VIDÉO YOUTUBE"
}
const programmerDebugData = computed(() => {
const content = programmer_content.value
if (!content) return null
return {
bios: Array.isArray(content.bios)
? content.bios.map(toMediaDebugItem)
: [],
logos: Array.isArray(content.logos)
? content.logos.map(toMediaDebugItem)
: [],
photos_orchestre: Array.isArray(content.photos_orchestre)
? content.photos_orchestre.map(toMediaDebugItem)
: [],
contacts: Array.isArray(content.Contacts)
? content.Contacts.map((contact) => ({
nom_complet: contact.nom_complet ?? null,
poste: contact.poste ?? null,
email: contact.email ?? null,
telephone: contact.telephone ?? null,
}))
: [],
}
})
watchEffect(() => {
if (pending.value) return
if (error.value) {
console.error("pro-programmer Strapi error", error.value)
return
}
if (!programmerDebugData.value) return
console.log("pro-programmer Strapi content", programmerDebugData.value)
})
</script>
<style lang="scss">
<style lang="scss" scoped>
.programmer-orchestre-page {
font-family: 'Inter', sans-serif;
}
</style>
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
.glass-panel {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
}
</style>