dev du parc

This commit is contained in:
2026-04-21 18:39:01 +02:00
parent 9e421b1d08
commit 31940e48ba
17 changed files with 1124 additions and 17 deletions

View File

@@ -0,0 +1,756 @@
<template>
<div class="parc-page">
<!-- ================== -->
<!-- FILS D'ARIANE -->
<!-- ================== -->
<PageSection tone="" content-size="default" class="breadcrum_wp">
<Breadcrumb/>
</PageSection>
<!-- ================== -->
<!-- EN-TêTE -->
<!-- ================== -->
<section class="fiche_header_simple_wp">
<div class="fiche_header_wp_gauche"></div>
<div class="fiche_header_inner">
<div class="fiche_header_titres">
<div>
<DsHeading as="h1" tone="default" textcase="uppercase" class="concert-card__title">
{{ parc_contenus?.header_titre }}
</DsHeading>
</div>
<DsText as="p" align="justify">
{{ parc_contenus?.header_text }}
</DsText>
</div>
<div class="fiche_header_img">
<DsMedia
v-if="parc_contenus?.image_illustration_header?.url"
:src="parc_contenus.image_illustration_header.url"
:alt="parc_contenus.image_illustration_header.alternativeText || ''"
fit="contain"
ratio="square"
/>
<div v-else class="img_placeholder" aria-hidden="true" />
</div>
</div>
<div class="fiche_header_wp_droite"></div>
</section>
<!-- ================== -->
<!-- DESCRIPTION -->
<!-- ================== -->
<PageSection tone="" content-size="default" padded_size="md" class="fiche_description">
<SectionContent v-if="parc_contenus?.description_generale" class="description_wp">
<StrapiBlocksConvert :blocks="parc_contenus.description_generale" />
</SectionContent>
</PageSection>
<!-- ================== -->
<!-- LES INSTRUMENTS -->
<!-- ================== -->
<PageSection :content="false" padded_size="md">
<Decalage
tone="dark"
title-tone="invert"
position="left"
button-tone="invert"
ensavoirplus-target="texte_cache_1"
ensavoirplus-group="parc-details"
>
<template #title>
LES INSTRUMENTS
</template>
<DsText as="p" tone="invert" align="justify">
Découvrer les 3 000 instruments à la location
</DsText>
</Decalage>
</PageSection>
<PageSection
id="texte_cache_1"
data-ensavoirplus-group="parc-details"
tone=""
content-size="default"
padded_size=""
class="decalage_ensavoirplus--hidden"
>
<div>
<!-- PDF -->
<a
v-if="parc_contenus?.documents?.[0]?.url"
:href="parc_contenus.documents[0].url"
:download="parc_contenus.documents[0].name || true"
class="flex items-center gap-2 px-6 py-2 max-w-[334px] bg-primary-container text-on-primary-container rounded-full font-bold text-sm hover:bg-primary-fixed"
target="_blank"
rel="noopener noreferrer"
>
<span class="material-symbols-outlined text-lg">picture_as_pdf</span>
CATALOGUE DU PARC
<span v-if="parc_contenus.documents[0].size">
(PDF • {{ formatKoToMo(parc_contenus.documents[0].size) }})
</span>
</a>
</div>
<!-- PAR CATEGORIES -->
<section class="space-y-12 mt-16">
<div
v-for="categorie in instruments_categories"
:key="categorie.id"
class="space-y-4"
>
<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] flex-1 bg-surface-container"></div>
</div>
<div class="space-y-4">
<div
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="border-surface-container pl-8 grid grid-cols-2 md:grid-cols-5 gap-6 items-start">
<div
v-for="instrument in categorie.instruments_list || []"
:key="instrument.id"
class="flex flex-col gap-3 group"
>
<div class="w-full aspect-[1/1] bg-surface-container-low rounded-lg flex items-center justify-center group-hover:bg-primary-container transition-colors">
<img
v-if="instrument.illustration_instrument?.url"
:src="instrument.illustration_instrument.url"
:alt="instrument.illustration_instrument.alternativeText || instrument.illustration_instrument.caption || instrument.illustration_instrument.name || ''"
class="w-full aspect-[1/1] object-contain rounded-lg"
>
</div>
<div class="text-center">
<p v-if="instrument.instrument_nom" class="text-xs font-bold uppercase tracking-tighter">
{{ instrument.instrument_nom }}
</p>
</div>
<button
type="button"
class="px-3 py-1.5 bg-surface-container-low text-on-surface-variant rounded-full text-sm font-medium hover:bg-primary-container hover:text-primary transition-colors"
@click="toggleInstrumentDescription(instrument.id)"
>
En savoir plus
</button>
<div v-if="openedInstrumentDescriptions[instrument.id]" class="text-center">
<StrapiBlocksConvert
v-if="instrument.instrument_description"
:blocks="instrument.instrument_description"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</PageSection>
<!-- ================== -->
<!-- DEVIS -->
<!-- ================== -->
<PageSection :content="false">
<Decalage
tone="brand"
title-tone="invert"
position="right"
button-tone="invert"
ensavoirplus-target="texte_cache_3"
ensavoirplus-group="parc-details"
>
<template #title>
Devis
</template>
<DsText as="p" tone="invert" align="justify">
Formulaire de demande de devis ou d'information sur la location de matériel ou sur l'organisation d'un événement en salle d'activités
</DsText>
</Decalage>
</PageSection>
<PageSection
id="texte_cache_3"
data-ensavoirplus-group="parc-details"
tone=""
content-size="default"
padded_size=""
class="mt-16 mb-16 decalage_ensavoirplus--hidden"
>
<div class="fiche_description">
<div class="w-full max-w-3xl rounded-2xl border border-outline-variant/20 bg-surface-container-lowest p-8 shadow-sm">
<div class="mb-8">
<h3 class="text-2xl font-bold tracking-tight">Faire une demande</h3>
<p class="mt-2 text-sm text-on-surface-variant">
Remplissez ce formulaire pour une demande de devis ou d'information. Les champs marqués dun astérisque sont obligatoires.
</p>
</div>
<form class="space-y-6" @submit.prevent="submitQuoteRequest">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label for="quote-request-type" class="block text-sm font-medium text-on-surface">
Type de demande *
</label>
<select
id="quote-request-type"
v-model="quoteForm.requestType"
class="w-full rounded-xl border border-outline-variant/30 bg-white px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary"
>
<option value="">Choisir un type</option>
<option value="location-materiel">Location de matériel</option>
<option value="organisation-salle-activites">Organisation dun événement en salle dactivités</option>
</select>
<p v-if="quoteFormErrors.requestType" class="text-xs text-error">{{ quoteFormErrors.requestType }}</p>
</div>
<div class="space-y-2">
<label for="quote-name" class="block text-sm font-medium text-on-surface">
Nom complet *
</label>
<input
id="quote-name"
v-model.trim="quoteForm.name"
type="text"
class="w-full rounded-xl border border-outline-variant/30 bg-white px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary"
placeholder="Votre nom et prénom"
>
<p v-if="quoteFormErrors.name" class="text-xs text-error">{{ quoteFormErrors.name }}</p>
</div>
<div class="space-y-2">
<label for="quote-email" class="block text-sm font-medium text-on-surface">
Email *
</label>
<input
id="quote-email"
v-model.trim="quoteForm.email"
type="email"
class="w-full rounded-xl border border-outline-variant/30 bg-white px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary"
placeholder="nom@exemple.fr"
>
<p v-if="quoteFormErrors.email" class="text-xs text-error">{{ quoteFormErrors.email }}</p>
</div>
<div class="space-y-2">
<label for="quote-phone" class="block text-sm font-medium text-on-surface">
Téléphone *
</label>
<input
id="quote-phone"
v-model.trim="quoteForm.phone"
type="tel"
class="w-full rounded-xl border border-outline-variant/30 bg-white px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary"
placeholder="06 00 00 00 00"
>
<p v-if="quoteFormErrors.phone" class="text-xs text-error">{{ quoteFormErrors.phone }}</p>
</div>
<div class="space-y-2">
<label for="quote-organisation" class="block text-sm font-medium text-on-surface">
Structure / organisme *
</label>
<input
id="quote-organisation"
v-model.trim="quoteForm.organization"
type="text"
class="w-full rounded-xl border border-outline-variant/30 bg-white px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary"
placeholder="Nom de votre structure"
>
<p v-if="quoteFormErrors.organization" class="text-xs text-error">{{ quoteFormErrors.organization }}</p>
</div>
</div>
<div class="space-y-2">
<label for="quote-message" class="block text-sm font-medium text-on-surface">
Message *
</label>
<textarea
id="quote-message"
v-model.trim="quoteForm.message"
rows="6"
class="w-full rounded-xl border border-outline-variant/30 bg-white px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary"
placeholder="Précisez votre besoin, les dates, le matériel ou lévénement concerné."
></textarea>
<p v-if="quoteFormErrors.message" class="text-xs text-error">{{ quoteFormErrors.message }}</p>
</div>
<div v-if="quoteSubmitError" class="rounded-xl border border-error/20 bg-error-container/20 px-4 py-3 text-sm text-error">
{{ quoteSubmitError }}
</div>
<div v-if="quoteSubmitSuccess" class="rounded-xl border border-primary/20 bg-primary-container/40 px-4 py-3 text-sm text-on-surface">
<span v-if="quoteEmailsSent">Votre demande a bien été envoyée. Un email de confirmation vous a été adressé.</span>
<span v-else>Votre demande a bien été enregistrée et transmise à léquipe du Parc.</span>
</div>
<div class="flex items-center gap-4">
<button
type="submit"
class="inline-flex items-center justify-center rounded-full bg-primary px-6 py-3 text-sm font-bold text-on-primary transition-colors hover:bg-primary-dim disabled:cursor-not-allowed disabled:opacity-60"
:disabled="quoteSubmitting"
>
{{ quoteSubmitting ? 'Envoi en cours...' : 'Envoyer la demande' }}
</button>
<p class="text-xs text-on-surface-variant">
En soumettant ce formulaire, jaccepte que les informations saisies soient utilisées dans le cadre de la demande de devis ou dinformation.
</p>
</div>
</form>
</div>
</div>
</PageSection>
<!-- ================== -->
<!-- INFOS PRATIQUES -->
<!-- ================== -->
<PageSection :content="false">
<Decalage tone="brandreverse" title-tone="invert" position="left" button-tone="invert" ensavoirplus-target="texte_cache_2"
ensavoirplus-group="parc-details">
<template #title>
Infos pratiques
</template>
<DsText tone="invert" as="p" align="justify">
Accès au parc instrumental de lOrchestre national dÎle-de-France
</DsText>
</Decalage>
</PageSection>
<!-- ================== -->
<!-- INFOS PRATIQUES -->
<!-- PARTIE CACHÉE -->
<!-- ================== -->
<div id="texte_cache_2" data-ensavoirplus-group="parc-details" class="decalage_ensavoirplus--hidden">
<PageSection tone="" content-size="default" padded_size="md">
<SectionContent class="fiche_description strapi-blocks">
<div class="">
<h1>Horaires</h1>
<p>De 9h30 à 12h et de 14h à 18h</p>
<p>Du lundi au vendredi</p>
<p><strong>Uniquement sur rendez-vous</strong></p>
</div>
</SectionContent>
</PageSection>
<PageSection tone="" content-size="default" padded_size="md">
<SectionContent class="fiche_description strapi-blocks">
<div>
<div class="mb-8"><iframe src="https://maps.google.com/maps?width=781&amp;height=538&amp;hl=en&amp;q=1,%20rue%20du%20Capitaine%20Alfred-Dreyfus%20Alfortvillle+(Parc%20instrumental)&amp;t=&amp;z=14&amp;ie=UTF8&amp;iwloc=B&amp;output=embed" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe></div>
<div>
<h1>Comment venir ?</h1>
<h2>Accès pour le transport dinstruments</h2>
<p>Depuis le 1, rue du Capitaine Alfred-Dreyfus, 94140 Alfortville</p>
<ul>
<li>Contourner le corps de bâtiment de droite en passant par la circulation du Centre technique municipal.</li>
<li>Stationner sur le parking du Parc instrumental.</li>
</ul>
<section class="img-gallery_wp">
<div class="img-gallery">
<div class="ds-media ds-media--">
<img class="ds-media__img ds-media__img--cover" src="/img/photos/acces1.jpg" alt="" loading="lazy" decoding="async">
</div>
</div>
</section>
<h2>Accès Évènement public</h2>
<p>Rendez-vous au 9, allée Jean-Baptiste Preux.</p>
<ul>
<li>Accès au parking par le grand portail coulissant.</li>
<li>Suivre le cheminement jusquà la salle dactivités.</li>
</ul>
<section class="img-gallery_wp">
<div class="img-gallery">
<div class="ds-media ds-media--">
<img class="ds-media__img ds-media__img--cover" src="/img/photos/acces2.jpg" alt="" loading="lazy" decoding="async">
</div>
</div>
</section>
</div>
</div>
</SectionContent>
</PageSection>
</div>
<!-- ================== -->
<!-- CONTACT -->
<!-- ================== -->
<PageSection tone="" content-size="default" padded_size="md" class="contact_spe_wp">
<ContactSpecifique
titre="Contacter les régisseurs"
nom="Stéphane Borsellino / Stéphane Nguyen Phu Khai"
poste=""
numero="01 88 15 00 80"
mail="leparcinstrumental@orchestre-ile.com"
adresse="1, rue du Capitaine Alfred-Dreyfus 94 140 Alfortvillle"
/>
</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>
</div>
</template>
<script setup>
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 logoparc from '/img/logos/logo_le_parc_noir.png'
import catalogue_parc from '/contenus/catalogue-du-parc-instrumental.pdf'
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 parc_strapi = ref({
data: {
id: 1,
documentId: "parc123456",
header_titre: "Un parc riche de plus de 3 000 instruments à la location",
header_text: "",
image_illustration_header: {
id: 301,
documentId: "img123456",
name: "parc_header.jpg",
alternativeText: "Vue du parc instrumental de l'Orchestre",
caption: "Parc instrumental",
width: 1600,
height: 900,
formats: {
thumbnail: {
url: "/"
}
},
ext: ".jpg",
mime: "image/jpeg",
size: 245.8,
url: logoparc
},
"description_generale": [
{
"type": "paragraph",
"children": [
{
"text": "Le Parc instrumental de lOrchestre, dont la mission principale est de faciliter les manifestations musicales en Île-de-France, dispose dun catalogue de plus de 3 000 instruments et contribue à plus de 500 manifestations par an (concerts, répétitions, cours, masterclasses...).",
"type": "text"
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "Il sadresse aux municipalités, communautés dagglomération, communautés de communes, conservatoires et écoles de musique, ensembles instrumentaux ou vocaux professionnels ou amateurs, organisateurs de concerts (théâtre, festival, association...), structures culturelles ainsi quaux particuliers.",
"type": "text"
}
]
},
{
"type": "paragraph",
"children": [
{
"text": "Situé dans un local industriel, sur 2 niveaux, il offre un espace total de 650m2 dont 400m2 dévolus au stockage des 3 000 instruments.",
"type": "text"
}
]
},
{
"type": "paragraph",
"children": [
{
"text": " Une salle polyvalente dédiée aux activités permet de recevoir des groupes pour de multiples actions culturelles.",
"type": "text"
}
]
}
],
"documents": [
{
"id": 246,
"documentId": "h38c6ppljk084pdgnngcbe0b",
"name": "catalogue-du-parc-instrumental",
"alternativeText": "catalogue-du-parc-instrumental",
"caption": "Catalogue du Parc",
"width": null,
"height": null,
"formats": null,
"hash": "catalogue-du-parc-instrumental",
"ext": ".pdf",
"mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"size": 2065.92,
"url": catalogue_parc,
"previewUrl": null,
"provider": "aws-s3",
"provider_metadata": null,
"createdAt": "2026-04-20T09:04:05.924Z",
"updatedAt": "2026-04-20T09:04:39.849Z",
"publishedAt": "2026-04-20T09:04:05.924Z",
"focalPoint": null
},
]
},
meta: {}
})
const parc_contenus = computed(() => parc_strapi.value?.data || null)
const endpointInstruments = "/api/__strapi__/instruments"
const populateInstruments = {
illustration_instrument: true,
}
const { items: instrumentsItems, refresh: refreshInstruments } = useStrapi(
endpointInstruments,
{
locale: "fr-FR",
populate: populateInstruments,
pageSize: 500,
}
)
onMounted(() => {
if (!instrumentsItems.value?.length) {
refreshInstruments()
}
})
const instruments_categories = computed(() => {
const groups = new Map()
for (const instrument of instrumentsItems.value || []) {
const categoryName = instrument.instrument_categorie || "Sans catégorie"
if (!groups.has(categoryName)) {
groups.set(categoryName, {
id: categoryName,
nom_categorie: categoryName,
instruments_list: [],
})
}
groups.get(categoryName).instruments_list.push(instrument)
}
return Array.from(groups.values())
})
const openedInstrumentDescriptions = ref({})
const quoteForm = reactive({
requestType: "",
name: "",
email: "",
phone: "",
organization: "",
message: "",
})
const quoteFormErrors = reactive({
requestType: "",
name: "",
email: "",
phone: "",
organization: "",
message: "",
})
const quoteSubmitting = ref(false)
const quoteSubmitSuccess = ref(false)
const quoteSubmitError = ref("")
const quoteEmailsSent = ref(false)
function toggleInstrumentDescription(instrumentId) {
openedInstrumentDescriptions.value[instrumentId] = !openedInstrumentDescriptions.value[instrumentId]
}
function resetQuoteFormErrors() {
quoteFormErrors.requestType = ""
quoteFormErrors.name = ""
quoteFormErrors.email = ""
quoteFormErrors.phone = ""
quoteFormErrors.organization = ""
quoteFormErrors.message = ""
}
function validateQuoteForm() {
resetQuoteFormErrors()
let isValid = true
if (!quoteForm.requestType) {
quoteFormErrors.requestType = "Le type de demande est obligatoire."
isValid = false
}
if (!quoteForm.name) {
quoteFormErrors.name = "Le nom est obligatoire."
isValid = false
}
if (!quoteForm.email) {
quoteFormErrors.email = "Lemail est obligatoire."
isValid = false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(quoteForm.email)) {
quoteFormErrors.email = "Lemail nest pas valide."
isValid = false
}
if (!quoteForm.phone) {
quoteFormErrors.phone = "Le téléphone est obligatoire."
isValid = false
}
if (!quoteForm.organization) {
quoteFormErrors.organization = "La structure est obligatoire."
isValid = false
}
if (!quoteForm.message) {
quoteFormErrors.message = "Le message est obligatoire."
isValid = false
}
return isValid
}
async function submitQuoteRequest() {
quoteSubmitSuccess.value = false
quoteSubmitError.value = ""
quoteEmailsSent.value = false
if (!validateQuoteForm()) {
return
}
quoteSubmitting.value = true
try {
const response = await $fetch("/api/parc-demandes", {
method: "POST",
body: {
requestType: quoteForm.requestType,
name: quoteForm.name,
email: quoteForm.email,
phone: quoteForm.phone,
organization: quoteForm.organization,
message: quoteForm.message,
},
})
quoteSubmitSuccess.value = true
quoteEmailsSent.value = Boolean(response?.emailsSent)
quoteForm.requestType = ""
quoteForm.name = ""
quoteForm.email = ""
quoteForm.phone = ""
quoteForm.organization = ""
quoteForm.message = ""
resetQuoteFormErrors()
} catch (error) {
quoteSubmitError.value = error?.data?.statusMessage || "Lenvoi de la demande a échoué."
} finally {
quoteSubmitting.value = false
}
}
function formatKoToMo(sizeInKo) {
if (typeof sizeInKo !== "number") return ""
return `${(sizeInKo / 1024).toFixed(1)} Mo`
}
</script>
<style lang="scss">
.breadcrum_wp {
padding-top: 30px;
}
// =======================
// SPÉCIFIQUE À CETTE PAGE
// =======================
.parc-page {
.fiche_description {
display: flex;
justify-content: center;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
> * {
max-width: 640px;
display: flex;
flex-direction: column;
}
}
.contact_spe_wp {
background-color: var(--c-background-jaune-clair);
margin-top: 50px;
margin-bottom: 20px;
}
}
</style>