Files
wondif_vue/app/pages/professionnels/programmer-orchestre.vue
2026-05-20 00:45:23 +02:00

660 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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 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 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 flex-wrap 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) {
const size = Number(sizeInKo)
if (!Number.isFinite(size)) return ""
if (size < 1024) return `${Math.round(size)} Ko`
return `${(size / 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" scoped>
.programmer-orchestre-page {
font-family: 'Inter', sans-serif;
}
.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>