generated from gitea_admin/default
ajout pro et tailwind
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Stack
|
# Stack
|
||||||
- NUXT 4
|
- NUXT 4
|
||||||
- VueJS 3.5
|
- VueJS 3.5
|
||||||
|
- Tailwindcss pour les pages professionnels
|
||||||
|
|
||||||
# Dev du site en local
|
# Dev du site en local
|
||||||
## NUXT
|
## NUXT
|
||||||
@@ -112,6 +113,13 @@ API pour avoir la page Les Missions
|
|||||||
https://bo.orchestre-ile.com/api/mission?locale=fr-FR&populate=image_illustration_header
|
https://bo.orchestre-ile.com/api/mission?locale=fr-FR&populate=image_illustration_header
|
||||||
https://bo.orchestre-ile.com/api/mission?locale=fr-FR&populate=*
|
https://bo.orchestre-ile.com/api/mission?locale=fr-FR&populate=*
|
||||||
|
|
||||||
|
https://bo.orchestre-ile.com/api/pro-programmer?locale=fr-FR&populate[bios]=true&populate[logos]=true&populate[Contacts]=true&populate[photos_orchestre]=true&populate[musique_chambre][populate][images]=true&populate[musique_chambre][populate][documents]=true&populate[musique_chambre][populate][videos]=true
|
||||||
|
|
||||||
|
https://bo.orchestre-ile.com/api/pro-programmer?locale=fr-FR&populate[bios]=true&populate[logos]=true&populate[Contacts]=true&populate[photos_orchestre]=true&populate[musique_chambre][populate][images]=true&populate[musique_chambre][populate][documents]=true&populate[musique_chambre][populate][videos]=true&populate[categorie][populate][evenement][populate][images]=true&populate[categorie][populate][evenement][populate][documents]=true&populate[categorie][populate][evenement][populate][videos]=true
|
||||||
|
|
||||||
|
https://bo.orchestre-ile.com/api/pro-programmer?locale=fr-FR&populate[bios]=true&populate[logos]=true&populate[Contacts]=true&populate[photos_orchestre]=true&populate[musique_chambre][populate]=images,documents,videos&populate[categorie][populate][evenement][populate]=images,documents,videos
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Afficher les données récupérées ou les erreurs dans la console
|
### Afficher les données récupérées ou les erreurs dans la console
|
||||||
const { items: scolaires, pending, error } = useStrapi(
|
const { items: scolaires, pending, error } = useStrapi(
|
||||||
|
|||||||
@@ -67,14 +67,16 @@
|
|||||||
max-width: 85%;
|
max-width: 85%;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: -50px;
|
margin-top: -7px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-card__meta {
|
.rounded-card__meta {
|
||||||
margin-top: calc(var(--sp-4) * -1);
|
margin-top: calc(var(--sp-4) * -1);
|
||||||
}
|
}
|
||||||
|
.rounded-card__title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
.rounded-card__title-link {
|
.rounded-card__title-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -88,10 +90,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-card__description {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.rounded-card__actions {
|
.rounded-card__actions {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<li class="header_nav_topbar_item" :class="{ 'is-active': isPro }">
|
<li class="header_nav_topbar_item" :class="{ 'is-active': isPro }">
|
||||||
Professionnels
|
Professionnels
|
||||||
<ul class="header_nav_topbar_submenu">
|
<ul class="header_nav_topbar_submenu">
|
||||||
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/programmer-orchestre">Programmer l'Orchestre</NuxtLink></li> -->
|
<li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/programmer-orchestre">Programmer l'Orchestre</NuxtLink></li>
|
||||||
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/studio">Le studio et les espaces</NuxtLink></li> -->
|
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/studio">Le studio et les espaces</NuxtLink></li> -->
|
||||||
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/louer">Louer des instruments</NuxtLink></li> -->
|
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/louer">Louer des instruments</NuxtLink></li> -->
|
||||||
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/recrutement">Recrutement / Concours</NuxtLink></li> -->
|
<!-- <li class="header_nav_topbar_submenu_item"><NuxtLink to="/professionnels/recrutement">Recrutement / Concours</NuxtLink></li> -->
|
||||||
@@ -59,11 +59,11 @@
|
|||||||
Action culturelle
|
Action culturelle
|
||||||
<ul class="header_nav_sub_menu">
|
<ul class="header_nav_sub_menu">
|
||||||
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/petite-enfance">Ateliers 0-3 ans</NuxtLink></li>
|
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/petite-enfance">Ateliers 0-3 ans</NuxtLink></li>
|
||||||
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/scolaires">Scolaires</NuxtLink></li>
|
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/scolaires">Lycées et scolaires</NuxtLink></li>
|
||||||
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/chantons">Chantons avec l'Orchestre</NuxtLink></li>
|
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/chantons">Chantons avec l'Orchestre</NuxtLink></li>
|
||||||
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/academie">Académie d'orchestre</NuxtLink></li>
|
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/academie">Académie d'orchestre</NuxtLink></li>
|
||||||
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/partenariat-grandes-ecoles">Partenariat Grandes Écoles</NuxtLink></li>
|
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/partenariat-grandes-ecoles">Partenariat Grandes Écoles</NuxtLink></li>
|
||||||
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/social">Actions à l'hopital</NuxtLink></li>
|
<li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/social">Actions à l'hôpital</NuxtLink></li>
|
||||||
<!-- <li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/ressources-pedagogiques">Ressources pédagogiques</NuxtLink></li> -->
|
<!-- <li class="header_nav_sub_menu_item"><NuxtLink to="/mediation/ressources-pedagogiques">Ressources pédagogiques</NuxtLink></li> -->
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@@ -196,11 +196,11 @@
|
|||||||
Action culturelle
|
Action culturelle
|
||||||
<ul class="header_drawer_sub_menu">
|
<ul class="header_drawer_sub_menu">
|
||||||
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/petite-enfance">Ateliers 0-3 ans</NuxtLink></li>
|
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/petite-enfance">Ateliers 0-3 ans</NuxtLink></li>
|
||||||
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/scolaires">Scolaires</NuxtLink></li>
|
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/scolaires">Lycées et scolaires</NuxtLink></li>
|
||||||
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/chantons">Chantons avec l'Orchestre</NuxtLink></li>
|
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/chantons">Chantons avec l'Orchestre</NuxtLink></li>
|
||||||
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/academie">Académie d'orchestre</NuxtLink></li>
|
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/academie">Académie d'orchestre</NuxtLink></li>
|
||||||
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/partenariat-grandes-ecoles">Partenariat Grandes Écoles</NuxtLink></li>
|
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/partenariat-grandes-ecoles">Partenariat Grandes Écoles</NuxtLink></li>
|
||||||
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/social">Actions à l'hopital</NuxtLink></li>
|
<li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/social">Actions à l'hôpital</NuxtLink></li>
|
||||||
<!-- <li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/ressources-pedagogiques">Ressources pédagogiques</NuxtLink></li> -->
|
<!-- <li class="header_drawer_sub_menu_item"><NuxtLink to="/mediation/ressources-pedagogiques">Ressources pédagogiques</NuxtLink></li> -->
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
:href="href"
|
:href="href"
|
||||||
class="strapi-inline__link"
|
class="strapi-inline__link"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_self"
|
||||||
>
|
>
|
||||||
<StrapiBlockChildsConvert
|
<StrapiBlockChildsConvert
|
||||||
v-for="(child, i) in children"
|
v-for="(child, i) in children"
|
||||||
|
|||||||
@@ -166,8 +166,9 @@
|
|||||||
h2 {
|
h2 {
|
||||||
padding-bottom: 7px;
|
padding-bottom: 7px;
|
||||||
font-weight: var(--fw-bold);
|
font-weight: var(--fw-bold);
|
||||||
font-size: 27px;
|
font-size: 26px;
|
||||||
color: var(--c-brand_rouge);
|
color: var(--c-brand_rouge);
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
|||||||
@@ -257,11 +257,9 @@
|
|||||||
id: '1',
|
id: '1',
|
||||||
imgSrc: '/contenus/insta_ondif_1.jpg',
|
imgSrc: '/contenus/insta_ondif_1.jpg',
|
||||||
imgAlt: 'le titre du post',
|
imgAlt: 'le titre du post',
|
||||||
title: "[Y'A PAS QUE LES ÂNES QUI MANGENT DU SON]",
|
title: "6€ la place dès 3 concerts pour les - de 28 ans",
|
||||||
description: `Tu as moins de 28 ans ? Paie 6 € la place dès 3 concerts de l’Orchestre à la Philharmonie de Paris<br>
|
description: `à la Philharmonie de Paris - Cité de la musique`,
|
||||||
🔴 Ile de France | Ministère de la culture<br>
|
url:"https://ondif.shop.secutix.com/selection/subscription?productId=10229737867151",
|
||||||
🎨 Agence belleville.eu`,
|
|
||||||
url:"",
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -295,7 +293,7 @@
|
|||||||
imgAlt: 'Du sur-mesure pour les petites oreilles',
|
imgAlt: 'Du sur-mesure pour les petites oreilles',
|
||||||
title: "Du sur-mesure pour les petites oreilles",
|
title: "Du sur-mesure pour les petites oreilles",
|
||||||
description: ``,
|
description: ``,
|
||||||
url:"",
|
url:"/mediation/petite-enfance",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,656 @@
|
|||||||
<template>
|
<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">L’Orchestre 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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>
|
</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>
|
||||||
|
|||||||
@@ -94,5 +94,5 @@ export default defineNuxtConfig({
|
|||||||
densities: [1, 2],
|
densities: [1, 2],
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ['@nuxt/image'],
|
modules: ['@nuxt/image', '@nuxtjs/tailwindcss'],
|
||||||
})
|
})
|
||||||
|
|||||||
1950
package-lock.json
generated
1950
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,11 @@
|
|||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"sass": "^1.93.2"
|
"sass": "^1.93.2",
|
||||||
|
"tailwindcss": "^3.4.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const COLLECTION_MAP = {
|
|||||||
insertion: "/api/grandes-ecoles",
|
insertion: "/api/grandes-ecoles",
|
||||||
hopital: "/api/hopital",
|
hopital: "/api/hopital",
|
||||||
articles_mag: "/api/mags",
|
articles_mag: "/api/mags",
|
||||||
|
"pro-programmer": "/api/pro-programmer",
|
||||||
|
"parametres": "/api/parametres",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|||||||
78
server/api/youtube/oembed.post.js
Normal file
78
server/api/youtube/oembed.post.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
function isValidUrl(url) {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isYoutubeUrl(url) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
const host = parsedUrl.hostname.replace(/^www\./, "")
|
||||||
|
|
||||||
|
return host === "youtube.com" || host === "youtu.be" || host === "m.youtube.com"
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineCachedEventHandler(
|
||||||
|
async (event) => {
|
||||||
|
const body = await readBody(event).catch(() => null)
|
||||||
|
|
||||||
|
if (!body || !Array.isArray(body.urls)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Invalid payload. Expected { urls: string[] }",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = body.urls
|
||||||
|
.map((url) => String(url).trim())
|
||||||
|
.filter((url) => isValidUrl(url))
|
||||||
|
.filter((url) => isYoutubeUrl(url))
|
||||||
|
.slice(0, 50)
|
||||||
|
|
||||||
|
if (urls.length === 0) {
|
||||||
|
return { results: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
urls.map(async (url) => {
|
||||||
|
const endpoint = new URL("https://www.youtube.com/oembed")
|
||||||
|
endpoint.searchParams.set("url", url)
|
||||||
|
endpoint.searchParams.set("format", "json")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $fetch(endpoint.toString(), { timeout: 10000 })
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
ok: true,
|
||||||
|
title: data?.title ?? null,
|
||||||
|
author_name: data?.author_name ?? null,
|
||||||
|
thumbnail_url: data?.thumbnail_url ?? null,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
ok: false,
|
||||||
|
error: error?.message || "Unknown oEmbed error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return { results }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAge: 60 * 60 * 6,
|
||||||
|
getKey: async (event) => {
|
||||||
|
const body = await readBody(event).catch(() => null)
|
||||||
|
const urls = Array.isArray(body?.urls) ? body.urls.join("|") : "none"
|
||||||
|
return `youtube:oembed:${urls}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
82
tailwind.config.js
Normal file
82
tailwind.config.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import forms from '@tailwindcss/forms'
|
||||||
|
import containerQueries from '@tailwindcss/container-queries'
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./app/components/**/*.{vue,js,ts}',
|
||||||
|
'./app/layouts/**/*.vue',
|
||||||
|
'./app/pages/**/*.vue',
|
||||||
|
'./app/app.vue',
|
||||||
|
'./app/plugins/**/*.{js,ts}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
surface: '#faf8ff',
|
||||||
|
'on-secondary-fixed-variant': '#4d5d73',
|
||||||
|
'on-primary': '#f8f7ff',
|
||||||
|
'surface-container-highest': '#d9e2ff',
|
||||||
|
'on-tertiary-container': '#4c4e69',
|
||||||
|
'error-dim': '#4e0309',
|
||||||
|
'on-tertiary': '#fbf8ff',
|
||||||
|
'secondary-fixed': '#d3e4fe',
|
||||||
|
'on-secondary-fixed': '#314055',
|
||||||
|
'primary-fixed-dim': '#c7d3ff',
|
||||||
|
'primary-container': '#dbe1ff',
|
||||||
|
'surface-tint': '#0053db',
|
||||||
|
'surface-container': '#eaedff',
|
||||||
|
'tertiary-container': '#dcddfe',
|
||||||
|
'on-error-container': '#752121',
|
||||||
|
'secondary-dim': '#44546a',
|
||||||
|
'on-surface': '#113069',
|
||||||
|
'tertiary-fixed': '#dcddfe',
|
||||||
|
'on-tertiary-fixed': '#393c55',
|
||||||
|
'primary-dim': '#0048c1',
|
||||||
|
'error-container': '#fe8983',
|
||||||
|
'on-surface-variant': '#445d99',
|
||||||
|
'surface-container-low': '#f2f3ff',
|
||||||
|
'primary': '#0053db',
|
||||||
|
'outline': '#6079b7',
|
||||||
|
'surface-bright': '#faf8ff',
|
||||||
|
'tertiary-dim': '#4f516c',
|
||||||
|
'on-error': '#fff7f6',
|
||||||
|
'primary-fixed': '#dbe1ff',
|
||||||
|
'on-background': '#113069',
|
||||||
|
'error': '#9f403d',
|
||||||
|
'background': '#faf8ff',
|
||||||
|
'surface-variant': '#d9e2ff',
|
||||||
|
'on-primary-container': '#0048bf',
|
||||||
|
'tertiary': '#5b5d78',
|
||||||
|
'on-primary-fixed-variant': '#0050d4',
|
||||||
|
'inverse-on-surface': '#959cb5',
|
||||||
|
'secondary': '#506076',
|
||||||
|
'on-secondary-container': '#435368',
|
||||||
|
'on-primary-fixed': '#003798',
|
||||||
|
'secondary-fixed-dim': '#c5d6f0',
|
||||||
|
'on-tertiary-fixed-variant': '#565873',
|
||||||
|
'inverse-primary': '#618bff',
|
||||||
|
'inverse-surface': '#060e20',
|
||||||
|
'tertiary-fixed-dim': '#cecfef',
|
||||||
|
'secondary-container': '#d3e4fe',
|
||||||
|
'outline-variant': '#98b1f2',
|
||||||
|
'surface-container-high': '#e2e7ff',
|
||||||
|
'surface-container-lowest': '#ffffff',
|
||||||
|
'surface-dim': '#cdd9ff',
|
||||||
|
'on-secondary': '#f7f9ff',
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
DEFAULT: '0.125rem',
|
||||||
|
lg: '0.25rem',
|
||||||
|
xl: '0.5rem',
|
||||||
|
full: '0.75rem',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
headline: ['Inter', 'sans-serif'],
|
||||||
|
body: ['Inter', 'sans-serif'],
|
||||||
|
label: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [forms, containerQueries],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user