ldc-agis/resources/ts/fo/components/ItemsListComponent.vue
2023-12-19 15:09:12 +00:00

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>