finalisation home page

This commit is contained in:
2026-02-06 20:20:01 +01:00
parent 91c1b03a2f
commit d9ac2b4cc5
45 changed files with 1892 additions and 148 deletions

102
README.md
View File

@@ -1,32 +1,86 @@
# Stack
- NUXT
- NUXT 4
# Dev du site en local
# NUXT
Démarrer NUXT
- npm run dev
- URL : http://localhost:3000/
# Site de développement
## URL
https://2025.orchestre-ile.com/
## package.json du serveur
Fichier /var/www/wondif_vue$ more package.json
{
"name": "wondif_vue",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"start": "node .output/server/index.mjs",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/image": "^1.11.0",
"nuxt": "^4.1.3",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"devDependencies": {
"sass": "^1.93.2"
# TODO
## Serveur web
Créer un vrai /security.txt (ou /.well-known/security.txt) au lieu de le renvoyer en 404.
dans le fichier de conf NGINX /etc/nginx/sites-available$ sudo nano wondif_2025
# Fichier standard optionnel (si tu ne le fournis pas)
location = /security.txt { return 404; }
# Dev
## MEDIAS QUERIES
@media (max-width: 599px) {
. {
xxx
}
}
@media (min-width: 600px) {
. {
xxx
}
}
@media (min-width: 700px) {
. {
xxx
}
}
@media (min-width: 800px) {
. {
xxx
}
}
@media (min-width: 900px) {
. {
xxx
}
}
@media (min-width: 1000px) {
. {
xxx
}
}
@media (min-width: 1100px) {
. {
xxx
}
}
@media (min-width: 1200px) {
. {
xxx
}
}
@media (min-width: 1300px) {
. {
xxx
}
}
@media (min-width: 1400px) {
. {
xxx
}
}
padded: { type: Boolean, default: true }, // padding vertical
&--padded {
padding-top: 30px;
padding-bottom: 50px;
}

View File

@@ -0,0 +1,4 @@
<svg width="28" height="30" viewBox="0 0 28 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 10H28V27C28 28.6562 26.6562 30 25 30H3C1.34375 30 0 28.6562 0 27V10ZM4 15V17C4 17.55 4.45 18 5 18H7C7.55 18 8 17.55 8 17V15C8 14.45 7.55 14 7 14H5C4.45 14 4 14.45 4 15ZM4 23V25C4 25.55 4.45 26 5 26H7C7.55 26 8 25.55 8 25V23C8 22.45 7.55 22 7 22H5C4.45 22 4 22.45 4 23ZM12 15V17C12 17.55 12.45 18 13 18H15C15.55 18 16 17.55 16 17V15C16 14.45 15.55 14 15 14H13C12.45 14 12 14.45 12 15ZM12 23V25C12 25.55 12.45 26 13 26H15C15.55 26 16 25.55 16 25V23C16 22.45 15.55 22 15 22H13C12.45 22 12 22.45 12 23ZM20 15V17C20 17.55 20.45 18 21 18H23C23.55 18 24 17.55 24 17V15C24 14.45 23.55 14 23 14H21C20.45 14 20 14.45 20 15ZM20 23V25C20 25.55 20.45 26 21 26H23C23.55 26 24 25.55 24 25V23C24 22.45 23.55 22 23 22H21C20.45 22 20 22.45 20 23Z" fill="white"/>
<path d="M10 2C10 0.89375 9.10625 0 8 0C6.89375 0 6 0.89375 6 2V4H3C1.34375 4 0 5.34375 0 7V10H28V7C28 5.34375 26.6562 4 25 4H22V2C22 0.89375 21.1063 0 20 0C18.8937 0 18 0.89375 18 2V4H10V2Z" fill="#E20018"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="23" height="31" viewBox="0 0 23 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.4958 2.67233C22.59 2.00789 22.1165 1.53441 21.4521 1.43762L17.5622 1.05831C16.9919 1.05831 16.5185 1.43762 16.4243 1.81693C16.045 3.23999 15.1896 4.18957 12.4402 3.99861C9.78247 3.80764 8.55037 2.66972 8.45619 1.24666C8.36202 0.676386 7.88592 0.393866 7.31827 0.297077L3.33422 0.0119418C2.66978 -0.0822314 2.19629 0.39125 2.09951 1.05569L0.012001 28.4784C-0.0821721 29.0487 0.39131 29.6163 0.96158 29.7131L19.1789 30.9478C19.8433 31.042 20.3168 30.5685 20.4136 29.9041L22.5011 2.67233H22.4958ZM11.9641 14.0594L14.2426 14.2504L14.0516 16.5289L11.7732 16.3379L11.9641 14.0594ZM8.26262 13.8685L10.5411 14.0594L10.3501 16.3379L8.07165 16.1469L8.26262 13.8685ZM4.46691 13.5833L6.74538 13.7743L6.55703 16.0528L4.27857 15.8618L4.46691 13.5833ZM17.7506 16.7146L15.4721 16.5236L15.6631 14.2452L17.9389 14.4361L17.7506 16.7146Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 950 B

View File

@@ -0,0 +1,3 @@
<svg width="23" height="31" viewBox="0 0 23 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.4958 2.67233C22.59 2.00789 22.1165 1.53441 21.4521 1.43762L17.5622 1.05831C16.9919 1.05831 16.5185 1.43762 16.4243 1.81693C16.045 3.23999 15.1896 4.18957 12.4402 3.99861C9.78247 3.80764 8.55037 2.66972 8.45619 1.24666C8.36202 0.676386 7.88592 0.393866 7.31827 0.297077L3.33422 0.0119418C2.66978 -0.0822314 2.19629 0.39125 2.09951 1.05569L0.012001 28.4784C-0.0821721 29.0487 0.39131 29.6163 0.96158 29.7131L19.1789 30.9478C19.8433 31.042 20.3168 30.5685 20.4136 29.9041L22.5011 2.67233H22.4958ZM11.9641 14.0594L14.2426 14.2504L14.0516 16.5289L11.7732 16.3379L11.9641 14.0594ZM8.26262 13.8685L10.5411 14.0594L10.3501 16.3379L8.07165 16.1469L8.26262 13.8685ZM4.46691 13.5833L6.74538 13.7743L6.55703 16.0528L4.27857 15.8618L4.46691 13.5833ZM17.7506 16.7146L15.4721 16.5236L15.6631 14.2452L17.9389 14.4361L17.7506 16.7146Z" fill="black" fill-opacity="0.85"/>
</svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@@ -16,35 +16,42 @@
grid-template-rows: 130px auto auto;
@include media_queries.media_max(tablet_700) {
@include media_queries.media_max(phone_500) {
grid-template-columns: 20px minmax(0, 1fr) 10px;
}
@include media_queries.media_min(phone_500) {
grid-template-columns: 30px minmax(0, 1fr) 10px;
}
@include media_queries.media_min(tablet_600) {
grid-template-columns: 10px minmax(0, 1fr) 10px;
}
@include media_queries.media_min(tablet_700) {
grid-template-columns: 10px minmax(0, 1fr) 10px;
grid-template-columns: 30px minmax(0, 1fr) 10px;
}
@include media_queries.media_min(tablet_800) {
grid-template-columns: 10px minmax(0, 1fr) 10px;
grid-template-columns: 30px minmax(0, 1fr) 10px;
grid-template-rows: 123px auto auto;
}
@include media_queries.media_min(tablet_900) {
grid-template-columns: 15px minmax(0, 1fr) 15px;
grid-template-columns: 35px minmax(0, 1fr) 15px;
}
@include media_queries.media_min(desktop_1000) {
grid-template-columns: auto 980px auto;
grid-template-columns: auto 930px auto;
}
@include media_queries.media_min(desktop_1100) {
grid-template-columns: auto 1090px auto;
grid-template-columns: auto 1000px auto;
}
@include media_queries.media_min(desktop_1200) {
grid-template-columns: auto 1160px auto;
grid-template-columns: auto 1100px auto;
}
@include media_queries.media_min(desktop_1300) {
grid-template-columns: 1fr 1250px 1fr;
grid-template-columns: 1fr 1200px 1fr;
}
@include media_queries.media_min(desktop_1500) {
grid-template-columns: 1fr 1420px 1fr;
grid-template-columns: 1fr 1400px 1fr;
}
@include media_queries.media_min(desktop_1800) {
grid-template-columns: 1fr 1700px 1fr;
grid-template-columns: 1fr 1410px 1fr;
}

View File

@@ -4,8 +4,10 @@
.header_navigation_topbar {
display: flex;
justify-content: flex-end;
align-items: center;
font-family: 'brandontext_regular';
//font-family: 'brandontext_regular';
font-family: var(--font-roboto);
font-size: 16px;
.header_nav_topbar_item {
@@ -46,7 +48,8 @@
.header_nav_topbar_submenu_item {
list-style: none;
font-family: 'brandontext_regular';
//font-family: 'brandontext_regular';
font-family: var(--font-roboto);
font-size: 15px;
padding-bottom: 2px;
&:hover {
@@ -131,11 +134,11 @@
}
@include media_queries.media_min(tablet_800) {
margin-left: 25px;
margin-right: 25px;
margin-right: 0px;
}
@include media_queries.media_min(tablet_900) {
margin-left: 30px;
margin-right: 30px;
margin-right: 0px;
}
@include media_queries.media_max(tablet_700) {
@@ -157,7 +160,8 @@
text-wrap-mode: nowrap;
font-family: 'brandontext_regular';
//font-family: 'brandontext_regular';
font-family: var(--font-roboto);
@include media_queries.media_min(tablet_600) {
font-size: 18px;
@@ -172,7 +176,7 @@
font-size: 24px;
}
@include media_queries.media_min(desktop_1800) {
font-size: 26px;
font-size: 24px;
}
.header_nav_item {
@@ -205,7 +209,7 @@
margin-right: 30px;
}
@include media_queries.media_min(desktop_1800) {
margin-right: 34px;
margin-right: 30px;
}
&:last-child {
margin-right: 0px;
@@ -222,18 +226,19 @@
opacity: 0;
transition: visibility 0.2s,opacity 0.2s cubic-bezier(0.4, 0, 1, 1);
padding-top: 10px;
padding-left: 9px;
padding-right: 16px;
padding-bottom: 5px;
padding-left: 25px;
padding-right: 22px;
padding-bottom: 10px;
text-align: left;
background-color: rgba(255, 255, 255, 0.93);
border-radius: 3px;
.header_nav_sub_menu_item {
list-style: none;
font-family: 'brandontext_regular';
list-style: circle;
//font-family: 'brandontext_regular';
font-family: var(--font-roboto);
font-size: 18px;
padding-bottom: 2px;
padding-bottom: 4px;
&:hover {
a {
color: var(--c-brand_rouge);
@@ -262,7 +267,7 @@
width: 83%;
}
@include media_queries.media_min(desktop_1800) {
width: 100%;
width: 83%;
}
}
}
@@ -297,7 +302,7 @@
}
/* Par défaut : desktop visible, mobile caché */
.header_nav--desktop { display: flex; }
.header_nav--desktop { display: flex; align-items: center;}
.header_nav--mobile { display: none; }
.header_nav--mobile-icons { display: none; }
.header_drawer { display: none; }
@@ -319,7 +324,7 @@
}
justify-content: flex-end;
margin-top: -23px;
margin-right: 60px;
margin-right: 30px;
}
.hamburger_black {
@@ -386,7 +391,7 @@
background-color: rgba(0, 0, 0, 0.9);
margin-top: 47px;
font-family: var(--font-roboto);
padding-bottom: 10px;
padding-right: 10px;
@@ -405,6 +410,7 @@
}
.header_drawer_link {
display: block;
text-decoration: none;
color: $blanc;
padding: 6px 0;
@@ -434,19 +440,22 @@
transition: visibility 0.2s,opacity 0.2s cubic-bezier(0.4, 0, 1, 1);
margin-top: 10px;
margin-left: 20px;
padding-top: 10px;
padding-left: 9px;
padding-top: 13px;
padding-left: 23px;
padding-right: 16px;
padding-bottom: 5px;
padding-bottom: 10px;
text-align: left;
background-color: rgba(255, 255, 255, 0.93);
border-radius: 3px;
}
.header_drawer_sub_menu_item {
list-style: none;
font-family: 'brandontext_regular';
//font-family: 'brandontext_regular';
font-family: var(--font-roboto);
font-size: 18px;
padding-bottom: 2px;
padding-bottom: 4px;
list-style: circle;
color: var(--c-text);
&:hover {
a {
color: var(--c-brand_rouge);

View File

@@ -2,17 +2,17 @@
position: absolute;
inset: 0;
z-index: 0;
transform: translateZ(0);
//transform: translateZ(0);
overflow: hidden;
}
.orb {
position: absolute;
border-radius: 999px;
filter: blur(3px);
opacity: 0.30;
will-change: transform;
transform: translate3d(0,0,0);
//filter: blur(3px);
opacity: 0.5;
//will-change: transform;
//transform: translate3d(0,0,0);
//background: radial-gradient(circle, rgba(255,255,255,0.20), rgba(255,255,255,0) 60%);
background: red;
@@ -21,12 +21,12 @@
.orb--1 { width: 100px; height: 100px; top: 2%; left: 18%; animation: orb1 6s ease-in-out infinite alternate; }
.orb--2 { width: 320px; height: 320px; top: 6%; left: 72%; animation: orb2 6s ease-in-out infinite alternate; }
@keyframes orb1 { to { transform: translate3d(30px, 18px, 0); } }
@keyframes orb2 { to { transform: translate3d(-26px, 34px, 0); } }
@keyframes orb3 { to { transform: translate3d(22px, -28px, 0); } }
@keyframes orb4 { to { transform: translate3d(34px, -14px, 0); } }
@keyframes orb5 { to { transform: translate3d(-18px, -18px, 0); } }
@keyframes orb6 { to { transform: translate3d(-34px, 12px, 0); } }
//@keyframes orb1 { to { transform: translate3d(30px, 18px, 0); } }
//@keyframes orb2 { to { transform: translate3d(-26px, 34px, 0); } }
//@keyframes orb3 { to { transform: translate3d(22px, -28px, 0); } }
//@keyframes orb4 { to { transform: translate3d(34px, -14px, 0); } }
//@keyframes orb5 { to { transform: translate3d(-18px, -18px, 0); } }
//@keyframes orb6 { to { transform: translate3d(-34px, 12px, 0); } }
@media (prefers-reduced-motion: reduce) {
.orb { animation: none !important; }

View File

@@ -0,0 +1,266 @@
<!-- Utilisé pour : -->
<!-- Index.vue / Encart PROS bas de page -->
<template>
<div class="banniere_pros_wp">
<div class="studio_img">
<DsMedia src="/contenus/studio_img_1.webp" alt="Image du studio pro" ratio="16-9" />
</div>
<div class="banniere_pros--description studio_description">
<div>
<img src="/img/logos/logo_le_studio_noir.png" alt="Logo du studio pro" width="200">
</div>
<DsHeading as="h5" tone="default" class="banniere_pros--description--titre">
On enregistre au studio !
</DsHeading>
<DsText as="p" tone="default" :clamp="3">
Un grand studio modulaire ouvert à tous les professionnels aux portes de Paris
</DsText>
</div>
<div class="parc_img">
<DsMedia src="/contenus/studio_img_2.webp" alt="Image du studio pro" ratio="16-9" />
</div>
<div class="banniere_pros--description parc_description">
<div>
<img src="/img/logos/logo_le_parc_noir.png" alt="Logo du parc instrumental pro" width="150">
</div>
<DsHeading as="h5" tone="default" class="banniere_pros--description--titre">
+ de 3 000 instruments en location
</DsHeading>
<DsText as="p" tone="default" :clamp="3">
Location dinstruments de musique pour les professionnels et les amateurs
</DsText>
</div>
</div>
</template>
<script setup>
import DsMedia from '@root/design-system/primitives/DsMedia.vue'
import DsText from '@root/design-system/primitives/DsText.vue'
import DsHeading from '@root/design-system/primitives/DsHeading.vue'
</script>
<style lang="scss">
.banniere_pros_wp {
display: grid;
}
@media (max-width: 599px) {
.banniere_pros_wp {
padding-left: 10px;
}
}
@media (min-width: 500px) {
.banniere_pros_wp {
row-gap: 50px;
}
}
@media (max-width: 399px) {
.banniere_pros_wp {
grid-template-columns: 30px 240px;
grid-template-rows: 130px 60px 130px 42px 111px 70px 140px;
//justify-content: center;
.ds-media--16-9 {
aspect-ratio: 4 / 3;
}
}
}
@media (min-width: 400px) {
.banniere_pros_wp {
grid-template-columns: 80px 290px;
grid-template-rows: 150px 60px 130px 42px 170px 70px 140px;
//justify-content: center;
.ds-media--16-9 {
aspect-ratio: 4 / 3;
}
}
}
@media (min-width: 500px) {
.banniere_pros_wp {
grid-template-columns: 150px 270px 40px 40px;
grid-template-rows: auto;
//justify-content: center;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 600px) {
.banniere_pros_wp {
grid-template-columns: 220px 250px 40px 40px;
grid-template-rows: auto;
//justify-content: center;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 700px) {
.banniere_pros_wp {
grid-template-columns: 220px 250px 40px 40px;
grid-template-rows: auto;
//justify-content: center;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 800px) {
.banniere_pros_wp {
grid-template-columns: 100px 190px 20px 40px 110px 140px 160px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 4 / 3;
}
}
}
@media (min-width: 900px) {
.banniere_pros_wp {
grid-template-columns: 130px 190px 40px 30px 160px 130px 169px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 4 / 3;
}
}
}
@media (min-width: 1000px) {
.banniere_pros_wp {
grid-template-columns: 160px 200px 90px 50px 160px 150px 150px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 1100px) {
.banniere_pros_wp {
grid-template-columns: 200px 200px 100px 50px 200px 150px 150px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 1200px) {
.banniere_pros_wp {
grid-template-columns: 230px 200px 100px 50px 230px 150px 150px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 1300px) {
.banniere_pros_wp {
grid-template-columns: 270px 200px 100px 50px 270px 150px 150px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
@media (min-width: 1400px) {
.banniere_pros_wp {
grid-template-columns: 320px 200px 100px 50px 320px 150px 150px;
grid-template-rows: auto;
align-items: center;
.ds-media--16-9 {
aspect-ratio: 16 / 9;
}
}
}
/* IMAGES */
.studio_img {
grid-column: 1 / 3;
grid-row: 1;
z-index: 1;
}
@media (max-width: 499px) {
.studio_img {
grid-column: 1 / 3;
grid-row: 1/2;
z-index: 1;
}
}
.parc_img {
grid-column: 4 / 7;
grid-row: 1;
z-index: 1;
}
@media (max-width: 799px) {
.parc_img {
grid-column: 1 / 3;
grid-row: 2;
z-index: 1;
}
}
@media (max-width: 499px) {
.parc_img {
grid-column: 1 / 3;
grid-row: 5/6;
z-index: 1;
}
}
/* DESCRIPTIONS (au-dessus des images) */
.studio_description {
grid-column: 2 / 5;
grid-row: 1;
z-index: 2;
}
@media (max-width: 499px) {
.studio_description {
grid-column: 2;
grid-row: 2/4;
z-index: 2;
}
}
.parc_description {
grid-column: 6 / 8;
grid-row: 1;
z-index: 2;
}
@media (max-width: 799px) {
.parc_description {
grid-column: 2 / 5;
grid-row: 2;
z-index: 2;
}
}
@media (max-width: 499px) {
.parc_description {
grid-column: 2;
grid-row: 6/8;
z-index: 2;
}
}
.banniere_pros--description {
background-color: var(--c-background-blanc-casse);
padding: 17px;
max-height: 170px;
&--titre {
font-size: 18px;
}
}
</style>

View File

@@ -0,0 +1,242 @@
<!-- Utilisé pour : -->
<!-- Index.vue / Tous à l'orchestre -->
<!-- Permet de filtrer des contenus grâce à des boutons -->
<template>
<!-- FILTRES -->
<SectionContent pad="xs" class="theme_tao--filters" role="tablist">
<DsButton
v-for="f in filters"
:key="f.key"
textColor="invert"
size="aplati"
borderColor="invert"
:aria-pressed="activeFilter === f.key"
:class="{ 'is-active': activeFilter === f.key }"
@click="activeFilter = f.key"
>
{{ f.label }}
</DsButton>
</SectionContent>
<!-- CARTES HORIZONTALES -->
<SectionContent class="theme_tao--cardlist">
<HorizontalCards
aria-label="Posts Instagram ONDIF"
hint-key="tao-cardlist-hint-seen"
:peek="30"
:fade-width="64"
:reset-key="activeFilter"
reset-behavior="smooth"
:reset-delay-ms="0"
>
<DsCard
v-for="card in filteredCards"
:key="card.id"
class="theme_tao--cardlist--card"
>
<div class="theme_tao--cardlist--card--wp">
<!-- Image -->
<div class="theme_tao--cardlist--card--img">
<NuxtImg :src="card.imgSrc" :alt="card.imgAlt" />
</div>
<!-- Content -->
<div class="theme_tao--cardlist--card--content">
<DsHeading as="h5" tone="default">
{{ card.title }}
</DsHeading>
<DsText as="p" tone="default" :clamp="6">
<span v-html="card.descriptionHtml" />
</DsText>
</div>
</div>
</DsCard>
</HorizontalCards>
</SectionContent>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import DsCard from '@root/design-system/components/DsCard.vue'
import DsHeading from '@root/design-system/primitives/DsHeading.vue'
import DsButton from '@root/design-system/primitives/DsButton.vue'
import SectionContent from '../components/section/SectionContent.vue'
import DsText from '@root/design-system/primitives/DsText.vue'
//--------------------------------------------------------------------------
// Filtres
//--------------------------------------------------------------------------
// Variable qui contient les catégories qui permettront de filtrer
const filters = [
{ key: 'famille', label: 'Famille' },
{ key: 'lieux', label: 'Lieux' },
{ key: 'ateliers', label: 'Ateliers' },
{ key: 'musique', label: 'Musique' },
]
// variable pour donner le filtre actif
// filtre par défaut : le premier
const activeFilter = ref(filters[0].key)
//--------------------------------------------------------------------------
// CONTENUS DES CARTES
//--------------------------------------------------------------------------
const cards = ref([
{
id: 'insta-1',
imgSrc: '/contenus/insta_ondif_1.jpg',
imgAlt: 'le titre du post',
title: "[Y'A PAS QUE LES ÂNES QUI MANGENT DU SON]",
descriptionHtml: `Tu as moins de 28 ans ? Paie 6 € la place dès 3 concerts de lOrchestre à la Philharmonie de Paris<br>
🔴 Ile de France | Ministère de la culture<br>
🎨 Agence belleville.eu`,
tags: ['musique','famille'],
},
{
id: 'insta-2',
imgSrc: '/contenus/insta_ondif_2.jpg',
imgAlt: 'le titre du post',
title: " Tous à lorchestre ! Un début de saison qui fait vibrer lOndif 🎶",
descriptionHtml: `Des lycéens conquis par Rachmaninov aux premières rencontres à Montereau, des petites oreilles émerveillées aux ateliers de Marly-la-Ville, sans oublier la musique portée jusqu'aux hôpitaux.<br>
Et plus de 250 choristes rassemblés pour le "Chantons avec" Alice Swing !<br>
Quelle énergie ❤️`,
tags: ['famille', 'ateliers', 'musique', 'lieux'],
},
{
id: 'insta-3',
imgSrc: '/contenus/insta_ondif_3.jpg',
imgAlt: 'le titre du post',
title: "L'Orchestre en résidence à Montereau",
descriptionHtml: `Notre équipe a rencontré les élèves de Montereau pour un premier atelier au Conservatoire municipal de musique Gaston Litaize`,
tags: ['lieux', 'ateliers', 'famille'],
},
{
id: 'insta-3',
imgSrc: '/contenus/insta_ondif_3.jpg',
imgAlt: 'le titre du post',
title: "L'Orchestre en résidence à Montereau",
descriptionHtml: `Notre équipe a rencontré les élèves de Montereau pour un premier atelier au Conservatoire municipal de musique Gaston Litaize`,
tags: ['lieux', 'ateliers', 'famille'],
},
{
id: 'insta-3',
imgSrc: '/contenus/insta_ondif_3.jpg',
imgAlt: 'le titre du post',
title: "L'Orchestre en résidence à Montereau",
descriptionHtml: `Notre équipe a rencontré les élèves de Montereau pour un premier atelier au Conservatoire municipal de musique Gaston Litaize`,
tags: ['lieux', 'ateliers', 'famille'],
},
{
id: 'insta-3',
imgSrc: '/contenus/insta_ondif_3.jpg',
imgAlt: 'le titre du post',
title: "L'Orchestre en résidence à Montereau",
descriptionHtml: `Notre équipe a rencontré les élèves de Montereau pour un premier atelier au Conservatoire municipal de musique Gaston Litaize`,
tags: ['lieux', 'ateliers', 'famille'],
},
])
//--------------------------------------------------------------------------
// Computed : cartes filtrées
//--------------------------------------------------------------------------
const filteredCards = computed(() => {
return cards.value.filter(card =>
card.tags.includes(activeFilter.value)
)
})
</script>
<style lang="scss">
.theme_tao {
margin-bottom: 50px;
&--description {
max-width: 800px;
}
&--txt {
margin-bottom: 35px;
}
&--filters {
display: flex;
flex-wrap: wrap;
column-gap: 10px;
row-gap: 14px;
.is-active {
background-color: var(--c-surface);
color: var(--c-backgroud-brandreverse);
}
}
&--cardlist {
margin-top: 20px;
&--card {
background-color: var(--c-surface);
border-radius: 10px;
//padding: 50px;
padding: clamp(18px, 2.4vw, 32px);
&--wp {
display: flex;
flex-direction: column;
align-items: center;
}
&--img {
max-width: 280px;
overflow: hidden;
img {
max-height: 280px;
}
@media (min-width: 400px) {
img {
max-height: 350px;
}
}
}
&--content {
max-width: 320px;
padding-top: 20px;
}
}
&--card {
/* base: mobile-first */
width: clamp(260px, 81vw, 360px);
}
@media (min-width: 400px) {
&--card {
width: clamp(260px, 80vw, 360px);
}
}
@media (min-width: 500px) {
&--card {
width: clamp(280px, 63vw, 460px);
}
}
@media (min-width: 700px) {
&--card {
width: clamp(280px, 60vw, 460px);
}
}
@media (min-width: 1024px) {
&--card {
width: clamp(320px, 33vw, 520px);
}
}
@media (min-width: 1440px) {
&--card {
width: clamp(360px, 28vw, 560px);
}
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<NewsletterCta />
<PageSection tone="brand" :padded="false" content-size="default" padt="sm" padb="sm" class="footer">
<PageSection tone="brand" content-size="default" padt="sm" padb="sm" class="footer">
<div class="footer--inner">
<SectionContent pad="xs" class="footer--logo">
<NuxtImg :src="logoDefault" :alt="brand.logoAlt" class="logo-img" />
@@ -26,19 +26,19 @@
<div class="nav--item nav--social">
<a class="social" href="...">
<NuxtImg :src="logoInstagram" alt="logo Instagram" height="37" width="37" class="social__icon" />
<NuxtImg :src="logoInstagram" alt="logo Instagram" height="30" width="30" class="social__icon" />
<span class="social__label">Instagram</span>
</a>
<a class="social" href="...">
<NuxtImg :src="logoyt" alt="logo You Tube" height="28" width="37" class="social__icon" />
<NuxtImg :src="logoyt" alt="logo You Tube" height="30" width="35" class="social__icon" />
<span class="social__label">YouTube</span>
</a>
<a class="social" href="...">
<NuxtImg :src="logofacebook" alt="logo Facebook" height="37" width="35" class="social__icon" />
<NuxtImg :src="logofacebook" alt="logo Facebook" height="30" width="28" class="social__icon" />
<span class="social__label">Facebook</span>
</a>
<a class="social" href="...">
<NuxtImg :src="logolinkedin" alt="logo LinkedIn" height="37" width="37" class="social__icon" />
<NuxtImg :src="logolinkedin" alt="logo LinkedIn" height="30" width="30" class="social__icon" />
<span class="social__label">LinkedIn</span>
</a>
@@ -51,7 +51,7 @@
</div>
</PageSection>
<PageSection :padded="false" content-size="default" padt="sm" padb="sm">
<PageSection content-size="default" padt="sm" padb="sm">
<SectionContent>
<div class="footer_logos">
<div><img class="footer_logos--img" :src="logoPrefet" height="80" alt="logo Préfet de la région d'Île-de-France" /></div>
@@ -60,7 +60,7 @@
</div>
</SectionContent>
</PageSection>
<PageSection :padded="false" content-size="default" padt="sm" padb="sm">
<PageSection content-size="default" padt="sm" padb="sm">
<SectionContent>
<div class="footer_mentions">
<div class="footer_mentions--item">© Orchestre national dÎle-de-France - 2026</div>
@@ -145,7 +145,7 @@
display: block;
padding: 4px;
border-radius: 5px;
background-color: rgb(255 255 255 / 83%);
background-color: rgb(255 255 255 / 60%);
}
}
}

View File

@@ -0,0 +1,303 @@
<!-- app/components/HorizontalCards.vue -->
<!-- Faire défiler des cartes qui se déplacent horizontalement -->
<!-- Les cartes sont dans le composant qui appelle celui-çi, donc cela vaut pour tous types de cartes-->
<template>
<!-- Root: classe is-scrolled pour piloter fade + hint -->
<div
class="hc"
:class="[
rootClass,
{ 'is-scrolled': hasScrolled }
]"
>
<!-- Optional title slot -->
<div v-if="$slots.title" class="hc__header">
<slot name="title" />
</div>
<!-- Hint icon (micro affordance) -->
<div v-if="showHint" class="hc__hint" aria-hidden="true">
<span class="hc__hint-icon">{{ hintIcon }}</span>
</div>
<div v-if="showHint" class="hc__hint--left" aria-hidden="true">
<span class="hc__hint-icon">{{ hintIcon }}</span>
</div>
<!-- Scroller -->
<div
ref="scroller"
class="hc__scroller"
:class="scrollerClass"
tabindex="0"
role="region"
:aria-label="ariaLabel"
@scroll.passive="onScroll"
>
<div class="hc__track" :class="trackClass">
<!-- Cards -->
<slot />
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref, computed, watch, nextTick } from 'vue'
const props = defineProps({
ariaLabel: { type: String, default: 'Horizontal content' },
/** Active lanimation "nudge" une seule fois (localStorage) */
nudgeOnce: { type: Boolean, default: true },
hintKey: { type: String, default: 'horizontal-cards-hint-seen' },
/** Nudge settings */
nudgePx: { type: Number, default: 48 },
nudgeDelayMs: { type: Number, default: 350 },
nudgeReturnDelayMs: { type: Number, default: 450 },
/** UI affordances */
showHint: { type: Boolean, default: true },
hintIcon: { type: String, default: '⇆' },
/** Peek: % de padding-right du scroller pour montrer la carte suivante */
peek: { type: Number, default: 30 }, // 2540 conseillé
/** Fade (px) : largeur du dégradé */
fadeWidth: { type: Number, default: 64 },
/** Classes hooks */
rootClass: { type: [String, Array, Object], default: '' },
scrollerClass: { type: [String, Array, Object], default: '' },
trackClass: { type: [String, Array, Object], default: '' },
// Reset scroll (ex: changement de filtre)
resetKey: { type: [String, Number], default: null },
resetBehavior: { type: String, default: 'smooth' }, // 'smooth' ou 'auto'
resetDelayMs: { type: Number, default: 0 },
})
const scroller = ref(null)
const hasScrolled = ref(false)
let t = null
const cssVars = computed(() => ({
'--hc-peek': `${props.peek}%`,
'--hc-fade-w': `${props.fadeWidth}px`
}))
const markSeen = () => {
try { localStorage.setItem(props.hintKey, '1') } catch (_) {}
}
const isSeen = () => {
try { return localStorage.getItem(props.hintKey) === '1' } catch (_) { return true }
}
const onScroll = () => {
if (!hasScrolled.value) {
hasScrolled.value = true
markSeen()
}
if (t) clearTimeout(t)
t = setTimeout(() => {}, 80)
}
onMounted(() => {
const el = scroller.value
if (!el) return
// Inject vars on root element
// (Vue naime pas style binding + class binding sur le root via computed uniquement,
// donc on set ici pour être sûr)
el.closest('.hc')?.style?.setProperty('--hc-peek', `${props.peek}%`)
el.closest('.hc')?.style?.setProperty('--hc-fade-w', `${props.fadeWidth}px`)
if (!props.nudgeOnce) return
if (isSeen()) return
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches
if (reduce) return
const canScroll = el.scrollWidth > el.clientWidth + 4
if (!canScroll) return
setTimeout(() => {
el.scrollBy({ left: props.nudgePx, behavior: 'smooth' })
setTimeout(() => {
el.scrollBy({ left: -props.nudgePx, behavior: 'smooth' })
markSeen()
}, props.nudgeReturnDelayMs)
}, props.nudgeDelayMs)
})
onBeforeUnmount(() => {
if (t) clearTimeout(t)
})
// Pour revenir à la première carte sur changement de filtre
const resetToStart = async () => {
const el = scroller.value
if (!el) return
await nextTick()
// Important : si tu changes le contenu + transition, un petit délai peut aider
const run = () => el.scrollTo({ left: 0, behavior: props.resetBehavior })
if (props.resetDelayMs > 0) {
setTimeout(run, props.resetDelayMs)
} else {
run()
}
}
watch(
() => props.resetKey,
(nv, ov) => {
// si cest la première fois (ov === null) tu peux choisir de reset ou pas
if (nv === ov) return
resetToStart()
}
)
</script>
<style lang="scss">
/* ==========================================================================
HorizontalCards (no lib)
- scroll-snap
- peek (last card cut)
- fade right
- hint icon
- nudge once (JS minimal)
========================================================================== */
.hc {
position: relative;
/* Vars (fallbacks) */
--hc-peek: 30%;
--hc-fade-w: 64px;
}
/* Title slot */
.hc__header {
margin-bottom: 0.75rem;
}
/* Hint icon */
.hc__hint {
pointer-events: none;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 6px 18px rgba(0,0,0,0.12);
opacity: 1;
transition: opacity 180ms ease, transform 180ms ease;
z-index: 2;
}
.hc__hint--left {
pointer-events: none;
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 6px 18px rgba(0,0,0,0.12);
opacity: 1;
transition: opacity 180ms ease, transform 180ms ease;
z-index: 2;
}
.hc__hint-icon {
font-size: 18px;
line-height: 1;
}
/* Scroller */
.hc__scroller {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
// Cacher la barre de défilement horizontal
/* Firefox */
scrollbar-width: none;
/* IE / Edge legacy */
-ms-overflow-style: none;
/* WebKit (Chrome, Safari, Edge Chromium) */
&::-webkit-scrollbar {
display: none;
}
/* ✅ peek */
padding: 0.25rem var(--hc-peek) 0.25rem 0;
outline: none;
/* Fade right */
&::after {
content: '';
pointer-events: none;
position: absolute;
top: 0;
right: 0;
width: var(--hc-fade-w);
height: 100%;
background: linear-gradient(
to left,
rgb(172 207 207 / 59%),
rgba(172, 207, 207, 0)
);
opacity: 1;
transition: opacity 180ms ease;
z-index: 1;
}
&:focus-visible {
outline: 2px solid currentColor;
outline-offset: 6px;
}
}
/* Track */
.hc__track {
display: flex;
flex-wrap: nowrap;
gap: 20px;
align-items: stretch;
}
/* Snap + “rail-friendly” defaults for children */
.hc__track > * {
flex: 0 0 auto;
scroll-snap-align: start;
}
/* After first scroll: calm UI */
.hc.is-scrolled {
.hc__hint, .hc__hint--left {
background: rgba(172, 207, 207, 0.9);
//opacity: 0;
transform: translateY(-50%) scale(0.96);
}
.hc__scroller::after {
opacity: 0.55;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="newslettercta">
<PageSection tone="brandreverse" :padded="false" content-size="default" padb="xs">
<PageSection tone="brandreverse" content-size="default" padb="xs">
<SectionTitle as="h1" tone="invert" pad="xs">
NEWSLETTER
</SectionTitle>
@@ -42,10 +42,8 @@
.newslettercta__content {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: center;
row-gap: 15px;
column-gap: 15px;
column-gap: 25px;
padding-right: 10px;
.newslettercta__content_text {
@@ -53,12 +51,18 @@
}
.newslettercta__button {
grid-column: 1;
justify-self: start;
justify-self: center;
}
@media (min-width: 1000px) {
.newslettercta__button {
justify-self: center;
margin-top: -10px;
}
}
}
@media (max-width: 300px) {
.newslettercta__content {
padding-left: calc(var(--section-title-pl, var(--sp-45)) - 20px);
padding-left: calc(var(--sp-45) - 20px);
}
}
@media (min-width: 500px) {

View File

@@ -0,0 +1,75 @@
<template>
<DsCard class="square-card">
<div class="square-card__grid">
<!-- Image -->
<div class="square-card__media">
<DsMedia :src="imgSrc" :alt="imgAlt" ratio="square" />
</div>
<!-- Content -->
<div class="square-card__content">
<DsHeading as="h5" tone="default" class="square-card__title">
{{ title }}
</DsHeading>
<!-- Description -->
<DsText as="p" tone="default" :clamp="3" class="square-card__description">
{{ description }}
</DsText>
</div>
</div>
</DsCard>
</template>
<script setup>
import DsCard from '@root/design-system/components/DsCard.vue'
import DsMedia from '@root/design-system/primitives/DsMedia.vue'
import DsHeading from '@root/design-system/primitives/DsHeading.vue'
import DsText from '@root/design-system/primitives/DsText.vue'
import DsButtonArrow from '@root/design-system/primitives/DsButtonArrow.vue'
defineProps({
id: { type: [String, Number], required: true },
title: { type: String, required: true },
url: { type: String, required: true },
description: { type: String, default: '' },
imgSrc: { type: String, default: '' },
imgAlt: { type: String, default: '' },
})
</script>
<style lang="scss">
.square-card__grid {
display: grid;
align-items: start;
}
.square-card__content {
display: grid;
gap: var(--sp-6);
max-width: 518px;
padding-left: 22px;
padding-right: 20px;
padding-top: 13px;
padding-bottom: 10px;
background: white;
max-width: 85%;
justify-self: center;
position: relative;
margin-top: -50px;
}
.square-card__meta {
margin-top: calc(var(--sp-4) * -1);
}
.square-card__description {
margin-top: 10px;
margin-bottom: 20px;
}
.square-card__actions {
margin-top: auto;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="square-card-list">
<slot />
</div>
</template>
<style lang="scss">
.square-card-list {
display: flex;
flex-wrap: wrap;
gap: var(--gap-cards);
justify-content: flex-start;
}
@media (max-width: 572px) {
.square-card-list {
justify-content: center;
}
}
// Afficher seulement 1 cards < 600px
@media (max-width: 599px) {
.square-card-list > .square-card:nth-child(2) {
display: none;
}
.square-card-list > .square-card:nth-child(3) {
display: none;
}
}
// Afficher seulement 2 cards < 900px
@media (max-width: 799px) {
.square-card-list > .square-card:nth-child(n+3) {
display: none;
}
}
// Taille des cards
@media (max-width: 599px) {
.square-card-list > .square-card {
max-width: 260px;
flex: 1 1 260px;
max-width: 90%;
}
}
@media (min-width: 600px) {
.square-card-list > .square-card {
max-width: 260px;
flex: 1 1 260px;
}
}
@media (min-width: 700px) {
.square-card-list > .square-card {
max-width: 280px;
flex: 1 1 280px;
}
}
@media (min-width: 800px) {
.square-card-list > .square-card {
max-width: 280px;
flex: 1 1 280px;
}
}
@media (min-width: 900px) {
.square-card-list > .square-card {
max-width: 260px;
flex: 1 1 260px;
}
}
@media (min-width: 1000px) {
.square-card-list > .square-card {
max-width: 280px;
flex: 1 1 280px;
}
}
@media (min-width: 1100px) {
.square-card-list > .square-card {
max-width: 300px;
flex: 1 1 300px;
}
}
@media (min-width: 1200px) {
.square-card-list > .square-card {
max-width: 258px;
flex: 1 1 258px;
}
}
@media (min-width: 1300px) {
.square-card-list > .square-card {
max-width: 283px;
flex: 1 1 283px;
}
}
@media (min-width: 1400px) {
// style appliqué au composant enfant via sa classe
.square-card-list > .square-card {
max-width: 308px;
flex: 1 1 308px;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<DsCard class="textleft-card">
<div class="textleft-card__grid">
<!-- Content -->
<div class="textleft-card__content">
<DsHeading as="h5" tone="default" class="textleft-card__title">
{{ title }}
</DsHeading>
<!-- Description -->
<DsText as="p" tone="default" :clamp="3" class="textleft-card__description">
{{ description }}
</DsText>
<!-- Actions -->
<div class="concert-card__actions">
<DsButtonArrow :to="`/concerts/${id}`" variant="secondary">
Découvrir
</DsButtonArrow>
</div>
</div>
</div>
</DsCard>
</template>
<script setup>
import DsCard from '@root/design-system/components/DsCard.vue'
import DsMedia from '@root/design-system/primitives/DsMedia.vue'
import DsHeading from '@root/design-system/primitives/DsHeading.vue'
import DsText from '@root/design-system/primitives/DsText.vue'
import DsButtonArrow from '@root/design-system/primitives/DsButtonArrow.vue'
defineProps({
id: { type: [String, Number], required: true },
title: { type: String, required: true },
url: { type: String, required: true },
description: { type: String, default: '' },
})
</script>
<style lang="scss">
.textleft-card__grid {
display: grid;
align-items: start;
}
.textleft-card__content {
display: grid;
gap: var(--sp-6);
max-width: 518px;
padding-left: 10px;
padding-right: 20px;
padding-top: 3px;
background: white;
border-left: 2px solid;
&:hover {
border-left-color: var(--c-brand_rouge);
}
}
.textleft-card__meta {
margin-top: calc(var(--sp-4) * -1);
}
.textleft-card__description {
margin-top: 10px;
margin-bottom: 20px;
}
.textleft-card__actions {
margin-top: auto;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="textleft-card-list">
<slot />
</div>
</template>
<style lang="scss">
.textleft-card-list {
display: flex;
flex-wrap: wrap;
gap: var(--gap-cards);
justify-content: flex-start;
}
@media (max-width: 572px) {
.textleft-card-list {
justify-content: center;
}
}
// Afficher seulement 1 cards < 600px
@media (max-width: 599px) {
.textleft-card-list > .textleft-card:nth-child(2) {
display: none;
}
.textleft-card-list > .textleft-card:nth-child(3) {
display: none;
}
}
// Afficher seulement 2 cards < 900px
@media (max-width: 799px) {
.textleft-card-list > .textleft-card:nth-child(3) {
display: none;
}
}
// Taille des cards
@media (max-width: 599px) {
.textleft-card-list > .textleft-card {
max-width: 260px;
flex: 1 1 260px;
max-width: 90%;
}
}
@media (min-width: 600px) {
.textleft-card-list > .textleft-card {
max-width: 247px;
flex: 1 1 247px;
}
}
@media (min-width: 700px) {
.textleft-card-list > .textleft-card {
max-width: 280px;
flex: 1 1 280px;
}
}
@media (min-width: 800px) {
.textleft-card-list > .textleft-card {
max-width: 231px;
flex: 1 1 231px;
}
}
@media (min-width: 900px) {
.textleft-card-list > .textleft-card {
max-width: 258px;
flex: 1 1 258px;
}
}
@media (min-width: 1000px) {
.textleft-card-list > .textleft-card {
max-width: 288px;
flex: 1 1 288px;
}
}
@media (min-width: 1100px) {
.textleft-card-list > .textleft-card {
max-width: 311px;
flex: 1 1 311px;
}
}
@media (min-width: 1200px) {
.textleft-card-list > .textleft-card {
max-width: 334px;
flex: 1 1 334px;
}
}
@media (min-width: 1300px) {
.textleft-card-list > .textleft-card {
max-width: 367px;
flex: 1 1 367px;
}
}
@media (min-width: 1400px) {
// style appliqué au composant enfant via sa classe
.textleft-card-list > .textleft-card {
max-width: 400px;
flex: 1 1 400px;
}
}
</style>

View File

@@ -1,11 +1,10 @@
<script setup>
import logoDefault from '/img/logos/logo_orchestre_red.svg'
import agendaRouge from '@/assets/img/icones/agenda_rouge.svg'
import ticket from '@/assets/img/icones/ticket_black.svg'
import mobile_agenda_icon from '@/assets/img/icones/agenda_rouge_fonce_blanc.svg'
import mobile_ticket from '@/assets/img/icones/ticket_white.svg'
import ticket from '@/assets/img/icones/ticket_noir.svg'
import mobile_agenda_icon from '@/assets/img/icones/agenda_blanc_non_opaque.svg'
import mobile_ticket from '@/assets/img/icones/ticket_blanc.svg'
const { brand } = useAppConfig()
</script>
<template>
@@ -18,17 +17,31 @@
<img :src="agendaRouge" alt="icone agenda" />
</template>
<template #ticket>
<img class="img_ticket" :src="ticket" alt="icone ticket" />
<img class="img_ticket_desktop" :src="ticket" alt="icone ticket" />
</template>
<template #mobile_agenda_icon>
<img :src="mobile_agenda_icon" alt="icone ticket" />
</template>
<template #mobile_ticket>
<img :src="mobile_ticket" alt="icone ticket" />
<img class="img_ticket_mob" :src="mobile_ticket" alt="icone ticket" />
</template>
</HeaderNav>
</template>
<style lang="scss">
.img_ticket_desktop {
max-height: 26px;
margin-top: 4px;
margin-left: -11px;
}
.img_ticket_mob {
max-height: 37px;
margin-top: 9px;
margin-left: -11px;
}
</style>

View File

@@ -32,7 +32,7 @@
<nav class="header_nav_cont" aria-label="Primary navigation">
<!-- Desktop nav -->
<ul class="header_nav header_nav--desktop">
<li class="header_nav_item brandontext_bold">
<li class="header_nav_item">
L'Orchestre
<ul class="header_nav_sub_menu">
<li class="header_nav_sub_menu_item"><NuxtLink to="/">Nos missions</NuxtLink></li>
@@ -56,7 +56,7 @@
</ul>
</li>
<li class="header_nav_item brandontext_bold">
<li class="header_nav_item">
Éducation et médiation
<ul class="header_nav_sub_menu">
<li class="header_nav_sub_menu_item"><NuxtLink to="/">Petite enfance</NuxtLink></li>
@@ -156,7 +156,7 @@
>
<ul class="header_drawer_inner">
<li
class="header_drawer_link brandontext_bold"
class="header_drawer_link"
:class="{ 'is-open': activeDrawer === 'orchestre' }"
@click="toggleDrawer('orchestre')"
>
@@ -188,7 +188,7 @@
</li>
<li
class="header_drawer_link brandontext_bold"
class="header_drawer_link"
:class="{ 'is-open': activeDrawer === 'education' }"
@click="toggleDrawer('education')"
>
@@ -217,15 +217,17 @@
</ul>
</li>
<li class="icon_mobile">
<NuxtLink class="header_drawer_link icon_mobile_agenda" to="/agenda" @click="close">
<!-- ICÔNE injectée -->
<slot name="mobile_agenda_icon" />
</NuxtLink>
<NuxtLink class="header_drawer_link icon_mobile_ticket" to="/agenda" @click="close">
<!-- ICÔNE injectée -->
<slot name="mobile_ticket" />
</NuxtLink>
</li>
</ul>
</div>
@@ -262,6 +264,12 @@
</script>
<style lang="scss">
.icon_mobile {
display: flex;
column-gap: 20px;
padding-left: 3px;
}
.icon_mobile_agenda {
width: 35px;
margin-top: 10px;

View File

@@ -7,12 +7,12 @@
<template>
<header>
<div class="bg-orbs" aria-hidden="true">
<span class="orb orb--1" />
<span class="orb orb--2" />
<span class="orb orb--3" />
<span class="orb orb--4" />
<span class="orb orb--5" />
<span class="orb orb--6" />
<!-- <span class="orb orb--1" /> -->
<!-- <span class="orb orb--2" /> -->
<!-- <span class="orb orb--3" /> -->
<!-- <span class="orb orb--4" /> -->
<!-- <span class="orb orb--5" /> -->
<!-- <span class="orb orb--6" /> -->
</div>
<div class="header_layout" :class="`header--${theme}`">
<slot />

View File

@@ -8,12 +8,14 @@
<section
class="page-section"
:class="[
`page-section--overflow--${overflow}`,
`page-section--${tone}`,
{ 'page-section--padded': padded }
{ 'page-section--padded': padded },
`page-section--padded--${padded_size}`
]"
>
<!-- Si content == true -->
<PageSectionInner v-if="content" :size="contentSize" :padt="padt" :padb="padb">
<PageSectionInner v-if="content" :size="contentSize" :padt="padt" :padb="padb" :position="position" :overflow="overflow">
<slot />
</PageSectionInner>
@@ -28,11 +30,13 @@
<script setup>
defineProps({
tone: { type: String, default: 'default' }, // default / brand / muted / dark…
padded: { type: Boolean, default: true }, // padding vertical
padded_size: { type: String, default: '' }, // none | sm | md | lg
contentSize: { type: String, default: 'default'}, // narrow/default/wide
content: { type: Boolean, default: true }, // contenu contraint ou full
padb : { type: String, default: '' }, // props pour PageSectionInner
padt : { type: String, default: '' } // props pour PageSectionInner
padt : { type: String, default: '' }, // props pour PageSectionInner
position : { type: String, default: '' },
overflow : { type: String, default: '' }
})
</script>
@@ -41,6 +45,9 @@
position: relative;
width: 100%;
//min-height: var(--sp-200);
&--overflow--hidden {
overflow: hidden;
}
/* tons = arrière-plan section */
&--default { background: transparent; }
@@ -50,9 +57,15 @@
// padding en haut et en bas
&--padded {
&--md {
padding-top: 30px;
padding-bottom: 50px;
}
&--lg {
padding-top: 30px;
padding-bottom: 120px;
}
}
}

View File

@@ -2,7 +2,7 @@
Des templates peuvent avoir toutes la même marge de page et d'autres, par ex, être full page -->
<template>
<div class="page-section--inner" :class="[`page-section--inner--${size}`,`page-section--inner--padb--${padb}`,`page-section--inner--padt--${padt}`]">
<div class="page-section--inner" :class="[`page-section--inner--${size}`,`page-section--inner--padb--${padb}`,`page-section--inner--padt--${padt}`,`page-section--inner--position--${position}`,`page-section--inner--overflow--${overflow}`]">
<slot />
</div>
</template>
@@ -11,7 +11,8 @@
defineProps({
size: { type: String, default: 'default' }, // default / wide / narrow
padb : { type: String, default: '' },
padt : { type: String, default: '' }
padt : { type: String, default: '' },
position : { type: String, dafault : ''}
})
</script>
@@ -25,7 +26,9 @@
// respiration sur les côtés avec marges minimale ( surtout utile pour mobiles)
&--position--relative {
position: relative;
}
// limite de largeur quand on veut une largeur plus petit (pour les articles par exemple)
&--narrow {
@@ -37,7 +40,7 @@
/* mobile / small screens */
@media (max-width: 700px) {
padding-inline: var(--page-padding-mobile);
//padding-inline: var(--page-padding-mobile);
}
@media (min-width: 0px) {

View File

@@ -29,30 +29,44 @@
&--tone-brandreverse { background: var(--c-backgroud-brandreverse); }
&--pad-xs {
padding-top: var(--sp-32);
//padding-top: var(--sp-32);
padding-bottom: var(--sp-16);
padding-left: var(--sp-45);
padding-right: var(--sp-8);
}
&--pad-sm {
padding-top: var(--sp-32);
//padding-top: var(--sp-32);
padding-bottom: var(--sp-16);
padding-left: var(--sp-45);
padding-right: var(--sp-8);
}
&--pad-md {
padding-top: var(--sp-80);
//padding-top: var(--sp-80);
padding-bottom: var(--sp-180);
padding-left: var(--sp-45);
padding-right: var(--sp-8);
}
&--pad-lg {
padding-top: var(--sp-80);
//padding-top: var(--sp-80);
padding-bottom: var(--sp-180);
padding-left: var(--sp-45);
padding-right: var(--sp-8);
}
}
@media (max-width: 400px) {
.section-content {
&--pad-xs {
//padding-bottom: calc(var(--sp-16) * 0.5);
padding-left: calc(var(--sp-45) * 0.5);
//padding-right: calc(var(--sp-8) * 0.5);
}
}
}
@media (max-width: 300px) {
.section-title {
padding-left: calc(var(--section-title-pl, var(--sp-45)) - 20px);
padding-left: calc(var(--sp-45) - 20px);
}
}

View File

@@ -45,10 +45,17 @@
padding-left: var(--sp-45);
}
}
@media (max-width: 400px) {
.section-title {
&--pad-xs {
padding-left: calc(var(--sp-45) * 0.5);
}
}
}
@media (max-width: 300px) {
.section-title {
padding-left: calc(var(--section-title-pl, var(--sp-45)) - 20px);
padding-left: calc(var(--sp-45) - 20px);
}
}

View File

@@ -1,7 +1,7 @@
<template>
<!-- Cas avec cadre couleur full page mais contenu dans marge -->
<PageSection tone="dark" :padded="true" content-size="default">
<PageSection tone="dark" padded_size="md" content-size="default">
<DsHeading as="h1" size="">CONCERTS À VENIR...</DsHeading>
<ConcertCard
id="1"
@@ -18,7 +18,7 @@
</PageSection>
<!-- Cas normal : toute la section est contenu dans les marges -->
<PageSection :padded="true" content-size="default">
<PageSection padded_size="md" content-size="default">
<DsHeading as="h1" size="">PAR TOUS ET POUR TOUS</DsHeading>
</PageSection>

View File

@@ -1,14 +1,18 @@
<template>
<!-- ================== -->
<!-- Fond noir -->
<PageSection tone="dark" :padded="false" content-size="default">
<!-- ================== -->
<PageSection tone="dark" content-size="default">
<SectionTitle tone="invert" pad="md">
CONCERTS À VENIR
</SectionTitle>
</PageSection>
<!-- ================== -->
<!-- Listes des prochains conncerts -->
<PageSection content-size="default" class="remonter_concert_list">
<!-- ================== -->
<PageSection padded_size="md" content-size="default" class="remonter_concert_list">
<ConcertCardList>
<ConcertCard
id="1"
@@ -49,16 +53,18 @@
</ConcertCardList>
</PageSection>
<!-- Carte Île-De-France Par tous et pour tous -->
<PageSection class="theme_ptpt_wp">
<SectionContent class="theme_ptpt">
<DsMedia :src="ptpt_img" alt="Carte Île-De-France" class="theme_ptpt--img"/>
<SectionContent tone="brand_rouge45" pad="xs" class="theme_ptpt--content">
<!-- ================== -->
<!-- Carte Île-De-France Partout et pour tous -->
<!-- ================== -->
<PageSection padded_size="md" class="theme_ppt_wp">
<SectionContent class="theme_ppt">
<DsMedia :src="ppt_img" alt="Carte Île-De-France" class="theme_ppt--img"/>
<SectionContent tone="brand_rouge45" pad="" class="theme_ppt--content">
<SectionTitle tone="invert" pad="xs">
PAR TOUS ET POUR TOUS
PARTOUT ET POUR TOUS
</SectionTitle>
<SectionContent pad="xs" class="theme_ptpt--description">
<DsText tone="invert" size="lg" class="theme_ptpt--txt" >
<SectionContent pad="xs" class="theme_ppt--description">
<DsText tone="invert" size="lg" class="theme_ppt--txt" >
Ici le texte qui décrit le concept de Tous à lOrchestre - Dans les régions - Ici le texte qui décrit le concept de Tous à lOrchestre - Dans les régions - Ici le texte qui décrit le concept de Tous à lOrchestre - Dans les régions -
</DsText>
<DsButtonArrow to="/" variant="invert">
@@ -69,22 +75,92 @@
</SectionContent>
</PageSection>
<!-- ================== -->
<!-- Tous à l'Orchestre -->
<PageSection tone="brandreverse" :padded="true" content-size="default" padb="xs" class="theme_tao">
<!-- ================== -->
<PageSection tone="brandreverse" padded_size="md" content-size="default" padb="xs" class="theme_tao">
<SectionContent>
<SectionTitle tone="invert" pad="xs">
TOUS À LORCHESTRE
</SectionTitle>
<SectionContent pad="xs" class="theme_ptpt--description">
<DsText tone="invert" size="lg" class="theme_ptpt--txt" >
<SectionContent pad="xs" class="theme_tao--description">
<DsText tone="invert" size="lg" class="theme_tao--txt" >
Phrase daccroche / explicative de cette rubrique par tous et pour tous
</DsText>
</SectionContent>
</SectionContent>
<!-- LES CARTES -->
<FilteredCards />
</PageSection>
<!-- ================== -->
<!-- ACTUALITÉS -->
<!-- ================== -->
<PageSection padded_size="md">
<SectionContent>
<SectionTitle tone="" pad="xs">
LES DERNIÈRES ACTUALITÉS
</SectionTitle>
</SectionContent>
<SquareCardList >
<SquareCard
v-for="actuscard in actuscards"
:key="actuscard.id"
:id="actuscard.id"
:imgSrc="actuscard.imgSrc"
:imgAlt="actuscard.imgAlt"
:title="actuscard.title"
:description="actuscard.description"
:url="actuscard.url"
></SquareCard>
</SquareCardList>
</PageSection>
<!-- ================== -->
<!-- ONDIF MAG -->
<!-- ================== -->
<PageSection padded_size="md" position="relative" overflow="hidden" class="theme_mag">
<div class="decoration--mag"><img src="/img/decoration/ellipse_mag.svg" alt=""></div>
<SectionContent>
<SectionTitle tone="" pad="xs">
ONDIF MAG
</SectionTitle>
<SectionContent pad="xs" class="theme_tao--description">
<DsText tone="" size="lg" class="theme_tao--txt" >
Le magazine en ligne de lOrchestre national dÎle-de-France vous invite à découvrir lactualité des concerts de lorchestre, des interprètes, des chefs et cheffes, de la création contemporaine et des projets de lONDIF sur le territoire francilien et au-delà. Portraits de musiciens, de compositeurs, dartistes invités, décryptages dœuvres, partez à la découverte de lactualité musicale !
</DsText>
</SectionContent>
</SectionContent>
<SectionContent pad="xs">
<TextCardLeftList >
<TextCardLeft
v-for="magcard in magcards"
:key="magcard.id"
:id="magcard.id"
:title="magcard.title"
:description="magcard.description"
:url="magcard.url"
></TextCardLeft>
</TextCardLeftList>
</SectionContent>
</PageSection>
<!-- ================== -->
<!-- L'ORCHESTRE POUR LES PROS -->
<!-- ================== -->
<PageSection padded_size="lg">
<SectionContent>
<SectionTitle tone="" pad="xs">
L'ORCHESTRE POUR LES PROS
</SectionTitle>
</SectionContent>
<SectionContent>
<BannierePros />
</SectionContent>
</PageSection>
</template>
@@ -92,10 +168,13 @@
import { onMounted, computed } from 'vue'
import { clientLog } from '~/utils/clientLog'
import SectionContent from '../components/section/SectionContent.vue'
import DsHeading from '@root/design-system/primitives/DsHeading.vue'
import DsText from '@root/design-system/primitives/DsText.vue'
import DsMedia from '@root/design-system/primitives/DsMedia.vue'
import DsButton from '@root/design-system/primitives/DsButton.vue'
import DsButtonArrow from '@root/design-system/primitives/DsButtonArrow.vue'
import ptpt_img from '@/assets/img/illustrations/map_idf.jpg'
import ppt_img from '@/assets/img/illustrations/map_idf.jpg'
import DsCard from '@root/design-system/components/DsCard.vue'
// Layout utilisé
definePageMeta({ layout: 'default' })
@@ -143,6 +222,70 @@
clientLog('info', 'test de log depuis vuejs', { })
clientLog('info', `STRAPI_URL : ${STRAPI_URL}`, { strapiUrl: STRAPI_URL })
})
//--------------------
// DONNÉES POUR LES ACTUS
//--------------------
const actuscards = ref([
{
id: '1',
imgSrc: '/contenus/actu1_1.jpg',
imgAlt: 'Les prochains concours',
title: "Les prochains concours",
description: `Inscrivez-vous aux auditions de l'Orchestre national d'Île-de-France.
Un nombre maximum de caractère sera défini pour le texte qui figure dans la description de lactu sur la page daccueil.`,
url:"#",
},
{
id: '2',
imgSrc: '/contenus/actu1_1.jpg',
imgAlt: 'Du sur-mesure pour les petites oreilles',
title: "Du sur-mesure pour les petites oreilles",
description: `Les ateliers 0-3 ans sont de retour !`,
url:"#",
},
{
id: '3',
imgSrc: '/contenus/actu1_1.jpg',
imgAlt: "Académie d'Orchestre 2026",
title: "Académie d'Orchestre 2026",
description: `Les ateliers 0-3 ans sont de retour !`,
url:"#",
},
{
id: '4',
imgSrc: '/contenus/actu1_1.jpg',
imgAlt: "Pablo González, nouveau directeur musical pour la saison 27.28",
title: "Pablo González, nouveau directeur musical pour la saison 27.28",
description: ``,
url:"#",
},
])
//--------------------
// DONNÉES POUR ONDIF MAG
//--------------------
const magcards = ref([
{
id: '1',
title: "LA FANTASTIQUE DE BERLIOZ",
description: `Lauteur suppose quun jeune musicien, affecté de cette maladie morale quun écrivain célèbre appelle le vague des passions, voit pour la première fois une femme qui réunit tous les charmes de lêtre idéal que rêvait son imagination...`,
url:"#",
},
{
id: '2',
title: "LITALIE BAROQUE, BERCEAU DU CONCERTO",
description: `Cest dans lItalie baroque du début du XVIIe siècle que naît le Stile Concertato (style concertant), et plus précisément à Venise, autour des grands maîtres qui ont œuvré à la basilique Saint-Marc, comme les Gabrieli.`,
url:"#",
},
{
id: '3',
title: "BEETHOVEN : LA SYMPHONIE N°4, CHEF-DOEUVRE À LA FOIS CLASSIQUE ET ROMANTIQUE",
description: `Avant la première exécution publique de lœuvre au Theater an der Wien, la partition de la Quatrième symphonie de Beethoven avait dabord été créée dans le palais du prince Lobkowitz en mars 1807.`,
url:"#",
},
])
</script>
<style lang="scss">
@@ -150,13 +293,13 @@
transform: translateY(-170px);
}
.theme_ptpt_wp {
.theme_ppt_wp {
margin-bottom: 50px;
}
.theme_ptpt {
display: grid;
// parce que le bloc du dessus à un transform: translateY(-170px); alors cela laisse un espace vide que l'on comble avec ce margin-top négatif
margin-top: -170px;
}
.theme_ppt {
display: grid;
&--img {
grid-row: 1;
@@ -165,11 +308,14 @@
&--content {
grid-row: 1;
grid-column: 1;
display: grid;
align-items: center;
}
&--description {
max-width: 520px;
margin-top: 35px;
max-width: 800px;
align-self: self-start;
// margin-top: 35px;
}
&--txt {
margin-bottom: 35px;
@@ -178,6 +324,146 @@
.theme_tao {
margin-bottom: 50px;
&--description {
max-width: 800px;
}
&--txt {
margin-bottom: 35px;
}
&--filters {
display: flex;
flex-wrap: wrap;
column-gap: 10px;
row-gap: 14px;
}
&--cardlist {
margin-top: 20px;
&--card {
background-color: var(--c-surface);
border-radius: 10px;
//padding: 50px;
padding: clamp(18px, 2.4vw, 32px);
&--wp {
display: flex;
flex-direction: column;
align-items: center;
}
&--img {
max-width: 280px;
overflow: hidden;
img {
max-height: 280px;
}
@media (min-width: 400px) {
img {
max-height: 350px;
}
}
}
&--content {
max-width: 320px;
padding-top: 20px;
}
}
&--card {
/* base: mobile-first */
width: clamp(260px, 81vw, 360px);
}
@media (min-width: 400px) {
&--card {
width: clamp(260px, 80vw, 360px);
}
}
@media (min-width: 500px) {
&--card {
width: clamp(280px, 63vw, 460px);
}
}
@media (min-width: 700px) {
&--card {
width: clamp(280px, 60vw, 460px);
}
}
@media (min-width: 1024px) {
&--card {
width: clamp(320px, 33vw, 520px);
}
}
@media (min-width: 1440px) {
&--card {
width: clamp(360px, 28vw, 560px);
}
}
}
}
.theme_mag {
@media (max-width: 499px) {
min-height: 650px;
}
@media (min-width: 500px) {
min-height: 650px;
}
@media (min-width: 700px) {
min-height: 650px;
}
@media (min-width: 900px) {
min-height: 650px;
}
@media (min-width: 1100px) {
min-height: 650px;
}
@media (min-width: 1300px) {
min-height: 650px;
}
@media (min-width: 1500px) {
min-height: 650px;
}
.decoration--mag {
position: absolute;
top: -20px;
@media (max-width: 499px) {
right: -150px;
}
@media (min-width: 500px) {
right: -120px;
}
@media (min-width: 700px) {
right: -100px;
}
@media (min-width: 900px) {
right: -100px;
}
@media (min-width: 1100px) {
right: -140px;
}
@media (min-width: 1300px) {
right: -100px;
}
@media (min-width: 1500px) {
right: -50px;
}
img {
height: 600px;
}
}
}

View File

@@ -116,6 +116,11 @@
}
/* Sizes */
&--aplati {
font-size: var(--text-md);
padding-inline: var(--sp-14);
min-height: 2rem;
}
&--sm {
font-size: var(--text-sm);
padding: var(--sp-4) var(--sp-8);

View File

@@ -12,6 +12,7 @@
--c-brand_rouge-weak: #e3061391;
--c-backgroud-black: #111;
--c-backgroud-brandreverse: #ACCFCF;
--c-background-blanc-casse: #FCFCFC;
/* États */
--c-success: green;

View File

@@ -26,3 +26,7 @@
--gap-cards: var(--sp-22);
}
}
.position--relative {
position: relative;
}

View File

@@ -7,6 +7,7 @@
--sp-6: 0.375rem; /* 6px */
--sp-8: 0.5rem; /* 8px */
--sp-12: 0.75rem; /* 12px */
--sp-14: 0.875rem; /* 14px */
/* Spacing standard */
--sp-16: 1rem; /* 16px */

View File

@@ -33,7 +33,7 @@
/* Line heights */
--lh-tight: 1.15;
--lh-snug: 1.25;
--lh-base: 1.5;
--lh-base: 1.3;
/* Letter spacing (optionnel mais utile) */
--ls-tight: -0.01em;

3
doc_contenu.md Normal file
View File

@@ -0,0 +1,3 @@
# Page Accueil
- bloc "Partout et pour tous"
- 700 signes (espaces compris) max

13
doc_generale.md Normal file
View File

@@ -0,0 +1,13 @@
# Page rendu architecture
1. app.vue contient <NuxtPage />, qui affiche la page courante (ex. index.vue).
2. Chaque page est rendue dans son layout (par défaut default.vue).
3. Dans default.vue, le <main> contient un <slot /> : cest là que le contenu de la page sinsère.
Donc le flux est : app.vue → <NuxtPage /> → layout default.vue → <main><slot /></main> → contenu de pages/*.
# Header
1. Dans le layout default le header contient
1.1 un contenant => <HeaderWrapper>
1.2 ce contenant contient le header => <HeaderDefault>

View File

@@ -47,6 +47,8 @@ export default defineNuxtConfig({
runtimeConfig: {
// Server-side only (jamais exposé au client)
strapiToken: process.env.STRAPI_API_TOKEN || '',
instagramAppId: process.env.NUXT_INSTAGRAM_APP_ID,
instagramClientToken: process.env.NUXT_INSTAGRAM_CLIENT_TOKEN,
// 🌍 Public (accessible dans le navigateur)
public: {

BIN
public/contenus/actu1_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/favicon_nuxt.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="466" height="932" viewBox="0 0 466 932" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M466 0C466 275.2 466 233.327 466 490.692C466 748.056 466 707.305 466 932C208.635 932 0 723.365 0 466C0 208.635 208.635 0 466 0Z" fill="#E30613" fill-opacity="0.59"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,108 @@
function isValidUrl(u) {
try {
new URL(u);
return true;
} catch {
return false;
}
}
function isInstagramPostUrl(u) {
try {
const url = new URL(u);
const host = url.hostname.replace(/^www\./, "");
if (host !== "instagram.com") return false;
const p = url.pathname;
return p.startsWith("/p/") || p.startsWith("/reel/") || p.startsWith("/tv/");
} catch {
return false;
}
}
export default defineCachedEventHandler(
async (event) => {
const config = useRuntimeConfig();
const appId = config.instagramAppId;
const clientToken = config.instagramClientToken;
if (!appId || !clientToken) {
throw createError({
statusCode: 500,
statusMessage:
"Missing Instagram oEmbed credentials (instagramAppId / instagramClientToken).",
});
}
const body = await readBody(event).catch(() => null);
if (!body || !Array.isArray(body.urls)) {
throw createError({
statusCode: 400,
statusMessage: "Invalid payload. Expected { urls: string[] }",
});
}
// normalise + filtre
const urls = body.urls
.map((u) => String(u).trim())
.filter((u) => isValidUrl(u))
.filter((u) => isInstagramPostUrl(u))
.slice(0, 24);
if (urls.length === 0) {
return { results: [] };
}
// Token Meta oEmbed: APP_ID|CLIENT_TOKEN
const accessToken = `${appId}|${clientToken}`;
const results = await Promise.all(
urls.map(async (url) => {
const endpoint = new URL(
"https://graph.facebook.com/v19.0/instagram_oembed"
);
endpoint.searchParams.set("url", url);
endpoint.searchParams.set("omitscript", "true");
endpoint.searchParams.set("access_token", accessToken);
try {
const data = await $fetch(endpoint.toString(), { timeout: 10000 });
return {
url,
ok: true,
html: data?.html || "",
title: data?.title ?? null,
author_name: data?.author_name ?? null,
thumbnail_url: data?.thumbnail_url ?? null,
};
} catch (e) {
return {
url,
ok: false,
error:
e?.data?.error?.message ||
e?.message ||
"Unknown oEmbed error",
};
}
})
);
return { results };
},
{
// cache 6h
maxAge: 60 * 60 * 6,
// clé de cache basée sur le payload (urls)
getKey: async (event) => {
const body = await readBody(event).catch(() => null);
const urls = Array.isArray(body?.urls) ? body.urls.join("|") : "none";
return `instagram:oembed:${urls}`;
},
}
);