avancées centre de ressource

This commit is contained in:
Valentin Le Moign 2025-03-26 00:28:41 +01:00
parent 9093caa557
commit a1916e3219
14 changed files with 670 additions and 30 deletions

View File

@ -3,9 +3,11 @@
import { defineStore } from 'pinia';
import REST from '../api/rest-axios';
import { useLayoutStore } from './layout';
import { findContentByPath } from '../utils/content/findContentByPath';
import { getCleanDate, fetchFromRelationships, getRelatedEtape } from '../utils/content/contentFetchUtils';
import { getCarteSensible, getTitreTexte, getChiffresCles, getDiaporama, getEntretien, getVideos } from '../utils/content/cleanParties';
import { getCarteSensible, getTitreTexte, getChiffresCles, getDiaporama, getEntretien, getVideos, getDocument, getGallerie } from '../utils/content/cleanParties';
import { getPartenaires, getGouvernance, getRessources } from '../utils/content/multiItemPages';
export const useContentStore = defineStore('content', {
@ -24,39 +26,44 @@ export const useContentStore = defineStore('content', {
const { contentType, rawContent } = await findContentByPath(contentTypes, path);
this.contentType = contentType;
if (this.contentType === 'etape' || this.contentType === 'static') {
if (
this.contentType === 'etape'
|| this.contentType === 'static'
|| this.contentType === 'ressourceItem'
) {
const vignettePromise = fetchFromRelationships('field_vignette', rawContent.relationships);
const partiesPromise = fetchFromRelationships('field_parties', rawContent.relationships);
const partiesPromise = fetchFromRelationships(this.contentType === 'ressourceItem' ? 'field_parties_ressource' : 'field_parties', rawContent.relationships);
let previousEtapePromise, nextEtapePromise;
if (contentType === 'etape') {
// related étapes
if (this.contentType === 'etape') {
previousEtapePromise = getRelatedEtape('previous', path);
nextEtapePromise = getRelatedEtape('next', path);
// coordinates
this.content.coordinates = {
lat: rawContent.attributes.field_geofield.lat,
lon: rawContent.attributes.field_geofield.lon,
};
// adresse
this.content.adresse = rawContent.attributes.field_adresse;
// étape number
this.content.etape_number = rawContent.attributes.field_arret_numero;
// couleur
this.content.couleur = rawContent.attributes.field_couleur;
// dates
this.content.dates = {
start: getCleanDate(rawContent.attributes.field_dates.value),
end: getCleanDate(rawContent.attributes.field_dates.end_value),
}
}
if (this.contentType === 'ressourceItem') {
this.content.ressourceType = rawContent.attributes.field_type_de_ressource;
this.content.auteurice = rawContent.attributes.field_autheurice;
this.content.date = getCleanDate(rawContent.attributes.field_date_ressource);
this.content.introduction = rawContent.attributes.field_introduction?.processed;
useLayoutStore().hideEtapeList(true);
}
// pageTitle
this.pageTitle = rawContent.attributes.metatag.find(tag => tag.tag === "meta")?.attributes.content;
// contentTitle
this.content.contentTitle = rawContent.attributes.title;
const [vignetteData, partiesData] = await Promise.all([vignettePromise, partiesPromise]);
@ -76,7 +83,7 @@ export const useContentStore = defineStore('content', {
if (partiesData) {
const partiesPromises = partiesData.map(async (partie) => {
const partieType = partie.type.replace(/^paragraph--/, "");
let partieContent = { type: partieType };
let partieContent = { type: partieType };
switch (partieType) {
case 'carte_sensible':
@ -102,10 +109,41 @@ export const useContentStore = defineStore('content', {
case 'video':
partieContent.videos = getVideos(partie);
break;
case 'document':
partieContent.document = await getDocument(partie);
break;
case 'galleries':
partieContent.gallerie = await getGallerie(partie);
break;
}
return partieContent;
});
// liens
if (rawContent.attributes.field_liens?.length) {
this.content.liens = [];
for (let lien of rawContent.attributes.field_liens) {
this.content.liens.push({
title: lien.title,
url: lien.uri,
});
}
}
// pièces jointes
if (rawContent.relationships.field_pieces_jointes?.data.length) {
this.content.pieces_jointes = [];
for (let pieceJointe of rawContent.relationships.field_pieces_jointes.data) {
if (pieceJointe.meta.display) {
const uuid = pieceJointe.id;
const response = await REST.get(`/jsonapi/file/file/${uuid}`);
this.content.pieces_jointes.push({
title: pieceJointe.meta.description,
url: response.data.data.attributes.uri.url,
});
}
}
}
this.content.parties = await Promise.all(partiesPromises);
}
@ -132,6 +170,8 @@ export const useContentStore = defineStore('content', {
switch (this.contentType) {
case 'ressource':
multiItemPageArray = await getRessources(rawContent);
this.content.ressourceTypes = new Set(multiItemPageArray.map(item => item.ressourceType));
useLayoutStore().hideEtapeList(true);
break;
case 'partenaire':
multiItemPageArray = await getPartenaires(rawContent);
@ -142,7 +182,6 @@ export const useContentStore = defineStore('content', {
}
this.content[`${this.contentType}s`] = multiItemPageArray;
console.log(this.content);
}
} catch (error) {
this.error = 'Failed to fetch data';
@ -157,6 +196,7 @@ export const useContentStore = defineStore('content', {
this.content = {};
this.loading = !forFrontDisplay;
this.error = null;
useLayoutStore().hideEtapeList(false);
}
},
});

View File

@ -46,6 +46,23 @@ export const useLayoutStore = defineStore('layout', {
this.toggleEtapeListScroll(isIntersecting, listeEtape, column, headerRect.height, animationToggleRect.top);
},
hideEtapeList(souldListHide) {
const etapeList = document.querySelector('#etapes-liste');
const listContainer = etapeList.parentNode;
if (souldListHide) {
listContainer.style.minWidth = '30vw';
etapeList.style.opacity = '0';
setTimeout(() => {
etapeList.style.display = 'none';
}, 300);
} else {
listContainer.style.minWidth = 'unset';
etapeList.style.display = 'block';
setTimeout(() => {
etapeList.style.opacity = '1';
}, 10);
}
},
toggleEtapeListScroll(isIntersecting, listeEtape, column, headerHeight, animationToggleTop) {
if (isIntersecting && !this.isEtapeListeScrollable
|| !isIntersecting && this.isEtapeListeScrollable) {

View File

@ -1,5 +1,5 @@
import REST from '../../api/rest-axios';
import { fetchFromRelationships } from './contentFetchUtils';
import { fetchFromRelationships, getCleanDate } from './contentFetchUtils';
export async function getCarteSensible(partie) {
@ -131,4 +131,46 @@ export function getVideos(partie) {
videos.push(videoUrl);
}
return videos;
}
export async function getDocument(partie) {
// const documentFetch = await fetchFromRelationships('field_document', partie.relationships);
const uuid = partie.relationships.field_document.data.id;
const documentFetch = await REST.get(`/jsonapi/file/file/${uuid}`);
const url = documentFetch.data.data.attributes.uri.url;
const titre = partie.attributes.field_titre;
const sousTitre = partie.attributes.field_sous_titre;
const date = partie.attributes.field_date ? getCleanDate(partie.attributes.field_date) : null;
const auteurice = partie.relationships.field_autheurice_s;
const description = partie.relationships.field_document.data.meta.description;
const vignetteFetch = await REST.get(`/jsonapi/file/file/${partie.relationships.field_vignette.data.id}`);
const vignette = { url: vignetteFetch.data.data.attributes.image_style_uri.content_small, alt: partie.relationships.field_vignette.data.meta.alt };
return { url, titre, sousTitre, date, auteurice, description, vignette };
}
export async function getGallerie(partie) {
const gallerieFetch = await fetchFromRelationships('field_gallerie', partie.relationships);
if (gallerieFetch) {
const titre = gallerieFetch.attributes.title;
const introduction = gallerieFetch.attributes.body?.processed;
const imagesFetch = await fetchFromRelationships('field_images', gallerieFetch.relationships);
let images = [];
imagesFetch.forEach((image, index) => {
images.push({
url: {
original: image.attributes.uri.url,
small: image.attributes.image_style_uri.content_small,
medium: image.attributes.image_style_uri.content_medium,
large: image.attributes.image_style_uri.content_large,
},
alt: gallerieFetch.relationships.field_images.data[index].meta.alt,
});
});
return { titre, introduction, images };
}
}

View File

@ -3,7 +3,7 @@ import REST from '../../api/rest-axios';
export async function fetchFromRelationships(field, relationships) {
field = relationships[field] ? field : `${field}_static`;
if (relationships[field].links) {
if (relationships[field]?.links) {
const contentLink = relationships[field].links.related.href;
return REST.get(contentLink)
.then(contentFetch => contentFetch.data.data)

View File

@ -10,9 +10,9 @@ export async function findContentByPath(contentTypes, path) {
)
);
if (content) {
if (content) {
return {
contentType: type,
contentType: content.type === 'node--ressource' ? 'ressourceItem' : type,
rawContent: content,
};
}

View File

@ -10,6 +10,10 @@
:content="content"
:couleur="content.couleur || brandColor" />
<main>
<RessourceItemHeader
v-if="contentType === 'ressourceItem'"
:content="content"
:couleur="brandColor" />
<div v-for="partie in content.parties" class="partie">
<ModaleCarteSensible
v-if="partie.type === 'carte_sensible'"
@ -37,6 +41,14 @@
<ModaleVideos
v-if="partie.type === 'video'"
:partie="partie" />
<ModaleGallerie
v-if="partie.type === 'galleries'"
:partie="partie"
:couleur="content.couleur || brandColor" />
<ModaleDocument
v-if="partie.type === 'document'"
:partie="partie"
:couleur="content.couleur || brandColor" />
</div>
<EquipeContent
v-if="contentType === 'gouvernance'"
@ -46,9 +58,9 @@
v-if="contentType === 'partenaire'"
:content="content" />
<CentreDeRessource
v-if="contentType === 'ressource'"
:content="content"
:couleur="brandColor" />
v-if="contentType === 'ressource'"
:content="content"
:couleur="brandColor" />
</main>
<PiecesJointes
:content="content"
@ -74,6 +86,7 @@ import ModaleFooter from './components/ModaleFooter.vue';
import EquipeContent from './components/EquipeContent.vue';
import PartenairesContent from './components/PartenairesContent.vue';
import CentreDeRessource from './components/CentreDeRessource.vue';
import RessourceItemHeader from './components/RessourceItemHeader.vue';
import PiecesJointes from './components/PiecesJointes.vue';
import ModaleCarteSensible from './components/parties/ModaleCarteSensible.vue';
@ -83,6 +96,8 @@ import ModaleDiaporama from './components/parties/ModaleDiaporama.vue';
import ModaleEntretien from './components/parties/ModaleEntretien.vue';
import ModaleExergue from './components/parties/ModaleExergue.vue';
import ModaleVideos from './components/parties/ModaleVideos.vue';
import ModaleGallerie from './components/parties/ModaleGallerie.vue';
import ModaleDocument from './components/parties/ModaleDocument.vue';
const store = useContentStore();
const mapState = useMapStore();

View File

@ -1,14 +1,153 @@
<template>
<div id="centre-de-ressource">
<div v-if="content.intro" class="intro">
<p v-html="content.intro"></p>
<!-- <p v-html="content.intro"></p> -->
</div>
<div class="filters">
<input type="text" v-model="searchQuery" placeholder="Rechercher..." class="search-bar">
<select v-model="selectedType">
<option value="">Tous les types</option>
<option v-for="type in content.ressourceTypes" :key="type" :value="type">
{{ type.replace(/_/g, ' ').replace(/^\w/, char => char.toUpperCase()) }}
</option>
</select>
</div>
<template v-for="type in filteredTypes" :key="type">
<div class="type-section" v-if="ressourcesToDisplay[type] && ressourcesToDisplay[type].length > 0">
<h3>{{ type.replace(/_/g, ' ').replace(/^\w/, char => char.toUpperCase()) }}</h3>
<div class="ressource-list">
<TransitionGroup name="itemFade" tag="div" appear>
<div class="ressource-item"
:data-href="ressource.url"
v-for="(ressource, ressourceIndex) in ressourcesToDisplay[type]"
:key="`${type}-${ressourceIndex}`"
:style="{ '--index': ressourceIndex - visibleItemsPerSection }">
<figure>
<img :src="ressource.vignette.url" :alt="ressource.vignette.alt" />
</figure>
<div>
<h4>{{ ressource.title }}</h4>
<p>Le {{ ressource.date.d }} {{ ressource.date.m }} {{ ressource.date.y }}</p>
<p>Par {{ ressource.auteurice }}</p>
</div>
</div>
</TransitionGroup>
</div>
<div class="button-container">
<div v-if="ressourcesByType(type).length > visibleItemsPerSection
&& ressourcesToDisplay[type].length === visibleItemsPerSection"
class="ressource-button"
@click="loadMore(type)">
Voir plus
</div>
<div v-if="ressourcesToDisplay[type].length > visibleItemsPerSection"
class="ressource-button"
@click="showLess(type)">
Voir moins
</div>
</div>
</div>
</template>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import router from '../../router/router';
import { useContentStore } from '../../stores/content';
import { useMapStore } from '../../stores/map';
import { handleClickableElements } from '../../utils/handle-navigation.js';
const store = useContentStore();
const mapStore = useMapStore();
const siteName = document.querySelector('#site_name').innerText;
const props = defineProps({
content: Object,
couleur: String,
});
</script>
const searchQuery = ref('');
const selectedType = ref('');
const ressourcesToDisplay = ref({});
const visibleItemsPerSection = 4;
const filteredTypes = computed(() => {
return selectedType.value ? [selectedType.value] : props.content.ressourceTypes;
});
const ressourcesByType = (type) => {
return props.content.ressources
.filter(ressource => ressource.ressourceType === type)
.filter(ressource =>
searchQuery.value === '' ||
ressource.title.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
const initializeRessources = (type) => {
ressourcesToDisplay.value[type] = ressourcesByType(type).slice(0, visibleItemsPerSection);
};
props.content.ressourceTypes.forEach(type => initializeRessources(type));
const loadMore = (type) => {
const currentLength = ressourcesToDisplay.value[type].length;
if (currentLength < ressourcesByType(type).length) {
ressourcesToDisplay.value[type] = ressourcesByType(type).slice(0, currentLength + visibleItemsPerSection);
}
};
const showLess = (type) => {
ressourcesToDisplay.value[type] = ressourcesByType(type).slice(0, visibleItemsPerSection);
};
watch(searchQuery, () => {
props.content.ressourceTypes.forEach(type => {
initializeRessources(type);
});
});
let relatedItemCards, baseUrl;
watch(ressourcesToDisplay.value, () => {
setClickableElements();
}, { deep: true });
watch(selectedType, () => {
setClickableElements();
});
onMounted(() => {
baseUrl = window.location.protocol + "//" + window.location.host;
setClickableElements();
});
function setClickableElements() {
setTimeout(() => {
relatedItemCards = document.querySelectorAll('.ressource-item');
handleClickableElements(relatedItemCards, store, router, baseUrl, siteName, mapStore);
}, 50);
}
</script>
<style scoped>
.itemFade-enter-active, .itemFade-leave-active {
transition: all 0.3s ease !important;
transition-delay: calc(0.1s * var(--index)) !important;
}
.itemFade-enter-from, .itemFade-leave-to {
opacity: 0;
transform: translateY(-20px) !important;
}
.itemFade-enter-to, .itemFade-leave-from {
opacity: 1;
transform: translateY(0px);
}
</style>

View File

@ -14,7 +14,7 @@
<div class="locality">
<div class="top-triangle" v-if="contentType === 'etape'"></div>
<div class="locality-title">
<h1>{{content.contentTitle}} <em v-if="content.adresse">({{ content.adresse.postal_code.slice(0, 2) }})</em></h1>
<h1>{{ contentType === 'ressourceItem' ? 'Centre de ressources' : content.contentTitle }} <em v-if="content.adresse">({{ content.adresse.postal_code.slice(0, 2) }})</em></h1>
</div>
</div>
</div>

View File

@ -0,0 +1,60 @@
<template>
<header id="ressource-item-header">
<div class="retour">
<p data-href="/ressources"> Retour au centre de ressources</p>
</div>
<div class="type">{{ content.displayedType }}</div>
<div class="title">
<h2
:style="{ background: `linear-gradient(transparent 70%, ${couleur} 70%)` }">
{{ content.contentTitle }}
</h2>
</div>
<div class="meta">Par {{ content.auteurice }}, le {{ content.date.d }} {{ content.date.m }} {{ content.date.y }}</div>
</header>
</template>
<script setup>
import { onMounted } from 'vue';
import router from '../../router/router';
import { useContentStore } from '../../stores/content';
import { useMapStore } from '../../stores/map';
import { handleClickableElements } from '../../utils/handle-navigation.js';
const store = useContentStore();
const mapStore = useMapStore();
const siteName = document.querySelector('#site_name').innerText;
const props = defineProps({
content: Object,
couleur: String,
});
function setDisplayedType() {
const ressourceType = props.content.ressourceType;
switch (ressourceType) {
case 'cartes_blanches':
props.content.displayedType = 'Carte blanche';
break;
case 'documents':
props.content.displayedType = 'Document';
break;
case 'galleries':
props.content.displayedType = 'Galerie';
break;
case 'videos':
props.content.displayedType = 'Vidéo';
break;
}
}
onMounted(() => {
const backToRessourcesLink = document.querySelectorAll('.retour > p');
const baseUrl = window.location.protocol + "//" + window.location.host;
handleClickableElements(backToRessourcesLink, store, router, baseUrl, siteName, mapStore);
setDisplayedType();
});
</script>

View File

@ -59,7 +59,7 @@ const handleImageClick = (event) => {
const swiperEl = img.closest('swiper-container');
swiperEl.querySelectorAll('swiper-slide').forEach((slide) => {
const img = slide.querySelector('img');
const altText = slide.querySelector('figcaption')?.textContent || '';
const altText = slide.querySelector('figcaption')?.textContent || '';
Object.values(props.partie.diaporama).forEach((image) => {
if (image.url.medium === img.src) {
swiperMedia.push({ src: image.url.large, alt: altText });

View File

@ -0,0 +1,88 @@
<template>
<div class="document"
:style="{ '--couleur': couleur }">
<div
class="intro"
v-html="partie.document.description">
</div>
<div class="document-grid">
<figure>
<a :href="partie.document.url" target="_blank">
<img :src="partie.document.vignette.url" :alt="partie.document.vignette.alt" />
</a>
</figure>
<div class="infos">
<p>{{ partie.document.titre }}</p>
<p>{{ partie.document.sousTitre }}</p>
<p>{{ partie.document.auteurice }}</p>
<p>{{ partie.document.date?.m }} {{ partie.document.date?.y }}</p>
</div>
<div class="download">
<a :href="partie.document.url" target="_blank">Télécharger le document</a>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
partie: Object,
couleur: String,
});
</script>
<style lang="scss" scoped>
.intro {
position: relative;
padding-left: 1.8rem;
margin: 3rem 0;
&::before {
content: '';
display: block;
position: absolute;
background-color: var(--couleur);
width: 0.8rem;
height: 100%;
margin-right: 1rem;
left: 0;
}
}
.document-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
column-gap: 2rem;
> figure {
grid-column: 1 / 2;
grid-row: 1 / 3;
margin: 0;
background-color: var(--couleur);
img {
padding: 2rem;
box-sizing: border-box;
width: 100%;
height: auto;
object-fit: cover;
transform: scale(1);
transition: transform 0.2s ease-in-out;
&:hover {
transform: scale(1.02);
}
}
}
> .infos {
> p {
margin: 0;
margin-bottom: 0.8rem;
&:first-of-type {
font-weight: bold;
}
}
}
> .download {
align-self: end;
justify-self: end;
}
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="gallerie">
<h3><p :style="{ background: `linear-gradient(transparent 70%, ${couleur} 70%)` }">{{ partie.gallerie.titre }}</p></h3>
<div class="intro" v-html="partie.gallerie.introduction"></div>
<div class="images-grid">
<figure v-for="image in partie.gallerie.images">
<img :src="image.url.medium" :alt="image.alt" @click="handleImageClick">
<figcaption class="caption">{{ image.alt }}</figcaption>
</figure>
</div>
</div>
<ImageModale
:isOpen="isModaleOpen"
:image="currentImage"
:swiperContent="swiperPopupContent"
@close="closeImageModale" />
</template>
<script setup>
import { onMounted } from 'vue';
import { useImageModal } from '../../composables/useImageModale';
import ImageModale from '../ImageModale.vue';
const props = defineProps({
partie: Object,
couleur: String,
});
const {
isModaleOpen,
currentImage,
swiperPopupContent,
openImageModale,
closeImageModale
} = useImageModal();
const handleImageClick = (event) => {
const img = event.target;
if (img.tagName === 'IMG') {
let swiperMedia = [];
const imgGrid = img.closest('.images-grid');
imgGrid.querySelectorAll('figure').forEach((figure) => {
const img = figure.querySelector('img');
const altText = figure.querySelector('figcaption')?.textContent || '';
Object.values(props.partie.gallerie.images).forEach((image) => {
if (img.src === image.url.medium) {
swiperMedia.push({ src: image.url.large, alt: altText });
}
});
});
console.log(swiperMedia);
openImageModale(img.src, img.alt, swiperMedia);
}
}
onMounted(() => {
setVerticalImages();
});
function setVerticalImages() {
const images = document.querySelectorAll('.images-grid figure img');
images.forEach((img) => {
if (img.naturalHeight > img.naturalWidth) {
img.classList.add('vertical');
img.closest('figure').querySelector('figcaption').style.textAlign = 'center';
}
});
}
</script>

View File

@ -9,7 +9,7 @@ export function useImageModal() {
const hamburger = document.querySelector('#hamburger');
const menu = document.querySelector('#menu');
const openImageModale = (src, alt, swiperMedia) => {
const openImageModale = (src, alt, swiperMedia) => {
currentImage.value = { src, alt };
swiperPopupContent.value = swiperMedia || [];
isModaleOpen.value = true;

View File

@ -773,7 +773,8 @@ body{
font-size: $labeur-font-size-desktop;
width: $modale-width-desktop;
}
&:has(#centre-de-ressource) {
&:has(#centre-de-ressource),
&:has(#ressource-item-header) {
@media screen and (min-width: $tablet-min-width) {
left: 8vw;
width: 84vw;
@ -927,6 +928,9 @@ body{
padding: 0 $modale-x-padding;
padding-bottom: 5vh;
box-sizing: border-box;
&:has(#ressource-item-header) {
padding-right: 50%;
}
> .partie,
> #equipe {
width: 100%;
@ -954,7 +958,8 @@ body{
}
.partie-title,
> .chiffres-cles,
> .entretien {
> .entretien,
> .gallerie {
> h3 {
position: relative;
display: inline-block;
@ -1091,6 +1096,54 @@ body{
> .videos iframe {
margin: 2rem 0;
}
> .gallerie {
> .intro {
margin-bottom: 2rem;
}
> .images-grid {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 5rem;
row-gap: 2rem;
}
figure {
margin: 0;
> img {
cursor: pointer;
transform: scale(1);
transition: transform 0.2s ease-in-out;
&:hover {
transform: scale(1.02);
}
&.vertical {
box-sizing: border-box;
padding: 0 25%;
}
}
}
}
> .document {
.download {
> a {
color: $main-color;
text-decoration: none;
font-size: $sm-font-size-mobile;
background-color: $brand-color;
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 1rem;
display: inline-block;
transform: scale(1);
transition: transform 0.2s ease-in-out;
&:hover {
transform: scale(1.02);
}
@media screen and (min-width: $desktop-min-width) {
font-size: $sm-font-size-desktop;
}
}
}
}
}
.caption {
font-size: $sm-font-size-mobile;
@ -1174,7 +1227,120 @@ body{
}
}
#centre-de-ressource {
background-color: red;
> .intro {
}
> .type-section {
> h3 {
display: inline-block;
font-size: $l-font-size-mobile;
font-family: 'Joost', sans-serif;
margin: 0;
margin-bottom: 2rem;
z-index: 1;
position: relative;
padding: 0 0.5rem;
background: linear-gradient(transparent 70%, $brand-color 70%);
@media screen and (min-width: $desktop-min-width) {
font-size: $l-font-size-desktop;
}
}
> .ressource-list > div {
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: start;
gap: 2rem;
margin-bottom: 2.5rem;
> .ressource-item {
display: flex;
gap: 1.5rem;
cursor: pointer;
transform: scale(1);
transition: transform 0.2s ease-in-out;
&:hover {
transform: scale(1.05);
}
> figure {
width: 40%;
margin: 0;
}
> div {
width: 50%;
> h4 {
font-size: $m-font-size-mobile;
font-family: 'Joost', sans-serif;
margin: 0;
margin-bottom: 0.5rem;
@media screen and (min-width: $desktop-min-width) {
font-size: $m-font-size-desktop;
}
}
> p {
margin: 0;
font-size: $sm-font-size-mobile;
@media screen and (min-width: $desktop-min-width) {
font-size: $sm-font-size-desktop;
}
}
}
}
}
> .button-container {
display: flex;
justify-content: center;
> .ressource-button {
display: inline;
font-size: $sm-font-size-mobile;
background-color: $brand-color;
padding: 0.5rem 1.5rem;
border-radius: 1rem;
cursor: pointer;
transition: transform 0.2s ease-in-out;
transform: scale(1);
&:hover {
transform: scale(0.95);
}
@media screen and (min-width: $desktop-min-width) {
font-size: $sm-font-size-desktop;
}
}
}
}
}
> #ressource-item-header {
> .retour {
margin-bottom: 1.5rem;
> p {
display: inline-block;
cursor: pointer;
font-size: $sm-font-size-mobile;
@media screen and (min-width: $desktop-min-width) {
font-size: $sm-font-size-desktop;
}
}
}
> .type,
> .meta {
text-align: center;
font-size: $m-font-size-mobile;
font-family: 'Joost', sans-serif;
@media screen and (min-width: $desktop-min-width) {
font-size: $m-font-size-desktop;
}
}
> .title {
width: 100%;
text-align: center;
> h2 {
display: inline-block;
margin-top: 1rem;
font-size: $xl-font-size-mobile;
font-family: 'Joost', sans-serif;
@media screen and (min-width: $desktop-min-width) {
font-size: $xl-font-size-desktop;
}
}
}
}
}
> .pieces-jointes {