597 lines
19 KiB
Vue
597 lines
19 KiB
Vue
<template>
|
|
<div class="items-list container">
|
|
<!-- TITLE & SELECT -->
|
|
<div
|
|
v-if="showSelect"
|
|
class="row pt-5"
|
|
>
|
|
<div class="col-12 col-lg-6">
|
|
<div class="ranges-title w-100 w-lg-fit">
|
|
<span class="divide d-block w-100 mt-xl-3 my-2" />
|
|
<h1 class="f-sheldon text-uppercase text-lg-start my-3 text-center">
|
|
{{ __('nos actualités') }}
|
|
</h1>
|
|
<span class="divide d-block w-100 mt-xl-3 my-2" />
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6">
|
|
<div class="d-flex align-items-center h-100 w-100 justify-content-end">
|
|
<fieldset class="select-form position-relative my-3 my-lg-0">
|
|
<select
|
|
class="form-control highlight yellow"
|
|
role="button"
|
|
@change="updateItems(1)"
|
|
:disabled="intLoading"
|
|
>
|
|
<option
|
|
value="0"
|
|
selected
|
|
>
|
|
{{ __('Toutes catégories') }}
|
|
</option>
|
|
<option
|
|
v-for="(categoryEnum, categoryEnumIndex) in categoryEnums"
|
|
:key="categoryEnumIndex"
|
|
:value="categoryEnum.value"
|
|
>
|
|
{{ categoryEnum.label }}
|
|
</option>
|
|
</select>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- MOBILE: SLIDER -->
|
|
<div
|
|
class="items-slider d-block d-lg-none bg-agis-white pb-5"
|
|
:class="(itemModels.length) ? '' : 'd-none'"
|
|
>
|
|
<div class="swiper swiper-items overflow-hidden">
|
|
<div
|
|
class="swiper-wrapper"
|
|
@touchend="checkActiveSlide()"
|
|
>
|
|
<div
|
|
class="swiper-slide swiper-slide-content"
|
|
v-for="(item, id) in itemModels"
|
|
:key="id"
|
|
>
|
|
<div class="swiper-slide-items-container">
|
|
<div class="mx-auto p-4">
|
|
<p
|
|
v-if="rangeId === null && !showSelect"
|
|
class="text-primary text-center f-readex-pro p-0"
|
|
>
|
|
{{ item.title }}
|
|
</p>
|
|
<img
|
|
class="card-img-top"
|
|
:src="urlPathBase + (item.picture_thumbnail as string)"
|
|
:alt="(item.picture_thumbnail_alt as string)"
|
|
:title="(item.picture_thumbnail_title as string)"
|
|
>
|
|
<div class="card-body">
|
|
<template v-if="rangeId === null && !showSelect">
|
|
<p class="card-text m-0">
|
|
{{ item.tagline }}
|
|
</p>
|
|
<a
|
|
class="card-text m-0"
|
|
:href="getShowItemRoute(item.slug as string)"
|
|
>
|
|
{{ __('> En savoir +') }}
|
|
</a>
|
|
</template>
|
|
<template v-if="showSelect">
|
|
<p class="card-text m-0">
|
|
{{ new Date(item.publish_at as string).toLocaleDateString() }}
|
|
</p>
|
|
<p class="card-text m-0">
|
|
{{ item.title }}
|
|
</p>
|
|
<a
|
|
class="card-text m-0"
|
|
:href="getShowItemRoute(item.slug as string)"
|
|
>
|
|
{{ __('> Lire la suite') }}
|
|
</a>
|
|
</template>
|
|
<template v-if="rangeId !== null">
|
|
<p class="card-text fw-bold m-0">
|
|
{{ item.title }}
|
|
</p>
|
|
<p class="card-text m-0">
|
|
{{ item.subtitle }}
|
|
</p>
|
|
<a
|
|
class="card-text m-0"
|
|
:href="getShowItemRoute(item.slug as string)"
|
|
>
|
|
{{ __('> En savoir +') }}
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-around">
|
|
<div class="swiper-button-prev mx-3">
|
|
<img
|
|
@click="checkActiveSlide()"
|
|
src="@/images/global/btn-arrow.png"
|
|
:title="__('Navigation du slider (gauche)')"
|
|
:alt="__('flèche vers la gauche')"
|
|
role="button"
|
|
>
|
|
</div>
|
|
<button
|
|
v-if="intLoadingMore"
|
|
class="btn btn-primary"
|
|
:title="__('Cliquer pour charger la suite')"
|
|
@click="updateItems(page + 1, true)"
|
|
>
|
|
{{ __('> Charger plus') }}
|
|
</button>
|
|
<div
|
|
class="swiper-button-next mx-3"
|
|
:class="(intLoadingMore) ? 'd-none': ''"
|
|
>
|
|
<img
|
|
@click="checkActiveSlide()"
|
|
src="@/images/global/btn-arrow.png"
|
|
:title="__('Navigation du slider (droite)')"
|
|
:alt="__('flèche vers la droite')"
|
|
role="button"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- DESKTOP: FLEX WRAP -->
|
|
<div
|
|
class="position-relative d-none d-lg-flex row my-4"
|
|
:class="(rangeId === null && !showSelect) ? 'ranges-flexwrap' : ((showSelect) ? 'news-flexwrap' : 'range-flexwrap')"
|
|
>
|
|
<template v-if="intLoading">
|
|
<div class="position-absolute top-0 left-0 bg-secondary w-100 h-100 d-flex justify-content-center align-items-center bg-opacity-25 z-1">
|
|
<div
|
|
class="spinner-border"
|
|
role="status"
|
|
>
|
|
<span class="visually-hidden">{{ __('Loading...') }}</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-for="index in paginator.per_page"
|
|
:key="index"
|
|
class="cards-container col-4"
|
|
>
|
|
<div
|
|
class="card bg-transparent border-0 bg-agis-white d-flex flex-column p-4 my-2"
|
|
:class="((index % 3 === 0) ? 'ms-auto' : (((index % 3 === 1)) ? 'me-auto' : 'mx-auto'))"
|
|
>
|
|
<div class="placeholder-glow">
|
|
<span class="placeholder placeholder-image" />
|
|
<span class="placeholder col-12" />
|
|
<span class="placeholder col-12" />
|
|
<span class="placeholder col-12" />
|
|
<span class="placeholder col-12" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div
|
|
v-else
|
|
v-for="(item, id) in itemModels"
|
|
:key="id"
|
|
class="cards-container col-4"
|
|
>
|
|
<div
|
|
class="card border-0 bg-transparent bg-agis-white d-flex flex-column p-4 my-2"
|
|
:class="((id % 3 === 0) ? 'ms-0 me-auto' : (((id % 3 === 1)) ? 'mx-auto' : 'ms-auto me-0'))"
|
|
>
|
|
<div class="text-center">
|
|
<template v-if="rangeId === null && !showSelect">
|
|
<p class="text-dark f-readex-pro p-0">
|
|
{{ item.title }}
|
|
</p>
|
|
<a
|
|
:href="getShowItemRoute(item.slug as string)"
|
|
class="stretched-link"
|
|
/>
|
|
</template>
|
|
<div class="w-fit position-relative">
|
|
<div class="card-range-hover position-absolute top-0 start-0 w-100 h-100 bg-opacity-50 bg-light p-2">
|
|
<p class="text-dark f-readex-pro p-0">
|
|
{{ String(item.subtitle).substring(0,100) + (String(item.subtitle).length > 100 ? '...' : '') }}
|
|
</p>
|
|
</div>
|
|
<img
|
|
class="img-fluid w-auto"
|
|
:src="urlPathBase + (item.picture_thumbnail as string)"
|
|
:alt="(item.picture_thumbnail_alt as string)"
|
|
:title="(item.picture_thumbnail_title as string)"
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="card-body py-2 px-0">
|
|
<template v-if="showSelect">
|
|
<p
|
|
v-if="showSelect"
|
|
class="card-text m-0"
|
|
>
|
|
{{ new Date(item.publish_at as string).toLocaleDateString() }}
|
|
</p>
|
|
<p class="card-text m-0">
|
|
{{ item.title }}
|
|
</p>
|
|
<a
|
|
class="card-text m-0"
|
|
:href="getShowItemRoute(item.slug as string)"
|
|
>
|
|
{{ __('> Lire la suite') }}
|
|
</a>
|
|
<template
|
|
v-for="(categoryEnum, categoryEnumIndex) in categoryEnums"
|
|
:key="categoryEnumIndex"
|
|
>
|
|
<p
|
|
class="card-text m-0"
|
|
v-if="item.category === categoryEnum.value"
|
|
>
|
|
{{ categoryEnum.label }}
|
|
</p>
|
|
</template>
|
|
</template>
|
|
<template v-if="rangeId !== null">
|
|
<p class="card-text fw-bold mb-1">
|
|
{{ item.title }}
|
|
</p>
|
|
<p class="card-text m-0">
|
|
{{ item.subtitle }}
|
|
</p>
|
|
<a
|
|
class="card-text stretched-link m-0"
|
|
:href="getShowItemRoute(item.slug as string)"
|
|
>
|
|
{{ __('> En savoir +') }}
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- PAGINATION -->
|
|
<div
|
|
v-if="paginator.total > paginator.per_page"
|
|
class="d-none d-lg-flex row w-100 mx-auto"
|
|
>
|
|
<nav
|
|
class="py-4"
|
|
:aria-label="__('Navigation des produits par gamme')"
|
|
>
|
|
<ul
|
|
class="pagination d-flex justify-content-center align-items-center list-unstyled d-flex mb-0"
|
|
>
|
|
<template v-for="(link, linkIndex) of paginator.links">
|
|
<li
|
|
:key="linkIndex"
|
|
v-if="link.url"
|
|
:class="`page-item d-flex ff-poppins-bold fs-md ${
|
|
link.active ? 'active' : ''
|
|
}`"
|
|
>
|
|
<a
|
|
@click.prevent="navigateOnPage(String(link.url))"
|
|
class="page-link d-flex align-items-center"
|
|
:href="link.url"
|
|
v-html="String(link.label)"
|
|
/>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<!-- NO ITEMS -->
|
|
<p
|
|
v-if="!intLoading"
|
|
:class="(itemModels.length <= 0) ? '' : 'd-none'"
|
|
>
|
|
{{ __('Aucune :name n\'est disponible', {'name': modelName}) }}
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import Swiper from "swiper";
|
|
import { Navigation } from "swiper/modules";
|
|
import { defineComponent } from "vue";
|
|
import parseLaravelErrorMessages from "./../../modules/parseLaravelErrorMessages";
|
|
import route from "./../../modules/route";
|
|
import trans from "./../../modules/trans";
|
|
|
|
export default defineComponent({
|
|
name: "ItemsListComponent",
|
|
mixins: [trans, route, parseLaravelErrorMessages],
|
|
data(): {
|
|
itemModels: LaravelModelList;
|
|
itemsFilteredRoute: string;
|
|
categoryEnums: {
|
|
label: string,
|
|
name: string,
|
|
value: number,
|
|
}[];
|
|
selectedValue: number,
|
|
swiper: Swiper|null,
|
|
intLoading: boolean,
|
|
intLoadingMore: boolean,
|
|
concatItemsFiltered: boolean,
|
|
page: number;
|
|
paginator: LaravelPaginator;
|
|
routeFiltered: string;
|
|
routeShow: string;
|
|
showSelect: boolean;
|
|
modelName: string;
|
|
rangeId: number;
|
|
urlPathBase: string;
|
|
} {
|
|
return {
|
|
itemModels: [],
|
|
itemsFilteredRoute: "",
|
|
categoryEnums: [],
|
|
selectedValue: 0,
|
|
swiper: null,
|
|
intLoading: false,
|
|
intLoadingMore: false,
|
|
concatItemsFiltered: false,
|
|
page: 1,
|
|
paginator: {
|
|
current_page: 1,
|
|
first_page_url: "",
|
|
from: 1,
|
|
last_page: 1,
|
|
last_page_url: "",
|
|
links: [],
|
|
next_page_url: null,
|
|
path: "",
|
|
per_page: 12,
|
|
prev_page_url: null,
|
|
to: 1,
|
|
total: 1,
|
|
},
|
|
routeFiltered: "",
|
|
routeShow: "",
|
|
showSelect: false,
|
|
modelName: "",
|
|
rangeId: 0,
|
|
urlPathBase: "",
|
|
};
|
|
},
|
|
mounted() {
|
|
const json = String(this.$attrs.json ?? "{}"),
|
|
data = JSON.parse(json);
|
|
this.modelName = data.modelName;
|
|
this.itemModels = data.itemModels.data;
|
|
this.paginator = data.itemModels;
|
|
this.categoryEnums = data.categoryEnums ?? null;
|
|
this.routeFiltered = data.routeFiltered;
|
|
this.routeShow = data.routeShow;
|
|
this.showSelect = data.showSelect;
|
|
this.rangeId = data.range_id ?? null;
|
|
this.urlPathBase = window.location.origin + "/";
|
|
if (this.showSelect) {
|
|
this.$nextTick(() => {
|
|
this.initSelect();
|
|
});
|
|
}
|
|
this.initSwiper();
|
|
},
|
|
methods: {
|
|
/** Init select ::after animation */
|
|
initSelect(): void {
|
|
const contactFormSelectList = document.querySelectorAll(".items-list .select-form select") as NodeListOf<HTMLSelectElement>;
|
|
document.body.addEventListener("click", function (e: MouseEvent) {
|
|
const target = e.target as HTMLElement | null;
|
|
contactFormSelectList.forEach(select => {
|
|
if (select.contains(target)) {
|
|
select.parentElement?.classList.toggle("active");
|
|
} else {
|
|
select.parentElement?.classList.remove("active");
|
|
}
|
|
});
|
|
});
|
|
},
|
|
/** Init items slider */
|
|
initSwiper(): void {
|
|
this.swiper = new Swiper(".swiper-items", {
|
|
modules: [Navigation],
|
|
grabCursor: true,
|
|
slidesPerView: 1,
|
|
spaceBetween: 20,
|
|
navigation: {
|
|
nextEl: ".swiper-button-next",
|
|
prevEl: ".swiper-button-prev",
|
|
},
|
|
});
|
|
this.checkActiveSlide();
|
|
this.swiper.on("slideChange", () => {
|
|
setTimeout(this.checkActiveSlide, 300);
|
|
});
|
|
},
|
|
/** Get selected value */
|
|
getSelectedValue(): number {
|
|
let target = document.querySelector(".items-list .select-form select") as HTMLSelectElement | null;
|
|
return Number(target?.value);
|
|
},
|
|
/** Get filtered items route */
|
|
getFilteredItemsRoute(): string {
|
|
const route = this.route(this.routeFiltered);
|
|
if (!route) {
|
|
throw new Error("Undefined route " + this.routeFiltered);
|
|
}
|
|
return route + "?page=" + this.page +
|
|
((this.showSelect != null) ? "&category=" + this.selectedValue : "") +
|
|
((this.rangeId != null) ? "&range_id=" + this.rangeId : "");
|
|
},
|
|
/** Get show item route */
|
|
getShowItemRoute(slug: string): string {
|
|
const route = this.route(this.routeShow, {
|
|
SLUG: slug,
|
|
});
|
|
if (!route) {
|
|
throw new Error("Undefined route " + this.routeShow);
|
|
}
|
|
return route;
|
|
},
|
|
/** Get filtered items */
|
|
getFilteredItems(): void {
|
|
window.axios
|
|
.get(this.itemsFilteredRoute)
|
|
.then((response) => {
|
|
if (this.intLoadingMore && this.concatItemsFiltered) {
|
|
this.itemModels = this.itemModels.concat(Object.values(response.data.data));
|
|
} else {
|
|
this.itemModels = Object.values(response.data.data);
|
|
}
|
|
this.paginator = response.data;
|
|
this.intLoading = false;
|
|
this.intLoadingMore = false;
|
|
this.$nextTick(() => {
|
|
this.swiper?.update();
|
|
});
|
|
})
|
|
.catch(this.ajaxErrorHandler);
|
|
},
|
|
/** Return error message after the ajax call */
|
|
ajaxErrorHandler(e: any): void {
|
|
this.intLoading = false;
|
|
let message = this.__("Une erreur est survenue");
|
|
if (e.response.status === 422) {
|
|
message =
|
|
this.parseValidationErrors(
|
|
e.response?.data?.errors ?? {}
|
|
) || message;
|
|
if (window.vueDebug) {
|
|
console.warn(e.response.data.errors);
|
|
}
|
|
} else if (e.response.status === 419) {
|
|
// * CSRF TOKEN MISMATCH (On ne pourrais plus faire d'appels AJAX sans recharger la page)
|
|
console.log(this.__("Votre session a expiré, la page va être rechargée"));
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} else if (e.response.status === 403) {
|
|
// * Access denied
|
|
console.log(this.__("Vous n'êtes pas autorisé a effectuer cette action"));
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
} else {
|
|
if (window.vueDebug) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
console.log(message);
|
|
},
|
|
/** Update items with filtered items */
|
|
updateItems(page: number, concat = false as boolean): void {
|
|
this.concatItemsFiltered = concat;
|
|
this.page = page;
|
|
this.intLoading = true;
|
|
if (this.showSelect) {
|
|
this.selectedValue = this.getSelectedValue();
|
|
}
|
|
this.itemsFilteredRoute = this.getFilteredItemsRoute();
|
|
this.getFilteredItems();
|
|
},
|
|
/** Get all links from the paginator */
|
|
navigateOnPage(pageUrl: string): void {
|
|
new URLSearchParams(pageUrl).get("page");
|
|
this.page = Number.parseInt(
|
|
String(new URL(pageUrl).searchParams.get("page") ?? 1)
|
|
);
|
|
this.updateItems(this.page);
|
|
},
|
|
/** Check if it's the last slide is activated */
|
|
checkActiveSlide(): void {
|
|
setTimeout(() => {
|
|
this.intLoadingMore = (
|
|
this.swiper?.activeIndex !== undefined &&
|
|
this.paginator.last_page > this.page &&
|
|
this.paginator.current_page * this.paginator.per_page <= (this.swiper?.activeIndex + 1)
|
|
) ? true : false;
|
|
}, 300);
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import "./../../../../node_modules/bootstrap/scss/functions";
|
|
@import "./../../../../node_modules/bootstrap/scss/variables";
|
|
@import "./../../../../node_modules/bootstrap/scss/mixins";
|
|
.items-list {
|
|
.swiper .swiper-button {
|
|
&-prev,
|
|
&-next { width: 3rem; }
|
|
&-next { rotate: 180deg; }
|
|
&-disabled { opacity: .5; }
|
|
}
|
|
.card-range-hover {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: .3s;
|
|
}
|
|
.news-flexwrap .card {
|
|
max-width: calc(265px + 3rem);
|
|
}
|
|
.ranges-flexwrap {
|
|
.col-4:nth-child(1),
|
|
.col-4:nth-child(3) {
|
|
transform: translateY(-3rem);
|
|
}
|
|
.card {
|
|
max-width: calc(246px + 3rem);
|
|
&:hover .card-range-hover {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
}
|
|
}
|
|
.range-flexwrap .card {
|
|
max-width: calc(332px + 3rem);
|
|
}
|
|
.cards-container .bg-agis-white .placeholder-image {
|
|
max-width: 265px;
|
|
width: 100%;
|
|
height: 354px;
|
|
}
|
|
.select-form {
|
|
width: 100%;
|
|
@include media-breakpoint-up(lg) {
|
|
width: 20rem;
|
|
}
|
|
&::after {
|
|
position: absolute;
|
|
right: 1rem;
|
|
bottom: 0.3rem;
|
|
display: block;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
background: url("@/images/global/btn-arrow.png");
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: 100% 100%;
|
|
content: "";
|
|
pointer-events: none;
|
|
transform: rotate(180deg);
|
|
transition: 0.1s ease-in-out;
|
|
}
|
|
|
|
&.active::after {
|
|
background: url("@/images/global/btn-arrow-dark.png");
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: 100% 100%;
|
|
transform: rotate(270deg);
|
|
}
|
|
}
|
|
}
|
|
</style>
|