rerefactor du fetching de contenus une mielleure ux au load des modales (description dans le readme)

This commit is contained in:
Valentin Le Moign 2025-06-26 15:26:18 +02:00
parent e85851bd4d
commit f44fbd8d06
21 changed files with 465 additions and 367 deletions

View File

@ -1,3 +1,8 @@
## Routing flow
![routing flow 1](https://nextcloud.val.kiwi/s/rCJPJgEPrkSNJAC)
![routing flow 2](https://nextcloud.val.kiwi/s/CpebG9MZAaKCwe3)
<img alt="Drupal Logo" src="https://www.drupal.org/files/Wordmark_blue_RGB.png" height="60px"> <img alt="Drupal Logo" src="https://www.drupal.org/files/Wordmark_blue_RGB.png" height="60px">
Drupal is an open source content management platform supporting a variety of Drupal is an open source content management platform supporting a variety of

View File

@ -1,6 +1,6 @@
import { initVueContentModale } from './utils/vue-setup'; import { initVueContentModale } from './utils/vue-setup';
import { processClickableElements } from './utils/process-clickable-elements'; import { processClickableElements } from './utils/process-clickable-elements';
import { handleReactiveness, setMenuToggle, setHamburgerWhenLogged } from './utils/layout-setup'; import { handleReactiveness, setMenuToggle, setRightSectionsWhenLogged } from './utils/layout-setup';
import { initFirstLoadRouting, handleClickableElements, handleBrowserNavigation } from './utils/handle-navigation'; import { initFirstLoadRouting, handleClickableElements, handleBrowserNavigation } from './utils/handle-navigation';
import { setupMapStore, preloadEtapesTiles } from './utils/map-setup'; import { setupMapStore, preloadEtapesTiles } from './utils/map-setup';
@ -13,16 +13,15 @@ import '../scss/main.scss'
(function ($, Drupal, drupalSettings) { (function ($, Drupal, drupalSettings) {
const CaravaneTheme = function () { const CaravaneTheme = function () {
function init () { function init () {
console.log('07/04 ci-cd fonctionne, import des configs ?')
// console.log('DrupalSettings', drupalSettings); // console.log('DrupalSettings', drupalSettings);
const baseUrl = window.location.protocol + "//" + window.location.host; const baseUrl = window.location.protocol + "//" + window.location.host;
const siteName = document.querySelector('#site_name').innerText; const siteName = document.querySelector('#site_name').innerText;
const { store, mapStore, router, route } = initVueContentModale(); const router = initVueContentModale();
handleReactiveness(); handleReactiveness();
setMenuToggle(); setMenuToggle();
setHamburgerWhenLogged(drupalSettings); setRightSectionsWhenLogged(drupalSettings);
// https://www.drupal.org/docs/extending-drupal/contributed-modules/contributed-module-documentation/leaflet/leaflet-api // https://www.drupal.org/docs/extending-drupal/contributed-modules/contributed-module-documentation/leaflet/leaflet-api
@ -44,22 +43,21 @@ import '../scss/main.scss'
} = processClickableElements(); } = processClickableElements();
const clickableElements = [...etapeListLinks, ...generalListLinks, logoLink, ...mapIcons, mapContainer]; const clickableElements = [...etapeListLinks, ...generalListLinks, logoLink, ...mapIcons, mapContainer];
setupMapStore(mapStore, map, settings); setupMapStore(map, settings);
// preloadEtapesTiles(mapStore, map); // preloadEtapesTiles(map);
initFirstLoadRouting(store, router, baseUrl, siteName); initFirstLoadRouting(router, baseUrl, siteName);
handleClickableElements(clickableElements, store, router, baseUrl, siteName, mapStore); handleClickableElements(clickableElements, router, baseUrl, siteName);
window.addEventListener("popstate", () => { window.addEventListener("popstate", () => {
handleBrowserNavigation(store, baseUrl, siteName, mapStore); handleBrowserNavigation(baseUrl, siteName);
}); });
}); });
} }
} }
} }
init() init()
} }
CaravaneTheme() CaravaneTheme()

View File

@ -1,201 +1,68 @@
// query et traitement des contenus // query et traitement des contenus
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import REST from '../api/rest-axios';
import { useLayoutStore } from './layout'; import { useLayoutStore } from './layout';
import { findContentByPath } from '../utils/content/findContentByPath'; import { findContentByPath } from '../utils/content/findContentByPath';
import { getCleanDate, fetchFromRelationships, getRelatedEtape, getRelatedRessources } from '../utils/content/contentFetchUtils'; import { fetchSingletonFullContent, fetchSingletonPartialContent } from '../utils/content/fetchSingleton';
import { getCarteSensible, getTitreTexte, getChiffresCles, getDiaporama, getEntretien, getVideos, getDocument, getGallerie } from '../utils/content/cleanParties'; import { fetchMultipleFullContent, fetchMultiplePartialContent } from '../utils/content/fetchMultiple';
import { getPartenaires, getGouvernance, getRessources } from '../utils/content/multiItemPages';
export const useContentStore = defineStore('content', { export const useContentStore = defineStore('content', {
state: () => ({ state: () => ({
contentType: '', contentType: '',
rawContent: {},
pageTitle: '', pageTitle: '',
content: {}, content: {},
partialLoading: false,
loading: false, loading: false,
error: null, error: null,
}), }),
actions: { actions: {
async fetchContentData(path) { // pages etape, statiques et ressource ont un seul item par page (singuliers)
// pages gouvernance (contact), ressources et partenaire ont plusieurs items par pages (multiples)
async fetchPartialContentData(path) {
this.resetStore(false); this.resetStore(false);
const contentTypes = ['etape', 'static', 'gouvernance', 'partenaire', 'ressource']; const contentTypes = ['etape', 'static', 'gouvernance', 'partenaire', 'ressource'];
try { try {
const { contentType, rawContent } = await findContentByPath(contentTypes, path); const { contentType, rawContent } = await findContentByPath(contentTypes, path);
this.contentType = contentType; this.contentType = contentType;
this.rawContent = rawContent;
if ( if (
this.contentType === 'etape' this.contentType === 'etape'
|| this.contentType === 'static' || this.contentType === 'static'
|| this.contentType === 'ressourceItem' || this.contentType === 'ressourceItem'
) { ) {
const vignettePromise = fetchFromRelationships('field_vignette', rawContent.relationships); let { pageTitle, partialContent } = fetchSingletonPartialContent(this.contentType, this.rawContent);
const partiesPromise = fetchFromRelationships(this.contentType === 'ressourceItem' ? 'field_parties_ressource' : 'field_parties', rawContent.relationships); this.pageTitle = pageTitle;
this.content = partialContent;
let previousEtapePromise, nextEtapePromise;
if (this.contentType === 'etape') {
previousEtapePromise = getRelatedEtape('previous', path);
nextEtapePromise = getRelatedEtape('next', path);
this.content.coordinates = {
lat: rawContent.attributes.field_geofield.lat,
lon: rawContent.attributes.field_geofield.lon,
};
this.content.adresse = rawContent.attributes.field_adresse;
this.content.etape_number = rawContent.attributes.field_arret_numero;
this.content.couleur = rawContent.attributes.field_couleur;
this.content.dates = {
start: getCleanDate(rawContent.attributes.field_dates.value),
end: getCleanDate(rawContent.attributes.field_dates.end_value),
}
this.content.relatedRessources = await getRelatedRessources(rawContent.id);
}
if (this.contentType === 'ressourceItem') {
console.log(rawContent);
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;
if (rawContent.relationships.field_etape.data) {
const relatedEtapeFetch = fetchFromRelationships('field_etape', rawContent.relationships);
const relatedEtape = await Promise.resolve(relatedEtapeFetch);
const relatedEtapeUrl = relatedEtape.attributes.metatag.find(tag => tag.tag === "link")?.attributes.href;
this.content.relatedEtape = await getRelatedEtape('', relatedEtapeUrl);
}
useLayoutStore().hideEtapeList(true);
}
this.pageTitle = rawContent.attributes.metatag.find(tag => tag.tag === "meta")?.attributes.content;
this.content.contentTitle = rawContent.attributes.title;
const [vignetteData, partiesData] = await Promise.all([vignettePromise, partiesPromise]);
if (vignetteData) {
this.content.vignette = {
url: {
original: vignetteData.attributes.uri.url,
small: vignetteData.attributes.image_style_uri.content_small,
medium: vignetteData.attributes.image_style_uri.content_medium,
large: vignetteData.attributes.image_style_uri.content_large,
},
alt: rawContent.relationships.field_vignette.data.meta.alt
};
}
if (partiesData) {
const partiesPromises = partiesData.map(async (partie) => {
const partieType = partie.type.replace(/^paragraph--/, "");
let partieContent = { type: partieType };
switch (partieType) {
case 'carte_sensible':
partieContent.carteSensible = await getCarteSensible(partie);
break;
case 'titre_texte':
const { titre, texte } = await getTitreTexte(partie);
partieContent.titre = titre;
partieContent.texte = texte;
break;
case 'chiffres_cles':
partieContent.chiffresCles = await getChiffresCles(partie);
break;
case 'diaporama':
partieContent.diaporama = await getDiaporama(partie);
break;
case 'entretien':
partieContent.entretien = await getEntretien(partie);
break;
case 'exergue':
partieContent.exergue = partie.attributes.field_texte_exergue.value;
break;
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);
}
// related étapes
if (contentType === 'etape') {
const [prevContent, nextContent] = await Promise.all([previousEtapePromise, nextEtapePromise]);
this.content.previous = prevContent;
this.content.next = nextContent;
}
} else { } else {
// pages gouvernance (contact), ressources et partenaire let { pageTitle, partialContent } = await fetchMultiplePartialContent(this.contentType);
// ont plusieurs items par pages this.pageTitle = pageTitle;
const intro = await REST.get(`/jsonapi/config_pages/intro_${this.contentType}/`); this.content = partialContent;
const introContent = intro.data.data[0];
this.pageTitle =
`${introContent.attributes.field_titre} ${introContent.attributes.metatag.find(tag => tag.tag === "meta")?.attributes.content}`;
this.content.contentTitle = introContent.attributes.field_titre;
this.content.intro = introContent.attributes.field_intro?.value;
let multiItemPageArray = [];
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);
break;
case 'gouvernance':
multiItemPageArray = await getGouvernance(rawContent);
break;
}
this.content[`${this.contentType}s`] = multiItemPageArray;
} }
} catch (error) { } catch(error) {
this.error = 'Failed to fetch data'; this.error = 'Failed to fetch partial data';
console.error('Issue with getNodeData', error); console.error('Issue with fetchPartialContentData', error);
} finally {
this.partialLoading = false;
}
},
async fetchFullContentData(path) {
try {
if (
this.contentType === 'etape'
|| this.contentType === 'static'
|| this.contentType === 'ressourceItem'
) {
this.content = { ...this.content, ...await fetchSingletonFullContent(this.contentType, this.rawContent, path) };
} else {
this.content = { ...this.content, ...await fetchMultipleFullContent(this.contentType, this.rawContent) };
}
} catch(error) {
this.error = 'Failed to fetch full data';
console.error('Issue with fetchFullContentData', error);
} finally { } finally {
this.loading = false; this.loading = false;
} }
@ -204,6 +71,7 @@ export const useContentStore = defineStore('content', {
this.contentType = ''; this.contentType = '';
this.pageTitle = ''; this.pageTitle = '';
this.content = {}; this.content = {};
this.partialLoading = !forFrontDisplay;
this.loading = !forFrontDisplay; this.loading = !forFrontDisplay;
this.error = null; this.error = null;
useLayoutStore().hideEtapeList(false); useLayoutStore().hideEtapeList(false);

View File

@ -128,6 +128,14 @@ export const useLayoutStore = defineStore('layout', {
setHeaderPosition(currentPageIsIndex) { setHeaderPosition(currentPageIsIndex) {
const header = document.querySelector('.layout-container > header'); const header = document.querySelector('.layout-container > header');
header.style.position = currentPageIsIndex ? 'fixed' : 'relative'; header.style.position = currentPageIsIndex ? 'fixed' : 'relative';
} },
toggleModaleTransition(shouldModaleTransition, enterDelay) {
if (shouldModaleTransition) {
document.documentElement.style.setProperty('--modale-enter-delay', `${enterDelay}s`);
} else {
document.documentElement.style.setProperty('margin-top', '0');
document.documentElement.style.setProperty('transition', 'none');
}
},
}, },
}) })

View File

@ -16,6 +16,39 @@ export const useMapStore = defineStore('mapState', {
animationDuration: 3, animationDuration: 3,
}), }),
actions: { actions: {
handleMapMovement(isModaleEtape, wasModaleEtape, lat = this.defaultMapCenter.lat, lon = this.defaultMapCenter.lng) {
if (this.animationsAreEnabled) {
if (isModaleEtape) {
if (!wasModaleEtape) {
// national -> détail
useLayoutStore().toggleModaleTransition(true, this.animationDuration);
this.zoomToPlace(lat, lon);
} else {
// détail -> détail
useLayoutStore().toggleModaleTransition(true, this.animationDuration);
this.zoomToPlace(lat, lon);
}
} else {
if (wasModaleEtape) {
// détail -> national
useLayoutStore().toggleModaleTransition(true, this.animationDuration);
this.resetMap();
} else {
// national -> national
useLayoutStore().toggleModaleTransition(true, 0);
}
}
} else {
if (isModaleEtape) {
// ? -> détail
this.zoomToPlace(lat, lon);
} else {
// ? -> national
this.resetMap();
}
useLayoutStore().toggleModaleTransition(false);
}
},
zoomToPlace(lat, long) { zoomToPlace(lat, long) {
if (useLayoutStore().isDesktop) long = long - 0.03; if (useLayoutStore().isDesktop) long = long - 0.03;
this.map.flyTo( this.map.flyTo(

View File

@ -122,7 +122,7 @@ export async function getRessourceItemCard(item) {
} }
const relatedEtape = await REST.get(item.relationships.field_etape.links.related.href); const relatedEtape = await REST.get(item.relationships.field_etape.links.related.href);
console.log(item); // console.log(item);
return { return {

View File

@ -0,0 +1,43 @@
import REST from '../../api/rest-axios';
import { useLayoutStore } from '../../stores/layout';
import { getPartenaires, getGouvernance, getRessources } from './multiItemPages';
export async function fetchMultiplePartialContent(contentType) {
let partialContent = {};
let pageTitle = '';
const intro = await REST.get(`/jsonapi/config_pages/intro_${contentType}/`);
const introContent = intro.data.data[0];
pageTitle =
`${introContent.attributes.field_titre} ${introContent.attributes.metatag.find(tag => tag.tag === "meta")?.attributes.content}`;
partialContent.contentTitle = introContent.attributes.field_titre;
partialContent.intro = introContent.attributes.field_intro?.value;
return { pageTitle, partialContent };
}
export async function fetchMultipleFullContent(contentType, rawContent) {
let content = {};
let multiItemPageArray = [];
switch (contentType) {
case 'ressource':
multiItemPageArray = await getRessources(rawContent);
content.ressourceTypes = new Set(multiItemPageArray.map(item => item.ressourceType));
useLayoutStore().hideEtapeList(true);
break;
case 'partenaire':
multiItemPageArray = await getPartenaires(rawContent);
break;
case 'gouvernance':
multiItemPageArray = await getGouvernance(rawContent);
break;
}
content[`${contentType}s`] = multiItemPageArray;
return content;
}

View File

@ -0,0 +1,153 @@
import REST from '../../api/rest-axios';
import { getCleanDate, fetchFromRelationships, getRelatedEtape, getRelatedRessources } from './contentFetchUtils';
import { getCarteSensible, getTitreTexte, getChiffresCles, getDiaporama, getEntretien, getVideos, getDocument, getGallerie } from './cleanParties';
import { useLayoutStore } from '../../stores/layout';
export function fetchSingletonPartialContent(contentType, rawContent) {
let partialContent = {};
let pageTitle = rawContent.attributes.metatag.find(tag => tag.tag === "meta")?.attributes.content;
partialContent.contentTitle = rawContent.attributes.title;
if (contentType === 'etape') {
partialContent.coordinates = {
lat: rawContent.attributes.field_geofield.lat,
lon: rawContent.attributes.field_geofield.lon,
};
partialContent.adresse = rawContent.attributes.field_adresse;
partialContent.etape_number = rawContent.attributes.field_arret_numero;
partialContent.couleur = rawContent.attributes.field_couleur;
partialContent.dates = {
start: getCleanDate(rawContent.attributes.field_dates.value),
end: getCleanDate(rawContent.attributes.field_dates.end_value),
}
}
if (contentType === 'ressourceItem') {
partialContent.ressourceType = rawContent.attributes.field_type_de_ressource;
partialContent.auteurice = rawContent.attributes.field_autheurice;
partialContent.date = getCleanDate(rawContent.attributes.field_date_ressource);
}
return { pageTitle, partialContent };
}
export async function fetchSingletonFullContent(contentType, rawContent, path) {
let content = {};
const vignettePromise = fetchFromRelationships('field_vignette', rawContent.relationships);
const partiesPromise = fetchFromRelationships(contentType === 'ressourceItem' ? 'field_parties_ressource' : 'field_parties', rawContent.relationships);
let previousEtapePromise, nextEtapePromise;
if (contentType === 'etape') {
previousEtapePromise = getRelatedEtape('previous', path);
nextEtapePromise = getRelatedEtape('next', path);
content.relatedRessources = await getRelatedRessources(rawContent.id);
}
if (contentType === 'ressourceItem') {
content.introduction = rawContent.attributes.field_introduction?.processed;
if (rawContent.relationships.field_etape.data) {
const relatedEtapeFetch = fetchFromRelationships('field_etape', rawContent.relationships);
const relatedEtape = await Promise.resolve(relatedEtapeFetch);
const relatedEtapeUrl = relatedEtape.attributes.metatag.find(tag => tag.tag === "link")?.attributes.href;
content.relatedEtape = await getRelatedEtape('', relatedEtapeUrl);
}
useLayoutStore().hideEtapeList(true);
}
const [vignetteData, partiesData] = await Promise.all([vignettePromise, partiesPromise]);
if (vignetteData) {
content.vignette = {
url: {
original: vignetteData.attributes.uri.url,
small: vignetteData.attributes.image_style_uri.content_small,
medium: vignetteData.attributes.image_style_uri.content_medium,
large: vignetteData.attributes.image_style_uri.content_large,
},
alt: rawContent.relationships.field_vignette.data.meta.alt
};
}
if (partiesData) {
const partiesPromises = partiesData.map(async (partie) => {
const partieType = partie.type.replace(/^paragraph--/, "");
let partieContent = { type: partieType };
switch (partieType) {
case 'carte_sensible':
partieContent.carteSensible = await getCarteSensible(partie);
break;
case 'titre_texte':
const { titre, texte } = await getTitreTexte(partie);
partieContent.titre = titre;
partieContent.texte = texte;
break;
case 'chiffres_cles':
partieContent.chiffresCles = await getChiffresCles(partie);
break;
case 'diaporama':
partieContent.diaporama = await getDiaporama(partie);
break;
case 'entretien':
partieContent.entretien = await getEntretien(partie);
break;
case 'exergue':
partieContent.exergue = partie.attributes.field_texte_exergue.value;
break;
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) {
content.liens = [];
for (let lien of rawContent.attributes.field_liens) {
content.liens.push({
title: lien.title,
url: lien.uri,
});
}
}
// pièces jointes
if (rawContent.relationships.field_pieces_jointes?.data.length) {
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}`);
content.pieces_jointes.push({
title: pieceJointe.meta.description,
url: response.data.data.attributes.uri.url,
});
}
}
}
content.parties = await Promise.all(partiesPromises);
}
// related étapes
if (contentType === 'etape') {
const [prevContent, nextContent] = await Promise.all([previousEtapePromise, nextEtapePromise]);
content.previous = prevContent;
content.next = nextContent;
}
return content;
}

View File

@ -55,7 +55,7 @@ export async function getGouvernance(rawContent) {
} }
export async function getRessources(rawContent) { export async function getRessources(rawContent) {
console.log(rawContent); // console.log(rawContent);
const ressourcesPromises = rawContent.map(item => getRessourceItemCard(item)); const ressourcesPromises = rawContent.map(item => getRessourceItemCard(item));

View File

@ -1,23 +1,27 @@
import { setActiveNavItem } from "./set-active-nav-item"; import { setActiveNavItem } from "./set-active-nav-item";
import { useContentStore } from "../stores/content";
import { useMapStore } from "../stores/map";
import { useLayoutStore } from '../stores/layout'; import { useLayoutStore } from '../stores/layout';
export async function initFirstLoadRouting(store, router, baseUrl, siteName) { export async function initFirstLoadRouting(router, baseUrl, siteName) {
const store = useContentStore();
const decoupled_origin = JSON.parse(window.localStorage.getItem('decoupled_origin')); const decoupled_origin = JSON.parse(window.localStorage.getItem('decoupled_origin'));
if(decoupled_origin) { if(decoupled_origin) {
router.push(decoupled_origin.url); router.push(decoupled_origin.url);
await store.fetchContentData(baseUrl + decoupled_origin.url); await store.fetchPartialContentData(baseUrl + decoupled_origin.url);
window.localStorage.removeItem("decoupled_origin"); window.localStorage.removeItem("decoupled_origin");
document.title = store.pageTitle; document.title = store.pageTitle;
setActiveNavItem(store.contentType, decoupled_origin.url); setActiveNavItem(store.contentType, decoupled_origin.url);
useLayoutStore().setHeaderPosition(false); useLayoutStore().setHeaderPosition(false);
await store.fetchFullContentData(baseUrl + decoupled_origin.url);
} else { } else {
document.title = siteName; document.title = siteName;
useLayoutStore().setHeaderPosition(true); useLayoutStore().setHeaderPosition(true);
} }
} }
export function handleClickableElements(clickableElements, store, router, baseUrl, siteName, mapStore) { export function handleClickableElements(clickableElements, router, baseUrl, siteName) {
for (const link of clickableElements) { for (const link of clickableElements) {
let href = link.href || link.dataset.href; let href = link.href || link.dataset.href;
if (href.startsWith(baseUrl)) href = href.replace(baseUrl, ''); if (href.startsWith(baseUrl)) href = href.replace(baseUrl, '');
@ -25,26 +29,29 @@ export function handleClickableElements(clickableElements, store, router, baseUr
link.onclick = async function (e) { link.onclick = async function (e) {
if (href !== window.location.pathname) { if (href !== window.location.pathname) {
router.push(href); router.push(href);
pageChange(href, store, siteName, mapStore, baseUrl); pageChange(href, siteName, baseUrl);
} }
} }
} }
} }
export async function handleBrowserNavigation(store, baseUrl, siteName, mapStore) { export async function handleBrowserNavigation(baseUrl, siteName) {
let href = window.location.pathname; let href = window.location.pathname;
if (href.startsWith(baseUrl)) href = href.replace(baseUrl, ''); if (href.startsWith(baseUrl)) href = href.replace(baseUrl, '');
pageChange(href, store, siteName, mapStore, baseUrl) pageChange(href, siteName, baseUrl)
} }
export async function pageChange(href, store, siteName, mapStore, baseUrl) { export async function pageChange(href, siteName, baseUrl) {
const store = useContentStore();
const mapStore = useMapStore();
if (href === '/') { if (href === '/') {
store.resetStore(true); store.resetStore(true);
document.title = siteName; document.title = siteName;
mapStore.resetMap(); mapStore.resetMap();
useLayoutStore().setHeaderPosition(true); useLayoutStore().setHeaderPosition(true);
} else { } else {
await store.fetchContentData(baseUrl + href); await store.fetchPartialContentData(baseUrl + href);
document.title = store.pageTitle; document.title = store.pageTitle;
useLayoutStore().setHeaderPosition(false); useLayoutStore().setHeaderPosition(false);
} }
@ -53,4 +60,8 @@ export async function pageChange(href, store, siteName, mapStore, baseUrl) {
const listeEtape = document.querySelector('#etapes-liste'); const listeEtape = document.querySelector('#etapes-liste');
const animationToggle = document.querySelector('#animation-toggle'); const animationToggle = document.querySelector('#animation-toggle');
if (!useLayoutStore().isDesktop) useLayoutStore().collapseEtapeListe(listeEtape, animationToggle); if (!useLayoutStore().isDesktop) useLayoutStore().collapseEtapeListe(listeEtape, animationToggle);
if (href !== '/') {
await store.fetchFullContentData(baseUrl + href);
}
} }

View File

@ -56,7 +56,7 @@ export function setMenuToggle() {
layoutStore.setUpHamburgerToggle(menuBurger, menuContainer); layoutStore.setUpHamburgerToggle(menuBurger, menuContainer);
} }
export function setHamburgerWhenLogged(drupalSettings) { export function setRightSectionsWhenLogged(drupalSettings) {
if (drupalSettings.user.uid != 0) { if (drupalSettings.user.uid != 0) {
const menuBurger = document.querySelector('#hamburger'); const menuBurger = document.querySelector('#hamburger');
const menuTitle = document.querySelector('#menu-title'); const menuTitle = document.querySelector('#menu-title');
@ -65,7 +65,11 @@ export function setHamburgerWhenLogged(drupalSettings) {
const headerTop = header.getBoundingClientRect().top; const headerTop = header.getBoundingClientRect().top;
menuTitle.style.top = `${headerTop}px`; menuTitle.style.top = `${headerTop}px`;
menuBurger.style.top = `${headerTop}px`; menuContainer.style.paddingTop = `${headerTop + 10}px`;
menuContainer.style.paddingTop = `${headerTop}px`; menuBurger.style.top = `${headerTop + 2}px`;
const etapesListContainer = document.querySelector('.block-region-third');
etapesListContainer.style.paddingTop = `50px`;
} }
} }

View File

@ -1,7 +1,10 @@
import { useLayoutStore } from '../stores/layout'; import { useLayoutStore } from '../stores/layout';
import { useMapStore } from '../stores/map';
import REST from '../api/rest-axios'; import REST from '../api/rest-axios';
export function setupMapStore(mapStore, map, settings) { export function setupMapStore(map, settings) {
const mapStore = useMapStore();
mapStore.map = map; mapStore.map = map;
mapStore.defaultMapCenter = map.getCenter(); mapStore.defaultMapCenter = map.getCenter();
mapStore.maxZoom = settings.settings.maxZoom; mapStore.maxZoom = settings.settings.maxZoom;
@ -13,7 +16,9 @@ export function setupMapStore(mapStore, map, settings) {
// not working // not working
// may or may not rework on it later // may or may not rework on it later
export async function preloadEtapesTiles(mapStore, map) { export async function preloadEtapesTiles(map) {
const mapStore = useMapStore();
function waitForEvent(el, eventName) { function waitForEvent(el, eventName) {
return new Promise((resolve) => { return new Promise((resolve) => {
el.once(eventName, resolve); el.once(eventName, resolve);

View File

@ -11,13 +11,19 @@ export function setActiveNavItem(contentType, href) {
} }
if (href === '/' || href === '') { if (href === '/' || href === '') {
staticNavItems[0].classList.add('is-active'); staticNavItems[1].classList.add('is-active');
for (let item of etapeNavItems) { for (let item of etapeNavItems) {
item.closest('li').classList.remove('inactive'); item.closest('li').classList.remove('inactive');
} }
} else { } else {
if (contentType === 'static') {
if (
contentType === 'static'
|| contentType === 'partenaire'
|| contentType === 'gouvernance'
|| contentType === 'ressource'
) {
for (let item of staticNavItems) { for (let item of staticNavItems) {
if (item.getAttribute('href') === href) { if (item.getAttribute('href') === href) {
item.classList.add('is-active'); item.classList.add('is-active');
@ -26,9 +32,12 @@ export function setActiveNavItem(contentType, href) {
} else if (contentType === 'etape') { } else if (contentType === 'etape') {
for (let item of etapeNavItems) { for (let item of etapeNavItems) {
if (item.getAttribute('href') === href) { if (item.getAttribute('href') === href) {
item.closest('li').classList.remove('inactive'); item.closest('li').classList.remove('inactive');
document.querySelector('#etapes-liste').scrollTo(0, item.closest('li').offsetTop);
} }
} }
} else if (contentType === "ressourceItem") {
staticNavItems[2].classList.add('is-active');
} }
} }

View File

@ -6,9 +6,6 @@ import AnimationToggle from '../vuejs/AnimationToggle.vue';
import VueImageZoomer from 'vue-image-zoomer'; import VueImageZoomer from 'vue-image-zoomer';
import 'vue-image-zoomer/dist/style.css'; import 'vue-image-zoomer/dist/style.css';
import { useContentStore } from '../stores/content';
import { useMapStore } from '../stores/map';
export function initVueContentModale() { export function initVueContentModale() {
const pinia = createPinia(); const pinia = createPinia();
@ -16,14 +13,12 @@ export function initVueContentModale() {
.use(pinia) .use(pinia)
.use(router) .use(router)
.use(VueImageZoomer); .use(VueImageZoomer);
const store = useContentStore();
const mapStore = useMapStore();
app.mount('#content-modale'); app.mount('#content-modale');
const animationToggle = createApp(AnimationToggle) const animationToggle = createApp(AnimationToggle)
.use(pinia) .use(pinia)
.mount('#animation-toggle'); .mount('#animation-toggle');
return { store, mapStore, router }; return router;
} }

View File

@ -3,9 +3,15 @@
:enter-active-class="animationsAreEnabled ? 'v-enter-active' : 'no-transition'" :enter-active-class="animationsAreEnabled ? 'v-enter-active' : 'no-transition'"
:leave-active-class="animationsAreEnabled ? 'v-leave-active' : 'no-transition'" :leave-active-class="animationsAreEnabled ? 'v-leave-active' : 'no-transition'"
> >
<div v-if="!loading && contentType != ''"> <div v-if="!partialLoading && contentType != ''">
<div class="content-wrapper"> <div
class="content-wrapper"
:class="
contentType === 'ressource' || contentType === 'ressourceItem'
? 'ressource' : ''"
>
<ModaleHeader <ModaleHeader
:loading="loading"
:contentType="contentType" :contentType="contentType"
:content="content" :content="content"
:couleur="content.couleur || brandColor" /> :couleur="content.couleur || brandColor" />
@ -14,7 +20,7 @@
v-if="contentType === 'ressourceItem'" v-if="contentType === 'ressourceItem'"
:content="content" :content="content"
:couleur="brandColor" /> :couleur="brandColor" />
<div v-for="partie in content.parties" class="partie"> <div v-if="!loading" v-for="partie in content.parties" class="partie">
<ModaleCarteSensible <ModaleCarteSensible
v-if="partie.type === 'carte_sensible'" v-if="partie.type === 'carte_sensible'"
:partie="partie" /> :partie="partie" />
@ -50,21 +56,27 @@
:partie="partie" :partie="partie"
:couleur="content.couleur || brandColor" /> :couleur="content.couleur || brandColor" />
</div> </div>
<EquipeContent <div class="content-loading" v-else>
v-if="contentType === 'gouvernance'" <div></div>
:content="content" <p>Chargement du contenu...</p>
:couleur="brandColor" /> </div>
<PartenairesContent <template v-if="!loading">
v-if="contentType === 'partenaire'" <EquipeContent
:content="content" /> v-if="contentType === 'gouvernance'"
<CentreDeRessource :content="content"
v-if="contentType === 'ressource'" :couleur="brandColor" />
:content="content" <PartenairesContent
:couleur="brandColor" /> v-if="contentType === 'partenaire'"
<RelatedRessources :content="content" />
v-if="contentType === 'etape' && content.relatedRessources.length" <CentreDeRessource
:relatedRessources="content.relatedRessources" v-if="contentType === 'ressource'"
:couleur="content.couleur || brandColor" /> :content="content"
:couleur="brandColor" />
<RelatedRessources
v-if="contentType === 'etape' && content.relatedRessources?.length"
:relatedRessources="content.relatedRessources"
:couleur="content.couleur || brandColor" />
</template>
</main> </main>
<PiecesJointes <PiecesJointes
v-if="content.pieces_jointes || content.liens" v-if="content.pieces_jointes || content.liens"
@ -111,123 +123,55 @@ const mapState = useMapStore();
const { const {
contentType, contentType,
content, content,
partialLoading,
loading, loading,
error, error,
} = storeToRefs(store); } = storeToRefs(store);
const { defaultMapCenter, animationDuration, animationsAreEnabled } = storeToRefs(mapState); const { animationsAreEnabled } = storeToRefs(mapState);
let isModaleEtape, wasModaleEtape; let isModaleEtape, wasModaleEtape;
const brandColor = "#80c8bf"; const brandColor = "#80c8bf";
const handleColorChange = () => {
watch(
() => content.value.couleur,
() => {
if (contentType.value === 'etape' && content.value.couleur) {
document.documentElement.style.setProperty('--etape-couleur', content.value.couleur || brandColor);
}
}
);
};
const handleMapMovement = () => {
watch(
() => loading.value,
() => {
if (!loading.value) {
isModaleEtape = contentType.value === 'etape';
console.log(contentType.value);
// Define helper functions in variables
const disableModaleTransition = () => {
document.documentElement.style.setProperty('margin-top', '0');
document.documentElement.style.setProperty('transition', 'none');
}
const setModaleTransition = (enterDelay) => {
document.documentElement.style.setProperty('--modale-enter-delay', `${enterDelay}s`);
};
const zoomToContentPlace = () => {
mapState.zoomToPlace(
content.value.coordinates.lat ? content.value.coordinates.lat : defaultMapCenter.value.lat,
content.value.coordinates.lon ? content.value.coordinates.lon : defaultMapCenter.value.lng
);
};
if (animationsAreEnabled.value) {
if (isModaleEtape) {
if (!wasModaleEtape) {
// national -> détail
setModaleTransition(animationDuration.value);
zoomToContentPlace();
} else {
// détail -> détail
setModaleTransition(animationDuration.value);
zoomToContentPlace();
}
} else {
if (wasModaleEtape) {
// détail -> national
setModaleTransition(animationDuration.value);
mapState.resetMap();
} else {
// national -> national
setModaleTransition(0);
}
}
} else {
if (isModaleEtape) {
// ? -> détail
zoomToContentPlace();
} else {
// ? -> national
mapState.resetMap();
}
disableModaleTransition();
}
scrollTo(0, 0);
wasModaleEtape = isModaleEtape;
}
},
);
};
watch(() => contentType.value, () => {
if (contentType.value === '') {
handleMapLock(false);
} else {
handleMapLock(true);
}
});
const handleMapLock = (shoudLock) => {
const checkAndExecute = () => {
const leafletLayer = document.querySelector('.leaflet-layer');
if (leafletLayer) {
if (shoudLock) {
mapState.lockMap();
} else {
mapState.unlockMap();
}
} else {
setTimeout(checkAndExecute, 100);
}
}
checkAndExecute();
}
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
watch(
() => content.value.couleur,
() => {
if (contentType.value === 'etape' && content.value.couleur) {
document.documentElement.style.setProperty('--etape-couleur', content.value.couleur || brandColor);
}
}
);
isModaleEtape = contentType.value === 'etape'; isModaleEtape = contentType.value === 'etape';
wasModaleEtape = isModaleEtape; wasModaleEtape = isModaleEtape;
handleColorChange(); watch(
handleMapMovement(); () => partialLoading.value,
() => {
if (!partialLoading.value) {
isModaleEtape = contentType.value === 'etape';
mapState.handleMapMovement(
isModaleEtape,
wasModaleEtape,
content.value.coordinates?.lat,
content.value.coordinates?.lon
);
scrollTo(0, 0);
wasModaleEtape = isModaleEtape;
}
}
);
watch(
() => contentType.value,
() => {
if (contentType.value === '') {
mapState.unlockMap();
} else {
mapState.lockMap();
}
}
);
}); });
}); });
</script> </script>

View File

@ -22,18 +22,13 @@ const props = defineProps({
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import router from '../../router/router'; import router from '../../router/router';
import { useContentStore } from '../../stores/content';
import { useMapStore } from '../../stores/map';
import { handleClickableElements } from '../../utils/handle-navigation.js'; import { handleClickableElements } from '../../utils/handle-navigation.js';
const store = useContentStore();
const mapStore = useMapStore();
const siteName = document.querySelector('#site_name').innerText; const siteName = document.querySelector('#site_name').innerText;
onMounted(() => { onMounted(() => {
const relatedEtapesCards = document.querySelectorAll('.card'); const relatedEtapesCards = document.querySelectorAll('.card');
const baseUrl = window.location.protocol + "//" + window.location.host; const baseUrl = window.location.protocol + "//" + window.location.host;
handleClickableElements(relatedEtapesCards, store, router, baseUrl, siteName, mapStore); handleClickableElements(relatedEtapesCards, router, baseUrl, siteName);
}); });
</script> </script>

View File

@ -1,7 +1,7 @@
<template> <template>
<header :class="{ 'not-etape': contentType !== 'etape' }"> <header :class="{ 'not-etape': contentType !== 'etape' }">
<div class="cover"> <div v-if="contentType === 'etape'" class="cover" :style="{ backgroundColor: `${couleur.substring(0, 7)}99`, aspectRatio: '3 / 2' }">
<img v-if="content.vignette" :src="content.vignette.url.medium" :alt="content.vignette.alt"> <img v-if="!loading && content.vignette" :src="content.vignette.url.medium" :alt="content.vignette.alt">
</div> </div>
<div v-if="contentType === 'etape' && content.dates" class="cartouche" :style="{ backgroundColor: couleur }"> <div v-if="contentType === 'etape' && content.dates" class="cartouche" :style="{ backgroundColor: couleur }">
<p>Étape n°{{content.etape_number}}</p> <p>Étape n°{{content.etape_number}}</p>
@ -23,6 +23,7 @@
<script setup> <script setup>
const props = defineProps({ const props = defineProps({
loading: Boolean,
contentType: String, contentType: String,
content: Object, content: Object,
couleur: String, couleur: String,

View File

@ -4,12 +4,12 @@
:id="`ressource-${index}`" :id="`ressource-${index}`"
:class="ressource.promoted ? 'promoted' : ''"> :class="ressource.promoted ? 'promoted' : ''">
<figure> <figure>
<img :src="ressource.vignette.url" :alt="ressource.vignette.alt" /> <img :src="ressource?.vignette.url" :alt="ressource?.vignette.alt" />
</figure> </figure>
<div> <div>
<h4>{{ ressource.title }}</h4> <h4>{{ ressource.title }}</h4>
<p>Le {{ ressource.date.d }} {{ ressource.date.m }} {{ ressource.date.y }}</p> <p>Le {{ ressource?.date.d }} {{ ressource?.date.m }} {{ ressource?.date.y }}</p>
<p>Par {{ ressource.auteurice }}</p> <p>Par {{ ressource?.auteurice }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -19,14 +19,8 @@ import { onMounted } from 'vue';
import router from '../../router/router'; import router from '../../router/router';
import { handleClickableElements } from '../../utils/handle-navigation.js'; import { handleClickableElements } from '../../utils/handle-navigation.js';
import { useContentStore } from '../../stores/content';
import { useMapStore } from '../../stores/map';
const store = useContentStore();
const mapStore = useMapStore();
const siteName = document.querySelector('#site_name').innerText; const siteName = document.querySelector('#site_name').innerText;
let relatedItemCards, baseUrl; let relatedItemCards, baseUrl;
onMounted(() => { onMounted(() => {
@ -36,7 +30,7 @@ onMounted(() => {
const setClickableElements = () => { const setClickableElements = () => {
relatedItemCards = document.querySelector(`#ressource-${props.index}`); relatedItemCards = document.querySelector(`#ressource-${props.index}`);
handleClickableElements([relatedItemCards], store, router, baseUrl, siteName, mapStore); handleClickableElements([relatedItemCards], router, baseUrl, siteName);
} }
defineExpose({ defineExpose({

View File

@ -3,14 +3,16 @@
<div class="retour"> <div class="retour">
<p data-href="/ressources"> Retour au centre de ressources</p> <p data-href="/ressources"> Retour au centre de ressources</p>
</div> </div>
<div class="type">{{ content.displayedType }}</div> <div class="type">{{ content?.displayedType }}</div>
<div class="title"> <div class="title">
<h2 <h2
:style="{ background: `linear-gradient(transparent 70%, ${couleur} 70%)` }"> :style="{ background: `linear-gradient(transparent 70%, ${couleur} 70%)` }">
{{ content.contentTitle }} {{ content?.contentTitle }}
</h2> </h2>
</div> </div>
<div class="meta">Par {{ content.auteurice }}, le {{ content.date.d }} {{ content.date.m }} {{ content.date.y }}</div> <div v-if="content.auteurice && content.date" class="meta">
Par {{ content?.auteurice }}, le {{ content?.date.d }} {{ content?.date.m }} {{ content?.date.y }}
</div>
</header> </header>
</template> </template>
@ -18,14 +20,9 @@
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import router from '../../router/router'; import router from '../../router/router';
import { useContentStore } from '../../stores/content';
import { useMapStore } from '../../stores/map';
import { handleClickableElements } from '../../utils/handle-navigation.js'; import { handleClickableElements } from '../../utils/handle-navigation.js';
const store = useContentStore(); const siteName = document.querySelector('#site_name')?.innerText;
const mapStore = useMapStore();
const siteName = document.querySelector('#site_name').innerText;
const props = defineProps({ const props = defineProps({
content: Object, content: Object,
@ -33,7 +30,7 @@ const props = defineProps({
}); });
function setDisplayedType() { function setDisplayedType() {
const ressourceType = props.content.ressourceType; const ressourceType = props.content?.ressourceType;
switch (ressourceType) { switch (ressourceType) {
case 'cartes_blanches': case 'cartes_blanches':
props.content.displayedType = 'Carte blanche'; props.content.displayedType = 'Carte blanche';
@ -57,7 +54,7 @@ function setDisplayedType() {
onMounted(() => { onMounted(() => {
const backToRessourcesLink = document.querySelectorAll('.retour > p'); const backToRessourcesLink = document.querySelectorAll('.retour > p');
const baseUrl = window.location.protocol + "//" + window.location.host; const baseUrl = window.location.protocol + "//" + window.location.host;
handleClickableElements(backToRessourcesLink, store, router, baseUrl, siteName, mapStore); handleClickableElements(backToRessourcesLink, router, baseUrl, siteName);
setDisplayedType(); setDisplayedType();
}); });
</script> </script>

View File

@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_P7sC{transform-origin:center;animation:spinner_svv2 .75s infinite linear}@keyframes spinner_svv2{100%{transform:rotate(360deg)}}</style><path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" class="spinner_P7sC"/></svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@ -754,6 +754,7 @@ body{
} }
} }
> #content-modale { > #content-modale {
pointer-events: none;
padding-bottom: 20vh; padding-bottom: 20vh;
@media screen and (min-width: $desktop-min-width) { @media screen and (min-width: $desktop-min-width) {
z-index: 6; z-index: 6;
@ -761,6 +762,7 @@ body{
> div:not(.image-viewer-wrapper, .image-modale) { > div:not(.image-viewer-wrapper, .image-modale) {
padding-bottom: 5vh; padding-bottom: 5vh;
> .content-wrapper { > .content-wrapper {
pointer-events: all;
left: 1.5vw; left: 1.5vw;
width: calc($modale-width-mobile); width: calc($modale-width-mobile);
top: 15vh; top: 15vh;
@ -768,21 +770,20 @@ body{
position: relative; position: relative;
background-color: white; background-color: white;
font-size: $labeur-font-size-mobile; font-size: $labeur-font-size-mobile;
&:has(#centre-de-ressource, #ressource-item-header) { &.ressource {
left: 5vw; left: 5vw;
} }
@media screen and (min-width: $desktop-min-width) { @media screen and (min-width: $desktop-min-width) {
font-size: $labeur-font-size-desktop; font-size: $labeur-font-size-desktop;
width: $modale-width-desktop; width: $modale-width-desktop;
} }
&:has(#centre-de-ressource), &.ressource {
&:has(#ressource-item-header) {
@media screen and (min-width: $tablet-min-width) { @media screen and (min-width: $tablet-min-width) {
left: 8vw; left: 8vw;
width: 84vw; width: 84vw;
.locality-title { .locality-title {
width: 42vw; width: 42vw !important;
margin-left: 21vw; margin-left: 21vw !important;
} }
} }
@media screen and (min-width: $desktop-min-width) { @media screen and (min-width: $desktop-min-width) {
@ -924,6 +925,7 @@ body{
} }
} }
> main { > main {
min-height: 30vh;
z-index: 1; z-index: 1;
position: relative; position: relative;
width: 100%; width: 100%;
@ -938,6 +940,26 @@ body{
padding: 0 calc($modale-x-padding * 4); padding: 0 calc($modale-x-padding * 4);
} }
} }
.content-loading {
width: 100%;
text-align: center;
padding-top: 2rem;
display: flex;
flex-direction: column;
align-items: center;
> div {
display: block;
width: 24px;
height: 24px;
background-image: url(/themes/custom/caravane/assets/pictograms/90-ring.svg);
background-size: 24px;
background-size: no-repeat;
margin-bottom: 1rem;
}
> p {
display: block;
}
}
> .partie, > .partie,
> #equipe { > #equipe {
width: 100%; width: 100%;
@ -1375,8 +1397,9 @@ body{
> .title { > .title {
width: 100%; width: 100%;
text-align: center; text-align: center;
margin-bottom: 1.5rem;
> h2 { > h2 {
display: inline-block; display: inline;
margin-top: 1rem; margin-top: 1rem;
font-size: $xl-font-size-mobile; font-size: $xl-font-size-mobile;
font-family: 'Joost', sans-serif; font-family: 'Joost', sans-serif;
@ -1388,7 +1411,7 @@ body{
} }
} }
> .pieces-jointes { > .pieces-jointes {
z-index: 1; z-index: 0;
position: relative; position: relative;
padding: 0 $modale-x-padding; padding: 0 $modale-x-padding;
box-sizing: border-box; box-sizing: border-box;
@ -1443,7 +1466,8 @@ body{
} }
> footer { > footer {
position: relative; position: relative;
z-index: 0; z-index: -1;
overflow: hidden;
.pattern-bottom { .pattern-bottom {
mask-image: linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0)); mask-image: linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0));
height: $modale-bottom-padding; height: $modale-bottom-padding;
@ -1741,4 +1765,14 @@ body{
} }
} }
} }
&.toolbar-fixed {
#content-modale .content-wrapper {
top: 20vh !important;
}
&.toolbar-vertical.toolbar-tray-open {
#content-modale .content-wrapper {
left: 15vw !important;
}
}
}
} }