Features: 1) Add useWishlistOverwrite composable for wishlist mutations, including adding, removing, and bulk actions; 2) Introduce new localized UI texts for cart and wishlist operations; 3) Enhance filtering logic with parseAttributesString and route query synchronization;
Fixes: 1) Replace `ElNotification` calls with `useNotification` utility across all authentication and user-related composables; 2) Add missing semicolons in multiple index exports and styled components; 3) Resolve issues with reactivity in `useStore` composable by renaming and restructuring product variables; Extra: 1) Refactor localized strings and translations for better readability and maintenance; 2) Tweak styles including scoped styles, z-index adjustments, and SCSS mixins; 3) Remove unused components and imports to streamline storefront layout.
This commit is contained in:
parent
53df1f5b88
commit
761fecf67f
75 changed files with 2336 additions and 375 deletions
|
|
@ -23,6 +23,7 @@ import {useRefresh} from "~/composables/auth";
|
||||||
import {useLanguages} from "~/composables/languages";
|
import {useLanguages} from "~/composables/languages";
|
||||||
import {useCompanyInfo} from "~/composables/company";
|
import {useCompanyInfo} from "~/composables/company";
|
||||||
import {useCategories} from "~/composables/categories";
|
import {useCategories} from "~/composables/categories";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
@ -32,7 +33,13 @@ const switchLocalePath = useSwitchLocalePath();
|
||||||
|
|
||||||
const showBreadcrumbs = computed(() => {
|
const showBreadcrumbs = computed(() => {
|
||||||
const name = typeof route.name === 'string' ? route.name : '';
|
const name = typeof route.name === 'string' ? route.name : '';
|
||||||
return !['index', 'brand', 'search'].some(prefix => name.startsWith(prefix));
|
return ![
|
||||||
|
'index',
|
||||||
|
'brand',
|
||||||
|
'search',
|
||||||
|
'profile',
|
||||||
|
'activate-user'
|
||||||
|
].some(prefix => name.startsWith(prefix));
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeState = computed(() => appStore.activeState);
|
const activeState = computed(() => appStore.activeState);
|
||||||
|
|
@ -66,7 +73,7 @@ watch(
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
let stopWatcher: () => void;
|
let stopWatcher: VoidFunction = () => {}
|
||||||
|
|
||||||
onMounted( async () => {
|
onMounted( async () => {
|
||||||
refreshInterval = setInterval(async () => {
|
refreshInterval = setInterval(async () => {
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,8 @@
|
||||||
@use "modules/transitions";
|
@use "modules/transitions";
|
||||||
@use "global/mixins";
|
@use "global/mixins";
|
||||||
@use "global/variables";
|
@use "global/variables";
|
||||||
@use "ui/collapse";
|
|
||||||
|
// UI
|
||||||
|
@use "ui/collapse";
|
||||||
|
@use "ui/notification";
|
||||||
|
@use "ui/rating";
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ button:focus-visible {
|
||||||
--el-skeleton-to-color: #c3c3c7 !important;
|
--el-skeleton-to-color: #c3c3c7 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-badge__content {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1680px) {
|
@media (max-width: 1680px) {
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
.el-icon {
|
.el-collapse .el-icon {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
39
storefront/assets/styles/ui/notification.scss
Normal file
39
storefront/assets/styles/ui/notification.scss
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
@use "../global/variables" as *;
|
||||||
|
|
||||||
|
.el-notification {
|
||||||
|
border: 2px solid $accent !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background-color: $accentDark;
|
||||||
|
animation: progress-animation linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: $accent;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-notification__closeBtn {
|
||||||
|
color: $accent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-animation {
|
||||||
|
0% {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
storefront/assets/styles/ui/rating.scss
Normal file
17
storefront/assets/styles/ui/rating.scss
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
@use "../global/variables" as *;
|
||||||
|
|
||||||
|
.el-rate .el-rate__icon.is-active {
|
||||||
|
color: $accent !important;
|
||||||
|
}
|
||||||
|
.el-rate .el-rate__icon {
|
||||||
|
color: #9a9a9a !important;
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white .el-rate__icon.is-active {
|
||||||
|
color: $white !important;
|
||||||
|
font-size: 24px !important;
|
||||||
|
}
|
||||||
|
.el-rate .el-rate__icon {
|
||||||
|
font-size: 24px !important;
|
||||||
|
}
|
||||||
|
|
@ -70,7 +70,6 @@ const setBlock = (state: boolean) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add loading state
|
// TODO: add loading state
|
||||||
// TODO: fix displaying main part (children categories)
|
|
||||||
|
|
||||||
const blockRef = ref(null)
|
const blockRef = ref(null)
|
||||||
onClickOutside(blockRef, () => setBlock(false))
|
onClickOutside(blockRef, () => setBlock(false))
|
||||||
|
|
@ -148,7 +147,7 @@ const setActiveCategory = (category: ICategory) => {
|
||||||
&__block {
|
&__block {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20% 80%;
|
grid-template-columns: 20% 80%;
|
||||||
max-height: 60vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__columns {
|
&__columns {
|
||||||
|
|
|
||||||
|
|
@ -13,37 +13,33 @@
|
||||||
<base-header-catalog />
|
<base-header-catalog />
|
||||||
<base-header-search />
|
<base-header-search />
|
||||||
<div class="header__actions">
|
<div class="header__actions">
|
||||||
<nuxt-link-locale to="/wishlist" class="header__actions-item">
|
<nuxt-link-locale to="/profile/wishlist" class="header__actions-item">
|
||||||
<div>
|
<el-badge :value="productsInWishlistQuantity" type="primary">
|
||||||
<!-- <ui-counter>0</ui-counter>-->
|
<icon name="mdi:cards-heart-outline" size="28" />
|
||||||
<!-- <skeletons-ui-counter />-->
|
</el-badge>
|
||||||
<Icon name="mdi:cards-heart-outline" size="28" />
|
|
||||||
</div>
|
|
||||||
<p>{{ t('header.actions.wishlist') }}</p>
|
<p>{{ t('header.actions.wishlist') }}</p>
|
||||||
</nuxt-link-locale>
|
</nuxt-link-locale>
|
||||||
<nuxt-link-locale to="/cart" class="header__actions-item">
|
<nuxt-link-locale to="/profile/cart" class="header__actions-item">
|
||||||
<div>
|
<el-badge :value="productsInCartQuantity" type="primary">
|
||||||
<!-- <ui-counter>0</ui-counter>-->
|
<icon name="ph:shopping-cart-light" size="28" />
|
||||||
<!-- <skeletons-ui-counter />-->
|
</el-badge>
|
||||||
<Icon name="ph:shopping-cart-light" size="28" />
|
|
||||||
</div>
|
|
||||||
<p>{{ t('header.actions.cart') }}</p>
|
<p>{{ t('header.actions.cart') }}</p>
|
||||||
</nuxt-link-locale>
|
</nuxt-link-locale>
|
||||||
<client-only>
|
<client-only>
|
||||||
<nuxt-link-locale
|
<nuxt-link-locale
|
||||||
to="/"
|
to="/profile/settings"
|
||||||
class="header__actions-item"
|
class="header__actions-item"
|
||||||
v-if="isAuthenticated"
|
v-if="isAuthenticated"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols-light:person-outline-rounded" size="32" />
|
<icon name="material-symbols-light:person-outline-rounded" size="32" />
|
||||||
<p @click="logout">{{ t('header.actions.profile') }}</p>
|
<p>{{ t('header.actions.profile') }}</p>
|
||||||
</nuxt-link-locale>
|
</nuxt-link-locale>
|
||||||
<div
|
<div
|
||||||
class="header__actions-item"
|
class="header__actions-item"
|
||||||
@click="appStore.setActiveState('login')"
|
@click="appStore.setActiveState('login')"
|
||||||
v-else
|
v-else
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols-light:person-outline-rounded" size="32" />
|
<icon name="material-symbols-light:person-outline-rounded" size="32" />
|
||||||
<p>{{ t('header.actions.login') }}</p>
|
<p>{{ t('header.actions.login') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
|
|
@ -51,7 +47,7 @@
|
||||||
class="header__actions-item"
|
class="header__actions-item"
|
||||||
@click="appStore.setActiveState('login')"
|
@click="appStore.setActiveState('login')"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols-light:person-outline-rounded" size="32" />
|
<icon name="material-symbols-light:person-outline-rounded" size="32" />
|
||||||
<p>{{ t('header.actions.login') }}</p>
|
<p>{{ t('header.actions.login') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -64,20 +60,34 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useLogout} from "~/composables/auth";
|
import {useLogout} from "~/composables/auth";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const wishlistStore = useWishlistStore();
|
||||||
|
const cartStore = useCartStore();
|
||||||
|
|
||||||
const isAuthenticated = computed(() => userStore.isAuthenticated)
|
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
||||||
|
|
||||||
const { logout } = useLogout()
|
const productsInCartQuantity = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
cartStore.currentOrder?.orderProducts?.edges.forEach((el) => {
|
||||||
|
count = count + el.node.quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
const productsInWishlistQuantity = computed(() => {
|
||||||
|
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges.length : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { logout } = useLogout();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.header {
|
.header {
|
||||||
box-shadow: 0 1px 2px #0000001a;
|
box-shadow: 0 1px 2px #0000001a;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
@click="clearSearch"
|
@click="clearSearch"
|
||||||
v-if="query"
|
v-if="query"
|
||||||
>
|
>
|
||||||
<Icon name="gridicons:cross" size="16" />
|
<icon name="gridicons:cross" size="16" />
|
||||||
</button>
|
</button>
|
||||||
<div class="search__tools-line" v-if="query"></div>
|
<div class="search__tools-line" v-if="query"></div>
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<Icon name="tabler:search" size="16" />
|
<icon name="tabler:search" size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -29,22 +29,23 @@
|
||||||
<skeletons-header-search v-if="loading" />
|
<skeletons-header-search v-if="loading" />
|
||||||
<div
|
<div
|
||||||
class="search__results-inner"
|
class="search__results-inner"
|
||||||
v-for="(blocks, item) in filteredSearchResults"
|
v-for="(blocks, category) in filteredSearchResults"
|
||||||
:key="item"
|
:key="category"
|
||||||
>
|
>
|
||||||
<div class="search__results-title">
|
<div class="search__results-title">
|
||||||
<p>{{ getBlockTitle(item) }}:</p>
|
<p>{{ getBlockTitle(category) }}:</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="search__item"
|
class="search__item"
|
||||||
v-for="item in blocks"
|
v-for="item in blocks"
|
||||||
:key="item.uuid"
|
:key="item.uuid"
|
||||||
|
@click.stop="goTo(category, item)"
|
||||||
>
|
>
|
||||||
<div class="search__item-left">
|
<div class="search__item-left">
|
||||||
<Icon name="ic:twotone-search" size="18" />
|
<icon name="ic:twotone-search" size="18" />
|
||||||
<p>{{ item.name }}</p>
|
<p>{{ item.name }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Icon name="line-md:external-link" size="18" />
|
<icon name="line-md:external-link" size="18" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
||||||
|
|
@ -90,6 +91,29 @@ function submitSearch() {
|
||||||
toggleSearch(false);
|
toggleSearch(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goTo(category: string, item: any) {
|
||||||
|
let path = "/";
|
||||||
|
console.log('c', category)
|
||||||
|
console.log(item)
|
||||||
|
switch (category) {
|
||||||
|
case "products":
|
||||||
|
path = `/product/${item.slug}`;
|
||||||
|
break;
|
||||||
|
case "categories":
|
||||||
|
path = `/catalog/${item.slug}`;
|
||||||
|
break;
|
||||||
|
case "brands":
|
||||||
|
path = `/brand/${item.uuid}`;
|
||||||
|
break;
|
||||||
|
case "posts":
|
||||||
|
path = "/";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSearch(false);
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="card"
|
class="card"
|
||||||
:class="{ 'card__list': productView === 'list' }"
|
:class="{ 'card__list': isList }"
|
||||||
>
|
>
|
||||||
<div class="card__wrapper">
|
<div class="card__wrapper">
|
||||||
<nuxt-link-locale
|
<nuxt-link-locale
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
>
|
>
|
||||||
<div class="card__block">
|
<div class="card__block">
|
||||||
<client-only>
|
<client-only>
|
||||||
<Swiper
|
<swiper
|
||||||
v-if="images.length"
|
v-if="images.length"
|
||||||
@swiper="onSwiper"
|
@swiper="onSwiper"
|
||||||
:modules="[EffectFade, Pagination]"
|
:modules="[EffectFade, Pagination]"
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
:pagination="paginationOptions"
|
:pagination="paginationOptions"
|
||||||
class="card__swiper"
|
class="card__swiper"
|
||||||
>
|
>
|
||||||
<SwiperSlide
|
<swiper-slide
|
||||||
v-for="(img, i) in images"
|
v-for="(img, i) in images"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="card__swiper-slide"
|
class="card__swiper-slide"
|
||||||
|
|
@ -29,16 +29,18 @@
|
||||||
:alt="product.name"
|
:alt="product.name"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="card__swiper-image"
|
class="card__swiper-image"
|
||||||
|
format="webp"
|
||||||
|
densities="x1"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</swiper-slide>
|
||||||
</Swiper>
|
</swiper>
|
||||||
<div class="card__image-placeholder" />
|
<div class="card__image-placeholder" />
|
||||||
<div
|
<div
|
||||||
v-for="(_, i) in images"
|
v-for="(image, idx) in images"
|
||||||
:key="i"
|
:key="idx"
|
||||||
class="card__block-hover"
|
class="card__block-hover"
|
||||||
:style="{ left: `${(100/ images.length) * i}%`, width: `${100/ images.length}%` }"
|
:style="{ left: `${(100/ images.length) * idx}%`, width: `${100/ images.length}%` }"
|
||||||
@mouseenter="goTo(i)"
|
@mouseenter="goTo(idx)"
|
||||||
@mouseleave="goTo(0)"
|
@mouseleave="goTo(0)"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
|
|
@ -50,19 +52,46 @@
|
||||||
<el-rate
|
<el-rate
|
||||||
v-model="rating"
|
v-model="rating"
|
||||||
size="large"
|
size="large"
|
||||||
allow-half
|
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<div class="card__quantity">{{ t('cards.product.stock') }} {{ product.quantity }}</div>
|
<div class="card__quantity">{{ t('cards.product.stock') }} {{ product.quantity }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__bottom">
|
<div class="card__bottom">
|
||||||
<ui-button class="card__bottom-button">
|
<ui-button
|
||||||
|
class="card__bottom-button"
|
||||||
|
v-if="isProductInCart"
|
||||||
|
@click="overwriteOrder({
|
||||||
|
type: 'remove',
|
||||||
|
productUuid: product.uuid,
|
||||||
|
productName: product.name
|
||||||
|
})"
|
||||||
|
:isLoading="removeLoading"
|
||||||
|
>
|
||||||
|
{{ t('buttons.removeFromCart') }}
|
||||||
|
</ui-button>
|
||||||
|
<ui-button
|
||||||
|
v-else
|
||||||
|
class="card__bottom-button"
|
||||||
|
@click="overwriteOrder({
|
||||||
|
type: 'add',
|
||||||
|
productUuid: product.uuid,
|
||||||
|
productName: product.name
|
||||||
|
})"
|
||||||
|
:isLoading="addLoading"
|
||||||
|
>
|
||||||
{{ t('buttons.addToCart') }}
|
{{ t('buttons.addToCart') }}
|
||||||
</ui-button>
|
</ui-button>
|
||||||
<div class="card__bottom-wishlist">
|
<div
|
||||||
<Icon name="mdi:cards-heart-outline" size="28" />
|
class="card__bottom-wishlist"
|
||||||
<!-- <Icon name="mdi:cards-heart" size="28" />-->
|
@click="overwriteWishlist({
|
||||||
|
type: (isProductInWishlist ? 'remove' : 'add'),
|
||||||
|
productUuid: product.uuid,
|
||||||
|
productName: product.name
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
<icon name="mdi:cards-heart" size="28" v-if="isProductInWishlist" />
|
||||||
|
<icon name="mdi:cards-heart-outline" size="28" v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -70,28 +99,36 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {IProduct} from "~/types/app/products";
|
import type {IProduct} from "~/types/app/products";
|
||||||
import { useAppConfig } from '~/composables/config';
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||||
import { EffectFade, Pagination } from 'swiper/modules';
|
import { EffectFade, Pagination } from 'swiper/modules';
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/effect-fade';
|
import 'swiper/css/effect-fade';
|
||||||
import 'swiper/css/pagination'
|
import 'swiper/css/pagination'
|
||||||
|
import {useWishlistOverwrite} from "~/composables/wishlist";
|
||||||
|
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
product: IProduct;
|
product: IProduct;
|
||||||
|
isList?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
const wishlistStore = useWishlistStore();
|
||||||
|
const cartStore = useCartStore();
|
||||||
|
|
||||||
const { COOKIES_PRODUCT_VIEW_KEY } = useAppConfig()
|
const { overwriteWishlist } = useWishlistOverwrite();
|
||||||
|
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
|
||||||
|
|
||||||
const productView = useCookie<string>(
|
const isProductInWishlist = computed(() => {
|
||||||
COOKIES_PRODUCT_VIEW_KEY as string,
|
const el = wishlistStore.wishlist?.products?.edges.find(
|
||||||
{
|
(el) => el?.node?.uuid === props.product.uuid
|
||||||
default: () => 'grid',
|
);
|
||||||
path: '/',
|
|
||||||
}
|
return !!el;
|
||||||
)
|
});
|
||||||
|
const isProductInCart = computed(() => {
|
||||||
|
return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid);
|
||||||
|
});
|
||||||
|
|
||||||
const rating = computed(() => {
|
const rating = computed(() => {
|
||||||
return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
|
return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
|
||||||
|
|
@ -171,7 +208,7 @@ function goTo(index: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include hover {
|
@include hover {
|
||||||
box-shadow: 0 0 30px 3px rgba($accentDark, 0.4);
|
box-shadow: 0 0 20px 2px rgba($accentDark, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__wrapper {
|
&__wrapper {
|
||||||
|
|
@ -248,6 +285,10 @@ function goTo(index: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__quantity {
|
&__quantity {
|
||||||
|
width: fit-content;
|
||||||
|
background-color: rgba($accent, 0.2);
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
padding: 5px 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
79
storefront/components/profile/navigation.vue
Normal file
79
storefront/components/profile/navigation.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<nav class="nav">
|
||||||
|
<nuxt-link-locale
|
||||||
|
class="nav__item"
|
||||||
|
:class="[{ active: route.path.includes('settings') }]"
|
||||||
|
to="/profile/settings"
|
||||||
|
>
|
||||||
|
<icon name="ic:baseline-settings" size="20" />
|
||||||
|
{{ t('profile.settings.title') }}
|
||||||
|
</nuxt-link-locale>
|
||||||
|
<nuxt-link-locale
|
||||||
|
class="nav__item"
|
||||||
|
:class="[{ active: route.path.includes('orders') }]"
|
||||||
|
to="/profile/orders"
|
||||||
|
>
|
||||||
|
<icon name="material-symbols:order-approve-rounded" size="20" />
|
||||||
|
{{ t('profile.orders.title') }}
|
||||||
|
</nuxt-link-locale>
|
||||||
|
<nuxt-link-locale
|
||||||
|
class="nav__item"
|
||||||
|
:class="[{ active: route.path.includes('wishlist') }]"
|
||||||
|
to="/profile/wishlist"
|
||||||
|
>
|
||||||
|
<icon name="mdi:cards-heart-outline" size="20" />
|
||||||
|
{{ t('profile.wishlist.title') }}
|
||||||
|
</nuxt-link-locale>
|
||||||
|
<nuxt-link-locale
|
||||||
|
class="nav__item"
|
||||||
|
:class="[{ active: route.path.includes('cart') }]"
|
||||||
|
to="/profile/cart"
|
||||||
|
>
|
||||||
|
<icon name="ph:shopping-cart-light" size="20" />
|
||||||
|
{{ t('profile.cart.title') }}
|
||||||
|
</nuxt-link-locale>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const {t} = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nav {
|
||||||
|
background-color: $white;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
padding-block: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
position: sticky;
|
||||||
|
top: 141px;
|
||||||
|
width: max-content;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 30px 5px 20px;
|
||||||
|
border-left: 2px solid $white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
color: $accent;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
color: $accentDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: $accent;
|
||||||
|
color: $accentDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="filter__wrapper" ref="filtersRef">
|
<div class="filter__wrapper" ref="filtersRef">
|
||||||
<div class="filter__top">
|
<div class="filter__top">
|
||||||
<h2>{{ t('store.filters.title') }}</h2>
|
<h2>{{ t('store.filters.title') }}</h2>
|
||||||
<Icon
|
<icon
|
||||||
name="line-md:close"
|
name="line-md:close"
|
||||||
size="30"
|
size="30"
|
||||||
@click="closeFilters"
|
@click="closeFilters"
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<template #title="{ isActive }">
|
<template #title="{ isActive }">
|
||||||
<div :class="['filter__collapse-title', { 'is-active': isActive }]">
|
<div :class="['filter__collapse-title', { 'is-active': isActive }]">
|
||||||
{{ attribute.attributeName }}
|
{{ attribute.attributeName }}
|
||||||
<Icon
|
<icon
|
||||||
name="material-symbols:keyboard-arrow-down"
|
name="material-symbols:keyboard-arrow-down"
|
||||||
size="22"
|
size="22"
|
||||||
class="filter__collapse-icon"
|
class="filter__collapse-icon"
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
:id="attribute.attributeName + '-all'"
|
:id="attribute.attributeName + '-all'"
|
||||||
v-model="selectedAllMap[attribute.attributeName]"
|
v-model="selectedAllMap[attribute.attributeName]"
|
||||||
@change="toggleAll(attribute.attributeName)"
|
@change="toggleAll(attribute.attributeName)"
|
||||||
:isFilter="true"
|
:isAccent="true"
|
||||||
>
|
>
|
||||||
{{ t('store.filters.all') }}
|
{{ t('store.filters.all') }}
|
||||||
</ui-checkbox>
|
</ui-checkbox>
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:id="attribute.attributeName + idx"
|
:id="attribute.attributeName + idx"
|
||||||
v-model="selectedMap[attribute.attributeName][value]"
|
v-model="selectedMap[attribute.attributeName][value]"
|
||||||
:isFilter="true"
|
:isAccent="true"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</ui-checkbox>
|
</ui-checkbox>
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {IStoreFilters} from "~/types";
|
import type {IStoreFilters} from "~/types";
|
||||||
import {useFilters} from "~/composables/store";
|
import {useFilters} from "~/composables/store";
|
||||||
|
import {useRouteQuery} from "@vueuse/router";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -81,7 +82,17 @@ const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { selectedMap, selectedAllMap, collapse, toggleAll, resetFilters, applyFilters } = useFilters(
|
const attributesQuery = useRouteQuery<string>('attributes', '');
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedMap,
|
||||||
|
selectedAllMap,
|
||||||
|
collapse,
|
||||||
|
toggleAll,
|
||||||
|
resetFilters,
|
||||||
|
applyFilters,
|
||||||
|
parseAttributesString
|
||||||
|
} = useFilters(
|
||||||
toRef(props, 'filterableAttributes')
|
toRef(props, 'filterableAttributes')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -110,6 +121,27 @@ watch(
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => attributesQuery.value,
|
||||||
|
(attrStr) => {
|
||||||
|
resetFilters();
|
||||||
|
if (!attrStr) return;
|
||||||
|
|
||||||
|
const initial = parseAttributesString(attrStr);
|
||||||
|
Object.entries(initial).forEach(([key, vals]) => {
|
||||||
|
vals.forEach(val => {
|
||||||
|
if (selectedMap[key] && selectedMap[key][val] !== undefined) {
|
||||||
|
selectedMap[key][val] = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (selectedMap[key]) {
|
||||||
|
selectedAllMap[key] = Object.values(selectedMap[key]).every(v => v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
v-for="product in products"
|
v-for="product in products"
|
||||||
:key="product.node.uuid"
|
:key="product.node.uuid"
|
||||||
:product="product.node"
|
:product="product.node"
|
||||||
|
:isList="productView === 'list'"
|
||||||
/>
|
/>
|
||||||
<skeletons-cards-product
|
<skeletons-cards-product
|
||||||
v-if="pending"
|
v-if="pending"
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useFilters, useStore} from "~/composables/store";
|
import {useFilters, useStore} from "~/composables/store";
|
||||||
import {useRouteQuery} from "@vueuse/router";
|
import {useRouteQuery} from "@vueuse/router";
|
||||||
|
import {useRouteParams} from "@vueuse/router";
|
||||||
import {useCategoryBySlug} from "~/composables/categories";
|
import {useCategoryBySlug} from "~/composables/categories";
|
||||||
import {useAppConfig} from '~/composables/config';
|
import {useAppConfig} from '~/composables/config';
|
||||||
|
|
||||||
|
|
@ -69,7 +71,7 @@ watch(
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { pending, products, pageInfo, prodVars } = await useStore(
|
const { pending, products, pageInfo, variables } = await useStore(
|
||||||
slug.value,
|
slug.value,
|
||||||
attributes.value,
|
attributes.value,
|
||||||
orderBy.value,
|
orderBy.value,
|
||||||
|
|
@ -86,29 +88,31 @@ function onFilterToggle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFiltersChange(newFilters: Record<string, string[]>) {
|
function onFiltersChange(newFilters: Record<string, string[]>) {
|
||||||
attributes.value = buildAttributesString(newFilters);
|
const attrString = buildAttributesString(newFilters);
|
||||||
|
attributes.value = attrString;
|
||||||
|
variables.attributes = attrString;
|
||||||
}
|
}
|
||||||
|
|
||||||
useIntersectionObserver(
|
useIntersectionObserver(
|
||||||
observer,
|
observer,
|
||||||
async ([{ isIntersecting }]) => {
|
async ([{ isIntersecting }]) => {
|
||||||
if (isIntersecting && pageInfo.value?.hasNextPage) {
|
if (isIntersecting && pageInfo.value?.hasNextPage && !pending.value) {
|
||||||
prodVars.productAfter = pageInfo.value.endCursor;
|
variables.productAfter = pageInfo.value.endCursor;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(orderBy, newVal => {
|
watch(orderBy, newVal => {
|
||||||
prodVars.orderBy = newVal || '';
|
variables.orderBy = newVal || '';
|
||||||
});
|
});
|
||||||
watch(attributes, newVal => {
|
watch(attributes, newVal => {
|
||||||
prodVars.attributes = newVal || '';
|
variables.attributes = newVal;
|
||||||
});
|
});
|
||||||
watch(minPrice, newVal => {
|
watch(minPrice, newVal => {
|
||||||
prodVars.minPrice = newVal || 0;
|
variables.minPrice = newVal || 0;
|
||||||
});
|
});
|
||||||
watch(maxPrice, newVal => {
|
watch(maxPrice, newVal => {
|
||||||
prodVars.maxPrice = newVal || 500000;
|
variables.maxPrice = newVal || 500000;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@
|
||||||
:class="{ active: productView === 'list' }"
|
:class="{ active: productView === 'list' }"
|
||||||
@click="setView('list')"
|
@click="setView('list')"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:view-list-sharp" size="16" />
|
<icon name="material-symbols:view-list-sharp" size="16" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="top__view-button"
|
class="top__view-button"
|
||||||
:class="{ active: productView === 'grid' }"
|
:class="{ active: productView === 'grid' }"
|
||||||
@click="setView('grid')"
|
@click="setView('grid')"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:grid-view" size="16" />
|
<icon name="material-symbols:grid-view" size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
@click="$emit('toggle-filter')"
|
@click="$emit('toggle-filter')"
|
||||||
>
|
>
|
||||||
{{ t('store.filters.title') }}
|
{{ t('store.filters.title') }}
|
||||||
<Icon name="line-md:filter" size="16" />
|
<icon name="line-md:filter" size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,7 +95,7 @@ watch(select, value => {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const { breadcrumbs } = useBreadcrumbs()
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
padding: 15px 250px 15px 50px;
|
padding: 15px 250px 15px 50px;
|
||||||
|
line-height: 140%;
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="checkbox" :class="{ isFilter }">
|
<label class="checkbox" :class="{ isAccent }">
|
||||||
<input
|
<input
|
||||||
:id="id"
|
:id="id"
|
||||||
class="checkbox__input"
|
class="checkbox__input"
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: string,
|
id?: string,
|
||||||
modelValue: boolean,
|
modelValue: boolean,
|
||||||
isFilter: boolean
|
isAccent?: boolean
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', v: boolean): void
|
(e: 'update:modelValue', v: boolean): void
|
||||||
|
|
@ -37,14 +37,15 @@ function onChange(e: Event) {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.isFilter {
|
&.isAccent {
|
||||||
& .checkbox__block {
|
& .checkbox__block {
|
||||||
border: 2px solid $accent;
|
border: 2px solid $accentDark;
|
||||||
border-radius: $default_border_radius;
|
border-radius: $default_border_radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .checkbox__label {
|
& .checkbox__label {
|
||||||
color: $accent;
|
color: $accentDark;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="counter">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.counter {
|
|
||||||
position: absolute !important;
|
|
||||||
top: -10px;
|
|
||||||
right: -15px;
|
|
||||||
background-color: $accent;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 20px;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -10,74 +10,75 @@
|
||||||
class="block__input"
|
class="block__input"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click.prevent="setPasswordVisible"
|
@click.prevent="togglePasswordVisible"
|
||||||
class="block__eyes"
|
class="block__eyes"
|
||||||
v-if="type === 'password' && modelValue"
|
v-if="type === 'password' && String(modelValue).length > 0"
|
||||||
>
|
>
|
||||||
<Icon v-if="isPasswordVisible === 'password'" name="mdi:eye-off-outline" />
|
<icon v-if="isPasswordVisible === 'password'" name="mdi:eye-off-outline" />
|
||||||
<Icon v-else name="mdi:eye-outline" />
|
<icon v-else name="mdi:eye-outline" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
<p v-if="!isValid" class="block__error">{{ errorMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const $emit = defineEmits();
|
type Rule = (value: string) => boolean | string;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | number): void;
|
||||||
|
}>();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string,
|
type: string,
|
||||||
placeholder: string,
|
placeholder: string,
|
||||||
isError?: boolean,
|
|
||||||
error?: string,
|
|
||||||
modelValue?: [string, number],
|
modelValue?: [string, number],
|
||||||
rules?: array,
|
rules?: Rule[],
|
||||||
numberOnly: boolean
|
numberOnly?: boolean
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isPasswordVisible = ref<string>(props.type);
|
const isPasswordVisible = ref(props.type);
|
||||||
const setPasswordVisible = () => {
|
const isValid = ref(true);
|
||||||
if (isPasswordVisible.value === 'password') {
|
const errorMessage = ref('');
|
||||||
isPasswordVisible.value = 'text';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isPasswordVisible.value = 'password';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onlyNumbersKeydown = (event) => {
|
function togglePasswordVisible() {
|
||||||
|
isPasswordVisible.value =
|
||||||
|
isPasswordVisible.value === 'password' ? 'text' : 'password';
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlyNumbersKeydown = (event: KeyboardEvent) => {
|
||||||
if (!/^\d$/.test(event.key) &&
|
if (!/^\d$/.test(event.key) &&
|
||||||
!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Home', 'End'].includes(event.key)) {
|
!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Home', 'End'].includes(event.key)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = ref<boolean>(true);
|
function onInput(e: Event) {
|
||||||
const errorMessage = ref<string>('');
|
const target = e.target as HTMLInputElement;
|
||||||
const onInput = (e: Event) => {
|
let value = target.value;
|
||||||
let value = e.target.value;
|
|
||||||
|
|
||||||
if (props.numberOnly) {
|
if (props.numberOnly) {
|
||||||
const newValue = value.replace(/\D/g, '');
|
const digitsOnly = value.replace(/\D/g, '');
|
||||||
if (newValue !== value) {
|
if (digitsOnly !== value) {
|
||||||
e.target.value = newValue;
|
target.value = digitsOnly;
|
||||||
value = newValue;
|
value = digitsOnly;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = true;
|
let valid = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
|
||||||
props.rules?.forEach((rule) => {
|
props.rules?.forEach((rule) => {
|
||||||
result = rule((e.target).value);
|
const result = rule(value);
|
||||||
|
if (result !== true) {
|
||||||
if (!result) {
|
valid = false;
|
||||||
errorMessage.value = String(result);
|
errorMessage.value = String(result);
|
||||||
result = false;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validate.value = result;
|
isValid.value = valid;
|
||||||
|
|
||||||
return $emit('update:modelValue', (e.target).value);
|
emit('update:modelValue', props.numberOnly ? Number(value) : value);
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
@ -98,7 +99,6 @@ const onInput = (e: Event) => {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
//border: 1px solid #b2b2b2;
|
|
||||||
border-radius: $default_border_radius;
|
border-radius: $default_border_radius;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,38 +6,42 @@
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
class="block__textarea"
|
class="block__textarea"
|
||||||
/>
|
/>
|
||||||
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
<p v-if="!isValid" class="block__error">{{ errorMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const $emit = defineEmits();
|
type Rule = (value: string) => boolean | string;
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | number): void;
|
||||||
|
}>();
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
placeholder: string,
|
placeholder: string,
|
||||||
isError?: boolean,
|
modelValue?: string,
|
||||||
error?: string,
|
rules?: Rule[]
|
||||||
modelValue?: [string, number],
|
|
||||||
rules?: array
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const validate = ref<boolean>(true)
|
const isValid = ref(true);
|
||||||
const errorMessage = ref<string>('')
|
const errorMessage = ref('');
|
||||||
|
|
||||||
const onInput = (e: Event) => {
|
const onInput = (e: Event) => {
|
||||||
let result = true
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
const value = target.value;
|
||||||
|
|
||||||
props.rules?.forEach((rule) => {
|
isValid.value = true;
|
||||||
result = rule((e.target).value)
|
errorMessage.value = '';
|
||||||
|
|
||||||
if (!result) {
|
props.rules?.forEach(rule => {
|
||||||
errorMessage.value = String(result)
|
const result = rule(value);
|
||||||
result = false
|
if (result !== true) {
|
||||||
|
isValid.value = false;
|
||||||
|
errorMessage.value = String(result);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
validate.value = result
|
emit('update:modelValue', value);
|
||||||
|
};
|
||||||
return $emit('update:modelValue', (e.target).value)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * from './useLogin'
|
export * from './useLogin';
|
||||||
export * from './useRefresh'
|
export * from './useRefresh';
|
||||||
export * from './useRegister'
|
export * from './useRegister';
|
||||||
export * from './useLogout'
|
export * from './useLogout';
|
||||||
export * from './usePasswordReset'
|
export * from './usePasswordReset';
|
||||||
export * from './useNewPassword'
|
export * from './useNewPassword';
|
||||||
|
|
@ -8,6 +8,7 @@ import { usePendingOrder } from '~/composables/orders';
|
||||||
import { useUserStore } from '~/stores/user';
|
import { useUserStore } from '~/stores/user';
|
||||||
import { useAppStore } from '~/stores/app';
|
import { useAppStore } from '~/stores/app';
|
||||||
import {DEFAULT_LOCALE} from "~/config/constants";
|
import {DEFAULT_LOCALE} from "~/config/constants";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -57,7 +58,10 @@ export function useLogin() {
|
||||||
userStore.setUser(authData.user);
|
userStore.setUser(authData.user);
|
||||||
cookieAccess.value = authData.accessToken
|
cookieAccess.value = authData.accessToken
|
||||||
|
|
||||||
ElNotification({ message: t('popup.success.login'), type: 'success' });
|
useNotification(
|
||||||
|
t('popup.success.login'),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
if (authData.user.language !== cookieLocale.value) {
|
if (authData.user.language !== cookieLocale.value) {
|
||||||
await checkAndRedirect(authData.user.language);
|
await checkAndRedirect(authData.user.language);
|
||||||
|
|
@ -78,11 +82,11 @@ export function useLogin() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {NEW_PASSWORD} from "@/graphql/mutations/auth.js";
|
||||||
import {isGraphQLError} from "~/utils/error";
|
import {isGraphQLError} from "~/utils/error";
|
||||||
import type {INewPasswordResponse} from "~/types";
|
import type {INewPasswordResponse} from "~/types";
|
||||||
import { useRouteQuery } from '@vueuse/router';
|
import { useRouteQuery } from '@vueuse/router';
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useNewPassword() {
|
export function useNewPassword() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
@ -25,10 +26,10 @@ export function useNewPassword() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.data?.confirmResetPassword.success) {
|
if (result?.data?.confirmResetPassword.success) {
|
||||||
ElNotification({
|
useNotification(
|
||||||
message: t('popup.success.newPassword'),
|
t('popup.success.newPassword'),
|
||||||
type: 'success'
|
'success'
|
||||||
})
|
);
|
||||||
|
|
||||||
await router.push({path: '/'})
|
await router.push({path: '/'})
|
||||||
|
|
||||||
|
|
@ -45,11 +46,11 @@ export function useNewPassword() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {RESET_PASSWORD} from "@/graphql/mutations/auth.js";
|
import {RESET_PASSWORD} from "@/graphql/mutations/auth.js";
|
||||||
import {isGraphQLError} from "~/utils/error";
|
import {isGraphQLError} from "~/utils/error";
|
||||||
import type {IPasswordResetResponse} from "~/types";
|
import type {IPasswordResetResponse} from "~/types";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function usePasswordReset() {
|
export function usePasswordReset() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
@ -16,10 +17,10 @@ export function usePasswordReset() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.data?.resetPassword.success) {
|
if (result?.data?.resetPassword.success) {
|
||||||
ElNotification({
|
useNotification(
|
||||||
message: t('popup.success.reset'),
|
t('popup.success.reset'),
|
||||||
type: 'success'
|
'success'
|
||||||
})
|
);
|
||||||
|
|
||||||
appStore.unsetActiveState();
|
appStore.unsetActiveState();
|
||||||
}
|
}
|
||||||
|
|
@ -34,11 +35,11 @@ export function usePasswordReset() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { usePendingOrder } from '~/composables/orders';
|
||||||
import { useUserStore } from '~/stores/user';
|
import { useUserStore } from '~/stores/user';
|
||||||
import { isGraphQLError } from '~/utils/error';
|
import { isGraphQLError } from '~/utils/error';
|
||||||
import {DEFAULT_LOCALE} from "~/config/constants";
|
import {DEFAULT_LOCALE} from "~/config/constants";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useRefresh() {
|
export function useRefresh() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -62,11 +63,11 @@ export function useRefresh() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {REGISTER} from "@/graphql/mutations/auth.js";
|
||||||
import {useMailClient} from "@/composables/utils";
|
import {useMailClient} from "@/composables/utils";
|
||||||
import {isGraphQLError} from "~/utils/error";
|
import {isGraphQLError} from "~/utils/error";
|
||||||
import type {IRegisterResponse} from "~/types";
|
import type {IRegisterResponse} from "~/types";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useRegister() {
|
export function useRegister() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
@ -31,48 +32,41 @@ export function useRegister() {
|
||||||
if (result?.data?.createUser?.success) {
|
if (result?.data?.createUser?.success) {
|
||||||
detectMailClient(email);
|
detectMailClient(email);
|
||||||
|
|
||||||
ElNotification({
|
useNotification(
|
||||||
message: h('div', [
|
h('div', [
|
||||||
h('p', t('popup.success.register')),
|
h('p', t('popup.success.register')),
|
||||||
mailClientUrl.value ? h(
|
mailClientUrl.value ? h(
|
||||||
'button',
|
'button',
|
||||||
{
|
{
|
||||||
style: {
|
class: 'el-notification__button',
|
||||||
marginTop: '10px',
|
onClick: () => {
|
||||||
padding: '6px 12px',
|
openMailClient()
|
||||||
backgroundColor: '#000000',
|
}
|
||||||
color: '#fff',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
},
|
},
|
||||||
onClick: () => {
|
t('buttons.goEmail')
|
||||||
openMailClient()
|
) : ''
|
||||||
}
|
]),
|
||||||
},
|
'success'
|
||||||
t('buttons.goEmail')
|
);
|
||||||
) : ''
|
|
||||||
]),
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
appStore.unsetActiveState();
|
appStore.unsetActiveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(error, (err) => {
|
watch(error, (err) => {
|
||||||
if (!err) return
|
if (!err) return;
|
||||||
console.error('useRegister error:', err)
|
console.error('useRegister error:', err);
|
||||||
let message = t('popup.errors.defaultError')
|
let message = t('popup.errors.defaultError');
|
||||||
if (isGraphQLError(err)) {
|
if (isGraphQLError(err)) {
|
||||||
message = err.graphQLErrors?.[0]?.message || message
|
message = err.graphQLErrors?.[0]?.message || message;
|
||||||
} else {
|
} else {
|
||||||
message = err.message
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
})
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './useBrands'
|
export * from './useBrands';
|
||||||
export * from './useBrandByUuid'
|
export * from './useBrandByUuid';
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './useBreadcrumbs'
|
export * from './useBreadcrumbs';
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export * from './useCategories'
|
export * from './useCategories';
|
||||||
export * from './useCategoryTags'
|
export * from './useCategoryTags';
|
||||||
export * from './useCategoryBySlug'
|
export * from './useCategoryBySlug';
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './useCompanyInfo'
|
export * from './useCompanyInfo';
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './useAppConfig'
|
export * from './useAppConfig';
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './useContactUs'
|
export * from './useContactUs';
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {isGraphQLError} from "~/utils/error";
|
import {isGraphQLError} from "~/utils/error";
|
||||||
import type {IContactUsResponse} from "~/types";
|
import type {IContactUsResponse} from "~/types";
|
||||||
import {CONTACT_US} from "~/graphql/mutations/contact";
|
import {CONTACT_US} from "~/graphql/mutations/contact";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useContactUs() {
|
export function useContactUs() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
@ -23,10 +24,10 @@ export function useContactUs() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.data?.contactUs.received) {
|
if (result?.data?.contactUs.received) {
|
||||||
ElNotification({
|
useNotification(
|
||||||
message: t('popup.success.contactUs'),
|
t('popup.success.contactUs'),
|
||||||
type: 'success'
|
'success'
|
||||||
})
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,11 +40,11 @@ export function useContactUs() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export * from './useLocaleRedirect'
|
export * from './useLocaleRedirect';
|
||||||
export * from './useLanguage'
|
export * from './useLanguage';
|
||||||
export * from './useLanguageSwitch'
|
export * from './useLanguageSwitch';
|
||||||
1
storefront/composables/notification/index.ts
Normal file
1
storefront/composables/notification/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './useNotification';
|
||||||
26
storefront/composables/notification/useNotification.ts
Normal file
26
storefront/composables/notification/useNotification.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
export function useNotification(
|
||||||
|
message: string,
|
||||||
|
type: string,
|
||||||
|
title?: string
|
||||||
|
) {
|
||||||
|
const duration = 5000;
|
||||||
|
|
||||||
|
const createProgressBar = (duration: number, message: string) => {
|
||||||
|
return h('div', [
|
||||||
|
h('p', message),
|
||||||
|
h('div', {
|
||||||
|
class: 'el-notification__progress',
|
||||||
|
style: {
|
||||||
|
animationDuration: `${duration}ms`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
ElNotification({
|
||||||
|
title: title,
|
||||||
|
duration,
|
||||||
|
message: createProgressBar(duration, message),
|
||||||
|
type: type
|
||||||
|
} as import('element-plus').NotificationOptions);
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './usePendingOrder'
|
export * from './usePendingOrder';
|
||||||
176
storefront/composables/orders/useOrderOverwrite.ts
Normal file
176
storefront/composables/orders/useOrderOverwrite.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
import type {
|
||||||
|
IAddToOrderResponse,
|
||||||
|
IBulkOrderResponse, IRemoveAllFromOrderResponse,
|
||||||
|
IRemoveFromOrderResponse, IRemoveKindFromOrderResponse,
|
||||||
|
} from "~/types";
|
||||||
|
import {
|
||||||
|
ADD_TO_CART,
|
||||||
|
BULK_CART,
|
||||||
|
REMOVE_ALL_FROM_CART,
|
||||||
|
REMOVE_FROM_CART,
|
||||||
|
REMOVE_KIND_FROM_CART
|
||||||
|
} from "~/graphql/mutations/cart";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
import {isGraphQLError} from "~/utils/error";
|
||||||
|
|
||||||
|
interface IOverwriteOrderArguments {
|
||||||
|
type: string,
|
||||||
|
productUuid?: string,
|
||||||
|
productName?: string,
|
||||||
|
bulkAction?: string,
|
||||||
|
products?: {
|
||||||
|
uuid: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrderOverwrite () {
|
||||||
|
const {t} = useI18n();
|
||||||
|
const cartStore = useCartStore();
|
||||||
|
|
||||||
|
const orderUuid = computed(() => cartStore.currentOrder?.uuid);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: addMutate,
|
||||||
|
loading: addLoading,
|
||||||
|
error: addError
|
||||||
|
} = useMutation<IAddToOrderResponse>(ADD_TO_CART);
|
||||||
|
const {
|
||||||
|
mutate: removeMutate,
|
||||||
|
loading: removeLoading,
|
||||||
|
error: removedError
|
||||||
|
} = useMutation<IRemoveFromOrderResponse>(REMOVE_FROM_CART);
|
||||||
|
const {
|
||||||
|
mutate: removeKindMutate,
|
||||||
|
loading: removeKindLoading,
|
||||||
|
error: removedKindError
|
||||||
|
} = useMutation<IRemoveKindFromOrderResponse>(REMOVE_KIND_FROM_CART);
|
||||||
|
const {
|
||||||
|
mutate: removeAllMutate,
|
||||||
|
loading: removeAllLoading,
|
||||||
|
error: removedAllError
|
||||||
|
} = useMutation<IRemoveAllFromOrderResponse>(REMOVE_ALL_FROM_CART);
|
||||||
|
const {
|
||||||
|
mutate: bulkMutate,
|
||||||
|
loading: bulkLoading,
|
||||||
|
error: bulkError
|
||||||
|
} = useMutation<IBulkOrderResponse>(BULK_CART);
|
||||||
|
|
||||||
|
async function overwriteOrder (
|
||||||
|
args: IOverwriteOrderArguments
|
||||||
|
) {
|
||||||
|
switch (args.type) {
|
||||||
|
case "add":
|
||||||
|
const addResult = await addMutate({
|
||||||
|
orderUuid: orderUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addResult?.data?.addOrderProduct?.order) {
|
||||||
|
cartStore.setCurrentOrders(addResult.data.addOrderProduct.order);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.addToCart', { product: args.productName }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "remove":
|
||||||
|
const removeResult = await removeMutate({
|
||||||
|
orderUuid: orderUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeResult?.data?.removeOrderProduct?.order) {
|
||||||
|
cartStore.setCurrentOrders(removeResult.data.removeOrderProduct.order);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.removeFromCart', { product: args.productName }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "removeKind":
|
||||||
|
const removeKindResult = await removeKindMutate({
|
||||||
|
orderUuid: orderUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeKindResult?.data?.removeOrderProductsOfAKind?.order) {
|
||||||
|
cartStore.setCurrentOrders(removeKindResult.data.removeOrderProductsOfAKind.order);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.removeFromCart', { product: args.productName }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "removeAll":
|
||||||
|
const removeAllResult = await removeAllMutate({
|
||||||
|
orderUuid: orderUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeAllResult?.data?.removeAllOrderProducts?.order) {
|
||||||
|
cartStore.setCurrentOrders(removeAllResult.data.removeAllOrderProducts.order);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.removeAllFromCart', { product: args.productName }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bulk":
|
||||||
|
const bulkResult = await bulkMutate({
|
||||||
|
orderUuid: orderUuid.value,
|
||||||
|
action: args.bulkAction,
|
||||||
|
products: args.products
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bulkResult?.data?.bulkOrderAction?.order) {
|
||||||
|
cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order);
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.bulkRemoveWishlist'),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error('No type provided for overwriteOrder');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(addError || removedError || removedKindError || removedAllError || bulkError, (err) => {
|
||||||
|
if (!err) return;
|
||||||
|
console.error('useOrderOverwrite error:', err);
|
||||||
|
let message = t('popup.errors.defaultError');
|
||||||
|
if (isGraphQLError(err)) {
|
||||||
|
message = err.graphQLErrors?.[0]?.message || message;
|
||||||
|
} else {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
useNotification(
|
||||||
|
message,
|
||||||
|
'error',
|
||||||
|
t('popup.errors.main')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return{
|
||||||
|
addLoading,
|
||||||
|
removeLoading,
|
||||||
|
removeKindLoading,
|
||||||
|
removeAllLoading,
|
||||||
|
bulkLoading,
|
||||||
|
overwriteOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export * from './useProducts'
|
export * from './useProducts';
|
||||||
export * from './useProductBySlug'
|
export * from './useProductBySlug';
|
||||||
export * from './useProductTags'
|
export * from './useProductTags';
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
import { GET_PRODUCT_BY_SLUG } from '~/graphql/queries/standalone/products'
|
import { GET_PRODUCT_BY_SLUG } from '~/graphql/queries/standalone/products';
|
||||||
import type { IProduct, IProductResponse } from '~/types'
|
import type { IProduct, IProductResponse } from '~/types';
|
||||||
|
|
||||||
export async function useProductBySlug(slug: string) {
|
export async function useProductBySlug(slug: string) {
|
||||||
const product = useState<IProduct | null>('currentProduct', () => null)
|
const product = useState<IProduct | null>('currentProduct', () => null);
|
||||||
|
|
||||||
const { data, error } = await useAsyncQuery<IProductResponse>(
|
const { data, error } = await useAsyncQuery<IProductResponse>(
|
||||||
GET_PRODUCT_BY_SLUG,
|
GET_PRODUCT_BY_SLUG,
|
||||||
{ slug }
|
{ slug }
|
||||||
)
|
);
|
||||||
|
|
||||||
const result = data.value?.products?.edges[0]?.node ?? null
|
const result = data.value?.products?.edges[0]?.node ?? null;
|
||||||
if (result) {
|
if (result) {
|
||||||
product.value = result
|
product.value = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(error, (err) => {
|
watch(error, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('useProductBySlug error:', err)
|
console.error('useProductBySlug error:', err);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
product
|
product
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './useFormValidation'
|
export * from './useFormValidation';
|
||||||
1
storefront/composables/scrollTo/index.ts
Normal file
1
storefront/composables/scrollTo/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './useScrollTo';
|
||||||
33
storefront/composables/scrollTo/useScrollTo.ts
Normal file
33
storefront/composables/scrollTo/useScrollTo.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export function useScrollTo () {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function scroll (id: string) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
console.error('Element not found:', id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollTo (
|
||||||
|
id: string,
|
||||||
|
routePath?: string
|
||||||
|
) {
|
||||||
|
if (routePath) {
|
||||||
|
router.push({
|
||||||
|
path: routePath
|
||||||
|
}).then(() => {
|
||||||
|
scroll(id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
scroll(id);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export * from './useSearch'
|
export * from './useSearch';
|
||||||
export * from './useSearchCombined'
|
export * from './useSearchCombined';
|
||||||
export * from './useSearchUi'
|
export * from './useSearchUi';
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {SEARCH} from "~/graphql/mutations/search";
|
import {SEARCH} from "~/graphql/mutations/search";
|
||||||
import type {ISearchResponse, ISearchResults} from "~/types";
|
import type {ISearchResponse, ISearchResults} from "~/types";
|
||||||
import {isGraphQLError} from "~/utils/error";
|
import {isGraphQLError} from "~/utils/error";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useSearch() {
|
export function useSearch() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
@ -37,11 +38,11 @@ export function useSearch() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './useStore'
|
export * from './useStore';
|
||||||
export * from './useFilters'
|
export * from './useFilters';
|
||||||
|
|
@ -79,6 +79,29 @@ export function useFilters(filterableAttributes: Ref<IStoreFilters[]>) {
|
||||||
.join(';');
|
.join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAttributesString(str: string): Record<string, string[]> {
|
||||||
|
const result: Record<string, string[]> = {};
|
||||||
|
if (!str) return result;
|
||||||
|
|
||||||
|
str.split(';').forEach(entry => {
|
||||||
|
const [name, expr] = entry.split('=');
|
||||||
|
if (!name || !expr) return;
|
||||||
|
|
||||||
|
if (expr.startsWith('in-')) {
|
||||||
|
try {
|
||||||
|
result[name] = JSON.parse(expr.slice(3));
|
||||||
|
} catch {
|
||||||
|
result[name] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (expr.startsWith('icontains-')) {
|
||||||
|
result[name] = [expr.slice('icontains-'.length)];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedMap,
|
selectedMap,
|
||||||
selectedAllMap,
|
selectedAllMap,
|
||||||
|
|
@ -86,6 +109,7 @@ export function useFilters(filterableAttributes: Ref<IStoreFilters[]>) {
|
||||||
toggleAll,
|
toggleAll,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
applyFilters,
|
applyFilters,
|
||||||
buildAttributesString
|
buildAttributesString,
|
||||||
|
parseAttributesString
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ export async function useStore(
|
||||||
maxPrice?: number,
|
maxPrice?: number,
|
||||||
productAfter?: string
|
productAfter?: string
|
||||||
) {
|
) {
|
||||||
const prodVars = reactive<ProdVars>({
|
const variables = reactive<ProdVars>({
|
||||||
first: 15,
|
first: 15,
|
||||||
categoriesSlugs: slug,
|
categoriesSlugs: slug,
|
||||||
attributes,
|
attributes,
|
||||||
|
|
@ -29,7 +29,10 @@ export async function useStore(
|
||||||
productAfter
|
productAfter
|
||||||
});
|
});
|
||||||
|
|
||||||
const { pending, data, error, refresh } = await useAsyncQuery<IProductResponse>(GET_PRODUCTS, prodVars);
|
const { pending, data, error, refresh } = await useAsyncQuery<IProductResponse>(
|
||||||
|
GET_PRODUCTS,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
const products = ref(data.value?.products.edges ?? []);
|
const products = ref(data.value?.products.edges ?? []);
|
||||||
const pageInfo = computed(() => data.value?.products.pageInfo ?? null);
|
const pageInfo = computed(() => data.value?.products.pageInfo ?? null);
|
||||||
|
|
@ -37,7 +40,7 @@ export async function useStore(
|
||||||
watch(error, e => e && console.error('useStore products error', e));
|
watch(error, e => e && console.error('useStore products error', e));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => prodVars.productAfter,
|
() => variables.productAfter,
|
||||||
async (newCursor, oldCursor) => {
|
async (newCursor, oldCursor) => {
|
||||||
if (!newCursor || newCursor === oldCursor) return;
|
if (!newCursor || newCursor === oldCursor) return;
|
||||||
await refresh();
|
await refresh();
|
||||||
|
|
@ -48,13 +51,13 @@ export async function useStore(
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
() => prodVars.attributes,
|
() => variables.attributes,
|
||||||
() => prodVars.orderBy,
|
() => variables.orderBy,
|
||||||
() => prodVars.minPrice,
|
() => variables.minPrice,
|
||||||
() => prodVars.maxPrice
|
() => variables.maxPrice
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
prodVars.productAfter = '';
|
variables.productAfter = '';
|
||||||
await refresh();
|
await refresh();
|
||||||
products.value = data.value?.products.edges ?? [];
|
products.value = data.value?.products.edges ?? [];
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +67,6 @@ export async function useStore(
|
||||||
pending,
|
pending,
|
||||||
products,
|
products,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
prodVars
|
variables
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from './useUserActivation'
|
export * from './useUserActivation';
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {ACTIVATE_USER} from "@/graphql/mutations/user.js";
|
import {ACTIVATE_USER} from "@/graphql/mutations/user.js";
|
||||||
import {isGraphQLError} from "~/utils/error";
|
import {isGraphQLError} from "~/utils/error";
|
||||||
import type {IUserActivationResponse} from "~/types";
|
import type {IUserActivationResponse} from "~/types";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
export function useUserActivation() {
|
export function useUserActivation() {
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
|
|
@ -14,10 +15,10 @@ export function useUserActivation() {
|
||||||
const result = await mutate({ token, uid });
|
const result = await mutate({ token, uid });
|
||||||
|
|
||||||
if (result?.data?.activateUser) {
|
if (result?.data?.activateUser) {
|
||||||
ElNotification({
|
useNotification(
|
||||||
message: t("popup.activationSuccess"),
|
t("popup.activationSuccess"),
|
||||||
type: "success"
|
'success'
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,11 +31,11 @@ export function useUserActivation() {
|
||||||
} else {
|
} else {
|
||||||
message = err.message;
|
message = err.message;
|
||||||
}
|
}
|
||||||
ElNotification({
|
useNotification(
|
||||||
title: t('popup.errors.main'),
|
message,
|
||||||
message,
|
'error',
|
||||||
type: 'error'
|
t('popup.errors.main')
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './useMailClient'
|
export * from './useMailClient';
|
||||||
export * from './usePageTitle'
|
export * from './usePageTitle';
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './useWishlist'
|
export * from './useWishlist';
|
||||||
|
export * from './useWishlistOverwrite';
|
||||||
152
storefront/composables/wishlist/useWishlistOverwrite.ts
Normal file
152
storefront/composables/wishlist/useWishlistOverwrite.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import type {
|
||||||
|
IAddToWishlistResponse, IBulkWishlistResponse,
|
||||||
|
IRemoveAllFromWishlistResponse,
|
||||||
|
IRemoveFromWishlistResponse
|
||||||
|
} from "~/types";
|
||||||
|
import {
|
||||||
|
ADD_TO_WISHLIST,
|
||||||
|
BULK_WISHLIST,
|
||||||
|
REMOVE_ALL_FROM_WISHLIST,
|
||||||
|
REMOVE_FROM_WISHLIST
|
||||||
|
} from "~/graphql/mutations/wishlist";
|
||||||
|
import {isGraphQLError} from "~/utils/error";
|
||||||
|
import {useNotification} from "~/composables/notification";
|
||||||
|
|
||||||
|
interface IOverwriteWishlistArguments {
|
||||||
|
type: string,
|
||||||
|
productUuid?: string,
|
||||||
|
productName?: string,
|
||||||
|
bulkAction?: string,
|
||||||
|
products?: {
|
||||||
|
uuid: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWishlistOverwrite() {
|
||||||
|
const {t} = useI18n();
|
||||||
|
const wishlistStore = useWishlistStore();
|
||||||
|
|
||||||
|
const wishlistUuid = computed(() => wishlistStore.wishlist?.uuid);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: addMutate,
|
||||||
|
loading: addLoading,
|
||||||
|
error: addError
|
||||||
|
} = useMutation<IAddToWishlistResponse>(ADD_TO_WISHLIST);
|
||||||
|
const {
|
||||||
|
mutate: removeMutate,
|
||||||
|
loading: removeLoading,
|
||||||
|
error: removedError
|
||||||
|
} = useMutation<IRemoveFromWishlistResponse>(REMOVE_FROM_WISHLIST);
|
||||||
|
const {
|
||||||
|
mutate: removeAllMutate,
|
||||||
|
loading: removeAllLoading,
|
||||||
|
error: removeAllError
|
||||||
|
} = useMutation<IRemoveAllFromWishlistResponse>(REMOVE_ALL_FROM_WISHLIST);
|
||||||
|
const {
|
||||||
|
mutate: bulkMutate,
|
||||||
|
loading: bulkLoading,
|
||||||
|
error: bulkError
|
||||||
|
} = useMutation<IBulkWishlistResponse>(BULK_WISHLIST);
|
||||||
|
|
||||||
|
async function overwriteWishlist (
|
||||||
|
args: IOverwriteWishlistArguments
|
||||||
|
) {
|
||||||
|
switch (args.type) {
|
||||||
|
case "add":
|
||||||
|
const addResult = await addMutate({
|
||||||
|
wishlistUuid: wishlistUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addResult?.data?.addWishlistProduct?.wishlist) {
|
||||||
|
wishlistStore.setWishlist(addResult.data.addWishlistProduct.wishlist);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.addToWishlist', { product: args.productName }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "remove":
|
||||||
|
const removeResult = await removeMutate({
|
||||||
|
wishlistUuid: wishlistUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeResult?.data?.removeWishlistProduct?.wishlist) {
|
||||||
|
wishlistStore.setWishlist(removeResult.data.removeWishlistProduct.wishlist);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.removeFromWishlist', { product: args.productName }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "removeAll":
|
||||||
|
const removeAllResult = await removeAllMutate({
|
||||||
|
wishlistUuid: wishlistUuid.value,
|
||||||
|
productUuid: args.productUuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (removeAllResult?.data?.removeAllWishlistProducts?.wishlist) {
|
||||||
|
wishlistStore.setWishlist(removeAllResult.data.removeAllWishlistProducts.wishlist);
|
||||||
|
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.removeAllFromWishlist'),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "bulk":
|
||||||
|
const bulkResult = await bulkMutate({
|
||||||
|
wishlistUuid: wishlistUuid.value,
|
||||||
|
action: args.bulkAction,
|
||||||
|
products: args.products
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
|
||||||
|
wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist);
|
||||||
|
useNotification(
|
||||||
|
t('popup.success.bulkRemoveWishlist'),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.error('No type provided for overwriteWishlist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(addError || removedError || removeAllError || bulkError, (err) => {
|
||||||
|
if (!err) return;
|
||||||
|
console.error('useWishlistOverwrite error:', err);
|
||||||
|
let message = t('popup.errors.defaultError');
|
||||||
|
if (isGraphQLError(err)) {
|
||||||
|
message = err.graphQLErrors?.[0]?.message || message;
|
||||||
|
} else {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
useNotification(
|
||||||
|
message,
|
||||||
|
'error',
|
||||||
|
t('popup.errors.main')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return{
|
||||||
|
addLoading,
|
||||||
|
removeLoading,
|
||||||
|
removeAllLoading,
|
||||||
|
bulkLoading,
|
||||||
|
overwriteWishlist
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,16 @@ export const PRODUCT_FRAGMENT = gql`
|
||||||
price
|
price
|
||||||
quantity
|
quantity
|
||||||
slug
|
slug
|
||||||
|
description
|
||||||
|
brand {
|
||||||
|
smallLogo
|
||||||
|
uuid
|
||||||
|
name
|
||||||
|
}
|
||||||
category {
|
category {
|
||||||
name
|
name
|
||||||
|
slug
|
||||||
|
uuid
|
||||||
}
|
}
|
||||||
images {
|
images {
|
||||||
edges {
|
edges {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export const USER_FRAGMENT = gql`
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
phoneNumber
|
phoneNumber
|
||||||
|
dateJoined
|
||||||
balance {
|
balance {
|
||||||
amount
|
amount
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
|
||||||
|
|
||||||
export const ADD_TO_CART = gql`
|
export const ADD_TO_CART = gql`
|
||||||
mutation addToCart(
|
mutation addToCart(
|
||||||
$orderUuid: String!,
|
$orderUuid: UUID!,
|
||||||
$productUuid: String!
|
$productUuid: UUID!
|
||||||
) {
|
) {
|
||||||
addOrderProduct(
|
addOrderProduct(
|
||||||
orderUuid: $orderUuid,
|
orderUuid: $orderUuid,
|
||||||
|
|
@ -19,8 +19,8 @@ export const ADD_TO_CART = gql`
|
||||||
|
|
||||||
export const REMOVE_FROM_CART = gql`
|
export const REMOVE_FROM_CART = gql`
|
||||||
mutation removeFromCart(
|
mutation removeFromCart(
|
||||||
$orderUuid: String!,
|
$orderUuid: UUID!,
|
||||||
$productUuid: String!
|
$productUuid: UUID!
|
||||||
) {
|
) {
|
||||||
removeOrderProduct(
|
removeOrderProduct(
|
||||||
orderUuid: $orderUuid,
|
orderUuid: $orderUuid,
|
||||||
|
|
@ -36,8 +36,8 @@ export const REMOVE_FROM_CART = gql`
|
||||||
|
|
||||||
export const REMOVE_KIND_FROM_CART = gql`
|
export const REMOVE_KIND_FROM_CART = gql`
|
||||||
mutation removeKindFromCart(
|
mutation removeKindFromCart(
|
||||||
$orderUuid: String!,
|
$orderUuid: UUID!,
|
||||||
$productUuid: String!
|
$productUuid: UUID!
|
||||||
) {
|
) {
|
||||||
removeOrderProductsOfAKind(
|
removeOrderProductsOfAKind(
|
||||||
orderUuid: $orderUuid,
|
orderUuid: $orderUuid,
|
||||||
|
|
@ -53,7 +53,7 @@ export const REMOVE_KIND_FROM_CART = gql`
|
||||||
|
|
||||||
export const REMOVE_ALL_FROM_CART = gql`
|
export const REMOVE_ALL_FROM_CART = gql`
|
||||||
mutation removeAllFromCart(
|
mutation removeAllFromCart(
|
||||||
$orderUuid: String!
|
$orderUuid: UUID!
|
||||||
) {
|
) {
|
||||||
removeAllOrderProducts(
|
removeAllOrderProducts(
|
||||||
orderUuid: $orderUuid
|
orderUuid: $orderUuid
|
||||||
|
|
@ -64,4 +64,23 @@ export const REMOVE_ALL_FROM_CART = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${ORDER_FRAGMENT}
|
${ORDER_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BULK_CART = gql`
|
||||||
|
mutation bulkOrderAction(
|
||||||
|
$orderUuid: UUID!,
|
||||||
|
$action: String!,
|
||||||
|
$products: [BulkActionOrderProductInput]!
|
||||||
|
) {
|
||||||
|
bulkOrderAction(
|
||||||
|
orderUuid: $orderUuid
|
||||||
|
action: $action
|
||||||
|
products: $products
|
||||||
|
) {
|
||||||
|
order {
|
||||||
|
...Order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${ORDER_FRAGMENT}
|
||||||
`
|
`
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {USER_FRAGMENT} from "~/graphql/fragments/user.fragment";
|
||||||
|
|
||||||
export const SWITCH_LANGUAGE = gql`
|
export const SWITCH_LANGUAGE = gql`
|
||||||
mutation setlanguage(
|
mutation setlanguage(
|
||||||
$uuid: UUID!,
|
$uuid: UUID!,
|
||||||
|
|
@ -12,4 +14,5 @@ export const SWITCH_LANGUAGE = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${USER_FRAGMENT}
|
||||||
`
|
`
|
||||||
|
|
@ -2,8 +2,8 @@ import {WISHLIST_FRAGMENT} from "@/graphql/fragments/wishlist.fragment.js";
|
||||||
|
|
||||||
export const ADD_TO_WISHLIST = gql`
|
export const ADD_TO_WISHLIST = gql`
|
||||||
mutation addToWishlist(
|
mutation addToWishlist(
|
||||||
$wishlistUuid: String!,
|
$wishlistUuid: UUID!,
|
||||||
$productUuid: String!
|
$productUuid: UUID!
|
||||||
) {
|
) {
|
||||||
addWishlistProduct(
|
addWishlistProduct(
|
||||||
wishlistUuid: $wishlistUuid,
|
wishlistUuid: $wishlistUuid,
|
||||||
|
|
@ -19,8 +19,8 @@ export const ADD_TO_WISHLIST = gql`
|
||||||
|
|
||||||
export const REMOVE_FROM_WISHLIST = gql`
|
export const REMOVE_FROM_WISHLIST = gql`
|
||||||
mutation removeFromWishlist(
|
mutation removeFromWishlist(
|
||||||
$wishlistUuid: String!,
|
$wishlistUuid: UUID!,
|
||||||
$productUuid: String!
|
$productUuid: UUID!
|
||||||
) {
|
) {
|
||||||
removeWishlistProduct(
|
removeWishlistProduct(
|
||||||
wishlistUuid: $wishlistUuid,
|
wishlistUuid: $wishlistUuid,
|
||||||
|
|
@ -36,7 +36,7 @@ export const REMOVE_FROM_WISHLIST = gql`
|
||||||
|
|
||||||
export const REMOVE_ALL_FROM_WISHLIST = gql`
|
export const REMOVE_ALL_FROM_WISHLIST = gql`
|
||||||
mutation removeAllFromWishlist(
|
mutation removeAllFromWishlist(
|
||||||
$wishlistUuid: String!
|
$wishlistUuid: UUID!
|
||||||
) {
|
) {
|
||||||
removeAllWishlistProducts(
|
removeAllWishlistProducts(
|
||||||
wishlistUuid: $wishlistUuid
|
wishlistUuid: $wishlistUuid
|
||||||
|
|
@ -47,4 +47,23 @@ export const REMOVE_ALL_FROM_WISHLIST = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${WISHLIST_FRAGMENT}
|
${WISHLIST_FRAGMENT}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const BULK_WISHLIST = gql`
|
||||||
|
mutation bulkWishlistAction(
|
||||||
|
$wishlistUuid: UUID!,
|
||||||
|
$action: String!,
|
||||||
|
$products: [BulkActionOrderProductInput]!
|
||||||
|
) {
|
||||||
|
bulkWishlistAction(
|
||||||
|
wishlistUuid: $wishlistUuid
|
||||||
|
action: $action
|
||||||
|
products: $products
|
||||||
|
) {
|
||||||
|
wishlist {
|
||||||
|
...Wishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${WISHLIST_FRAGMENT}
|
||||||
`
|
`
|
||||||
|
|
@ -20,7 +20,7 @@ export const GET_BRANDS = gql`
|
||||||
|
|
||||||
export const GET_BRAND_BY_UUID = gql`
|
export const GET_BRAND_BY_UUID = gql`
|
||||||
query getBrandbyUuid(
|
query getBrandbyUuid(
|
||||||
$uuid: String!
|
$uuid: UUID!
|
||||||
) {
|
) {
|
||||||
brands(
|
brands(
|
||||||
uuid: $uuid
|
uuid: $uuid
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,24 @@ import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
|
||||||
|
|
||||||
export const GET_PRODUCTS = gql`
|
export const GET_PRODUCTS = gql`
|
||||||
query getProducts(
|
query getProducts(
|
||||||
$after: String,
|
$productAfter: String,
|
||||||
$first: Int,
|
$first: Int,
|
||||||
$categoriesSlugs: String,
|
$categoriesSlugs: String,
|
||||||
$orderBy: String,
|
$orderBy: String,
|
||||||
$minPrice: Decimal,
|
$minPrice: Decimal,
|
||||||
$maxPrice: Decimal,
|
$maxPrice: Decimal,
|
||||||
$productName: String
|
$productName: String,
|
||||||
|
$attributes: String
|
||||||
) {
|
) {
|
||||||
products(
|
products(
|
||||||
after: $after,
|
after: $productAfter,
|
||||||
first: $first,
|
first: $first,
|
||||||
categoriesSlugs: $categoriesSlugs,
|
categoriesSlugs: $categoriesSlugs,
|
||||||
orderBy: $orderBy,
|
orderBy: $orderBy,
|
||||||
minPrice: $minPrice,
|
minPrice: $minPrice,
|
||||||
maxPrice: $maxPrice,
|
maxPrice: $maxPrice,
|
||||||
name: $productName
|
name: $productName,
|
||||||
|
attributes: $attributes
|
||||||
) {
|
) {
|
||||||
edges {
|
edges {
|
||||||
cursor
|
cursor
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"addToCart": "Add To Cart",
|
"addToCart": "Add to cart",
|
||||||
|
"removeFromCart": "Remove from cart",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"goEmail": "Take me to my inbox",
|
"goEmail": "Take me to my inbox",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
"buy": "Buy Now",
|
"checkout": "Checkout",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"sendLink": "Send link",
|
"sendLink": "Send link",
|
||||||
"topUp": "Top up"
|
"topUp": "Top up"
|
||||||
|
|
@ -37,7 +38,8 @@
|
||||||
"confirmNewPassword": "Confirm new password"
|
"confirmNewPassword": "Confirm new password"
|
||||||
},
|
},
|
||||||
"checkboxes": {
|
"checkboxes": {
|
||||||
"remember": "Remember me"
|
"remember": "Remember me",
|
||||||
|
"chooseAll": "Choose all"
|
||||||
},
|
},
|
||||||
"popup": {
|
"popup": {
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
@ -51,17 +53,21 @@
|
||||||
"confirmEmail": "Verification E-mail link successfully sent!",
|
"confirmEmail": "Verification E-mail link successfully sent!",
|
||||||
"reset": "If specified email exists in our system, we will send a password recovery email!",
|
"reset": "If specified email exists in our system, we will send a password recovery email!",
|
||||||
"newPassword": "You have successfully changed your password!",
|
"newPassword": "You have successfully changed your password!",
|
||||||
"contactUs": "Your message was sent successfully!"
|
"contactUs": "Your message was sent successfully!",
|
||||||
|
"addToCart": "{product} has been added to the cart!",
|
||||||
|
"removeFromCart": "{product} has been removed from the cart!",
|
||||||
|
"removeAllFromCart": "You have successfully emptied the cart!",
|
||||||
|
"addToWishlist": "{product} has been added to the wishlist!",
|
||||||
|
"removeFromWishlist": "{product} has been removed from the wishlist!",
|
||||||
|
"removeAllFromWishlist": "You have successfully emptied the wishlist!",
|
||||||
|
"bulkRemoveWishlist": "Selected items have been successfully removed from the wishlist!"
|
||||||
},
|
},
|
||||||
"addToCart": "{product} has been added to the cart!",
|
|
||||||
"addToCartLimit": "Total quantity limit is {quantity}!",
|
"addToCartLimit": "Total quantity limit is {quantity}!",
|
||||||
"failAdd": "Please log in to make a purchase",
|
"failAdd": "Please log in to make a purchase",
|
||||||
"activationSuccess": "E-mail successfully verified. Please Sign In!",
|
"activationSuccess": "E-mail successfully verified. Please Sign In!",
|
||||||
"successUpdate": "Profile successfully updated!",
|
"successUpdate": "Profile successfully updated!",
|
||||||
|
|
||||||
"payment": "Your purchase is being processed! Please stand by",
|
"payment": "Your purchase is being processed! Please stand by",
|
||||||
"successCheckout": "Order purchase successful!",
|
"successCheckout": "Order purchase successful!"
|
||||||
"addToWishlist": "{product} has been added to the wishlist!"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
|
@ -113,7 +119,10 @@
|
||||||
},
|
},
|
||||||
"breadcrumbs": {
|
"breadcrumbs": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"catalog": "Catalog"
|
"catalog": "Catalog",
|
||||||
|
"contact": "Contact",
|
||||||
|
"wishlist": "Wishlist",
|
||||||
|
"cart": "Cart"
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"title": "Contact us"
|
"title": "Contact us"
|
||||||
|
|
@ -123,7 +132,8 @@
|
||||||
"filters": {
|
"filters": {
|
||||||
"title": "Filters",
|
"title": "Filters",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"reset": "Reset"
|
"reset": "Reset",
|
||||||
|
"all": "All"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
|
@ -131,5 +141,26 @@
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"brands": "Brands",
|
"brands": "Brands",
|
||||||
"byRequest": "by request"
|
"byRequest": "by request"
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"characteristics": "All characteristics",
|
||||||
|
"similar": "Similar products"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"orders": {
|
||||||
|
"title": "Orders"
|
||||||
|
},
|
||||||
|
"wishlist": {
|
||||||
|
"title": "Wishlist",
|
||||||
|
"total": "{quantity} items worth {amount}"
|
||||||
|
},
|
||||||
|
"cart": {
|
||||||
|
"title": "Cart",
|
||||||
|
"quantity": "Quantity: ",
|
||||||
|
"total": "Total: "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ export default defineNuxtConfig({
|
||||||
authType: 'Bearer',
|
authType: 'Bearer',
|
||||||
authHeader: 'X-EVIBES-AUTH',
|
authHeader: 'X-EVIBES-AUTH',
|
||||||
tokenStorage: 'cookie',
|
tokenStorage: 'cookie',
|
||||||
tokenName: `${process.env.EVIBES_PROJECT_NAME?.toLowerCase()}-access`,
|
tokenName: `${process.env.EVIBES_PROJECT_NAME?.toLowerCase()}-access`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
357
storefront/package-lock.json
generated
357
storefront/package-lock.json
generated
|
|
@ -865,30 +865,30 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
|
||||||
"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
|
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/utils": "^0.2.9"
|
"@floating-ui/utils": "^0.2.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/dom": {
|
"node_modules/@floating-ui/dom": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
|
||||||
"integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
|
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.7.1",
|
"@floating-ui/core": "^1.7.2",
|
||||||
"@floating-ui/utils": "^0.2.9"
|
"@floating-ui/utils": "^0.2.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.9",
|
"version": "0.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|
@ -969,9 +969,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@iconify/collections": {
|
"node_modules/@iconify/collections": {
|
||||||
"version": "1.0.557",
|
"version": "1.0.563",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.557.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.563.tgz",
|
||||||
"integrity": "sha512-283MvzQA7NgWkjt2AiMURoyW2hUWu0C4c0PjgAOXiQ4lEVQlwIYP8a9FR3xSECfc+EAMq2+M+1kbUlq09doRtw==",
|
"integrity": "sha512-Y5GNkqMQW4RfF5nG7GN9zht2IexXpygFb01T1+xvm/hvEBgn7CDnxO2v3D586VhcA5N93iEBpLVGyV4gaHnZOA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/types": "*"
|
"@iconify/types": "*"
|
||||||
|
|
@ -1765,17 +1765,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/icon": {
|
"node_modules/@nuxt/icon": {
|
||||||
"version": "1.13.0",
|
"version": "1.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-1.14.0.tgz",
|
||||||
"integrity": "sha512-Sft1DZj/U5Up60DMnhp+hRDNDXRkMhwHocxgDkDkAPBxqR8PRyvzxcMIy3rjGMu0s+fB6ZdLs6vtaWzjWuerQQ==",
|
"integrity": "sha512-4kb2rbvbSll784LUme2fDm62NW0Tryr8wADFEU3vIoOj4TZywcwPafIl0MT6ah3RNgbPd174EFVOaUdPSUQENA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/collections": "^1.0.548",
|
"@iconify/collections": "^1.0.560",
|
||||||
"@iconify/types": "^2.0.0",
|
"@iconify/types": "^2.0.0",
|
||||||
"@iconify/utils": "^2.3.0",
|
"@iconify/utils": "^2.3.0",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
"@nuxt/devtools-kit": "^2.4.1",
|
"@nuxt/devtools-kit": "^2.5.0",
|
||||||
"@nuxt/kit": "^3.17.3",
|
"@nuxt/kit": "^3.17.5",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"local-pkg": "^1.1.1",
|
"local-pkg": "^1.1.1",
|
||||||
"mlly": "^1.7.4",
|
"mlly": "^1.7.4",
|
||||||
|
|
@ -1783,7 +1783,7 @@
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
"std-env": "^3.9.0",
|
"std-env": "^3.9.0",
|
||||||
"tinyglobby": "^0.2.13"
|
"tinyglobby": "^0.2.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/image": {
|
"node_modules/@nuxt/image": {
|
||||||
|
|
@ -3251,13 +3251,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/integrations": {
|
"node_modules/@vueuse/integrations": {
|
||||||
"version": "13.3.0",
|
"version": "13.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.4.0.tgz",
|
||||||
"integrity": "sha512-h5mGRYPbiTZTFP/AKELLPGnUDBly7z7Qd1pgEQlT3ItQ0NlZM0vB+8SOQycpSBOBlgg72Zgw+mi2r+4O/G8RuQ==",
|
"integrity": "sha512-rwNoE0MNJBUuSzTZcUVrkovtHvpWIySOcC6XpcS33ZarHDNhd9CPvCD4eNl3N0Phz1he1JV0iYULRyPQ5HCbFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "13.3.0",
|
"@vueuse/core": "13.4.0",
|
||||||
"@vueuse/shared": "13.3.0"
|
"@vueuse/shared": "13.4.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
|
@ -3316,6 +3316,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
|
||||||
|
"version": "13.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.4.0.tgz",
|
||||||
|
"integrity": "sha512-OnK7zW3bTq/QclEk17+vDFN3tuAm8ONb9zQUIHrYQkkFesu3WeGUx/3YzpEp+ly53IfDAT9rsYXgGW6piNZC5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
|
"@vueuse/metadata": "13.4.0",
|
||||||
|
"@vueuse/shared": "13.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
|
||||||
|
"version": "13.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.4.0.tgz",
|
||||||
|
"integrity": "sha512-CPDQ/IgOeWbqItg1c/pS+Ulum63MNbpJ4eecjFJqgD/JUCJ822zLfpw6M9HzSvL6wbzMieOtIAW/H8deQASKHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
|
||||||
|
"version": "13.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.4.0.tgz",
|
||||||
|
"integrity": "sha512-+AxuKbw8R1gYy5T21V5yhadeNM7rJqb4cPaRI9DdGnnNl3uqXh+unvQ3uCaA2DjYLbNr1+l7ht/B4qEsRegX6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vueuse/metadata": {
|
"node_modules/@vueuse/metadata": {
|
||||||
"version": "13.3.0",
|
"version": "13.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.3.0.tgz",
|
||||||
|
|
@ -6531,6 +6569,20 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -7764,6 +7816,257 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-darwin-arm64": "1.30.1",
|
||||||
|
"lightningcss-darwin-x64": "1.30.1",
|
||||||
|
"lightningcss-freebsd-x64": "1.30.1",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.30.1",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.30.1",
|
||||||
|
"lightningcss-linux-x64-musl": "1.30.1",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.30.1",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.30.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
||||||
|
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss/node_modules/detect-libc": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,17 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useBrandByUuid} from "~/composables/brands";
|
import {useBrandByUuid} from "~/composables/brands";
|
||||||
|
import {usePageTitle} from "~/composables/utils/index.js";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const slug = computed(() => route.params.uuid)
|
const slug = computed(() => route.params.uuid)
|
||||||
|
|
||||||
|
const { setPageTitle } = usePageTitle();
|
||||||
|
|
||||||
const { brand } = await useBrandByUuid(slug.value);
|
const { brand } = await useBrandByUuid(slug.value);
|
||||||
|
|
||||||
|
setPageTitle(brand.value?.name ?? 'Brand');
|
||||||
// TODO: add product by this brand
|
// TODO: add product by this brand
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,28 @@
|
||||||
import {useUserActivation} from "~/composables/user";
|
import {useUserActivation} from "~/composables/user";
|
||||||
import { useRouteQuery } from '@vueuse/router';
|
import { useRouteQuery } from '@vueuse/router';
|
||||||
|
|
||||||
const {t} = useI18n()
|
const {t} = useI18n();
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore();
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: t('breadcrumbs.home'),
|
title: t('breadcrumbs.home'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const token = useRouteQuery('token', '')
|
const token = useRouteQuery('token', '');
|
||||||
const uid = useRouteQuery('uid', '')
|
const uid = useRouteQuery('uid', '');
|
||||||
|
|
||||||
const { activateUser } = useUserActivation();
|
const { activateUser } = useUserActivation();
|
||||||
|
|
||||||
onMounted( async () => {
|
onMounted( async () => {
|
||||||
if (route.path.includes('activate-user') && token.value && uid.value) {
|
if (route.path.includes('activate-user') && token.value && uid.value) {
|
||||||
await activateUser(token.value, uid.value)
|
await activateUser(token.value, uid.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.path.includes('reset-password') && token.value && uid.value) {
|
if (route.path.includes('reset-password') && token.value && uid.value) {
|
||||||
appStore.setActiveState('new-password')
|
appStore.setActiveState('new-password');
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,537 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="product" v-if="product">
|
<div class="product" v-if="product">
|
||||||
<ui-title>{{ product?.name }}</ui-title>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="product__wrapper">
|
<div class="product__wrapper">
|
||||||
|
<h1 class="product__title">{{ product.name }}</h1>
|
||||||
|
<div class="product__block">
|
||||||
|
<div class="product__images">
|
||||||
|
<div class="product__images-gallery">
|
||||||
|
<div
|
||||||
|
v-for="(image, idx) in images"
|
||||||
|
:key="idx"
|
||||||
|
@click="selectImage(image)"
|
||||||
|
:class="[{ active: image === selectedImage }]"
|
||||||
|
>
|
||||||
|
<nuxt-img
|
||||||
|
:src="image"
|
||||||
|
:alt="product.name"
|
||||||
|
format="webp"
|
||||||
|
densities="x1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nuxt-img
|
||||||
|
:src="selectedImage"
|
||||||
|
:alt="product.name"
|
||||||
|
class="product__images-main"
|
||||||
|
format="webp"
|
||||||
|
densities="x1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="product__center">
|
||||||
|
<p class="product__center-description">{{ product.description }}</p>
|
||||||
|
<p
|
||||||
|
class="product__center-characteristic"
|
||||||
|
@click="scrollTo('characteristics')"
|
||||||
|
>
|
||||||
|
{{ t('product.characteristics') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="product__info">
|
||||||
|
<div class="product__info-inner">
|
||||||
|
<div class="product__info-top">
|
||||||
|
<p>{{ t('cards.product.stock') }} {{ product.quantity }}</p>
|
||||||
|
<nuxt-img
|
||||||
|
:src="product.brand.smallLogo"
|
||||||
|
:alt="product.brand.name"
|
||||||
|
format="webp"
|
||||||
|
densities="x1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-rate
|
||||||
|
class="white"
|
||||||
|
v-model="rating"
|
||||||
|
allow-half
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<div class="product__info-price">{{ product.price }}</div>
|
||||||
|
<div class="product__info-bottom">
|
||||||
|
<ui-button
|
||||||
|
class="product__info-button"
|
||||||
|
>
|
||||||
|
{{ t('buttons.addToCart') }}
|
||||||
|
</ui-button>
|
||||||
|
<div class="product__info-wishlist">
|
||||||
|
<icon name="mdi:cards-heart-outline" size="28" />
|
||||||
|
<!-- <icon name="mdi:cards-heart" size="28" />-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<client-only>
|
||||||
|
<div class="characteristics" id="characteristics">
|
||||||
|
<div class="characteristics__wrapper">
|
||||||
|
<h6 class="characteristics__title">{{ t('product.characteristics') }}</h6>
|
||||||
|
<div class="characteristics__list">
|
||||||
|
<div
|
||||||
|
class="characteristics__column"
|
||||||
|
v-for="group in attributes"
|
||||||
|
:key="group.uuid"
|
||||||
|
>
|
||||||
|
<h6 class="characteristics__column-title">{{ group.name }}</h6>
|
||||||
|
<p
|
||||||
|
class="characteristics__item"
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.uuid"
|
||||||
|
>
|
||||||
|
<span class="characteristics__item-label"><span>{{ item.name }}</span></span>
|
||||||
|
<span class="characteristics__item-value">{{ item.valuesStr }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</client-only>
|
||||||
|
<div class="similar">
|
||||||
|
<h6 class="similar__title">{{ t('product.similar') }}</h6>
|
||||||
|
<div class="similar__inner">
|
||||||
|
<div class="similar__button prev" ref="prevButton">
|
||||||
|
<icon name="material-symbols:arrow-back-ios-new-rounded" size="30" />
|
||||||
|
</div>
|
||||||
|
<swiper
|
||||||
|
class="similar__swiper"
|
||||||
|
:modules="[Navigation]"
|
||||||
|
:spaceBetween="30"
|
||||||
|
:breakpoints="{
|
||||||
|
200: {
|
||||||
|
slidesPerView: 4
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
:navigation="{ prevEl: prevButton, nextEl: nextButton }"
|
||||||
|
>
|
||||||
|
<swiper-slide
|
||||||
|
v-for="prod in products"
|
||||||
|
:key="prod.node.uuid"
|
||||||
|
>
|
||||||
|
<cards-product
|
||||||
|
:product="prod.node"
|
||||||
|
/>
|
||||||
|
</swiper-slide>
|
||||||
|
</swiper>
|
||||||
|
<div class="similar__button next" ref="nextButton">
|
||||||
|
<icon name="material-symbols:arrow-back-ios-new-rounded" size="30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useProductBySlug} from "~/composables/products";
|
import {useProductBySlug, useProducts} from "~/composables/products";
|
||||||
import {usePageTitle} from "~/composables/utils";
|
import {usePageTitle} from "~/composables/utils";
|
||||||
|
import {useRouteParams} from "@vueuse/router";
|
||||||
|
import {useScrollTo} from "~/composables/scrollTo";
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
import {Navigation} from "swiper/modules";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
|
const {t} = useI18n();
|
||||||
|
|
||||||
const { setPageTitle } = usePageTitle()
|
const { setPageTitle } = usePageTitle();
|
||||||
|
const { scrollTo } = useScrollTo();
|
||||||
|
|
||||||
const slug = route.params.slug as string
|
const slug = useRouteParams<string>('slug');
|
||||||
|
|
||||||
const { product } = await useProductBySlug(slug)
|
const { product } = await useProductBySlug(slug.value);
|
||||||
setPageTitle(product.value?.name ?? 'Product')
|
const { products, getProducts } = await useProducts();
|
||||||
|
await getProducts({
|
||||||
|
categoriesSlugs: product.value?.category.slug
|
||||||
|
})
|
||||||
|
|
||||||
|
const images = computed<string[]>(() =>
|
||||||
|
product.value
|
||||||
|
? product.value.images.edges.map(e => e.node.image)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
const rating = computed(() => {
|
||||||
|
return product.value?.feedbacks.edges[0]?.node?.rating ?? 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributes = computed(() => {
|
||||||
|
const edges = product.value?.attributeGroups.edges ?? [];
|
||||||
|
|
||||||
|
const mainIndex = edges.findIndex(e => e.node.name === 'Основные характеристики');
|
||||||
|
const ordered = mainIndex >= 0
|
||||||
|
? [edges[mainIndex], ...edges.slice(0, mainIndex), ...edges.slice(mainIndex + 1)]
|
||||||
|
: edges;
|
||||||
|
|
||||||
|
return ordered.map(groupEdge => {
|
||||||
|
const { node } = groupEdge
|
||||||
|
return {
|
||||||
|
uuid: node.uuid,
|
||||||
|
name: node.name,
|
||||||
|
items: node.attributes.map(attr => ({
|
||||||
|
uuid: attr.uuid,
|
||||||
|
name: attr.name,
|
||||||
|
valuesStr: attr.values.map(v => v.value).join(', ')
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedImage = ref<string>(images.value[0] ?? '');
|
||||||
|
const selectImage = (image: string) => {
|
||||||
|
selectedImage.value = image;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevButton = ref<HTMLElement | null>(null);
|
||||||
|
const nextButton = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
setPageTitle(product.value?.name ?? 'Product');
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.params.slug,
|
() => route.params.slug,
|
||||||
async (newSlug) => {
|
async (newSlug) => {
|
||||||
if (typeof newSlug === 'string') {
|
if (typeof newSlug === 'string') {
|
||||||
const { product } = await useProductBySlug(newSlug)
|
const { product } = await useProductBySlug(newSlug);
|
||||||
setPageTitle(product.value?.name ?? 'Product')
|
setPageTitle(product.value?.name ?? 'Product');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.product {
|
||||||
|
margin-top: 25px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #5743b5;
|
||||||
|
color: #5743b5;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__block {
|
||||||
|
margin-top: 25px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
background-color: $white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 50px;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__images {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&-gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
& div {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 75px;
|
||||||
|
height: 75px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
background-color: $white;
|
||||||
|
border: 2px solid $white;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
box-shadow: 0 0 10px 1px $accent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0 0 0 0 transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 2px solid $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
& img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-main {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 2px solid $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
|
||||||
|
&-description {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-characteristic {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
color: $accentDark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
width: 400px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
background-color: $accent;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
&-inner {
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
border: 2px solid $white;
|
||||||
|
padding: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& p {
|
||||||
|
color: $white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
& img {
|
||||||
|
width: 75px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-price {
|
||||||
|
width: fit-content;
|
||||||
|
background-color: $accentDark;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
border: 1px solid $white;
|
||||||
|
padding: 7px 20px;
|
||||||
|
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-bottom {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
width: unset !important;
|
||||||
|
padding-inline: 25px !important;
|
||||||
|
background-color: $white !important;
|
||||||
|
color: $accent !important;
|
||||||
|
|
||||||
|
font-size: 20px !important;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: #e3e3e3 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-wishlist {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 41px;
|
||||||
|
height: 41px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: $white;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
font-size: 22px;
|
||||||
|
color: $accent;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: #e3e3e3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.characteristics {
|
||||||
|
padding-top: 100px;
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
background-color: $white;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid $accentDark;
|
||||||
|
color: $black;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit,minmax(100px, 30%));
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: $black;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2,1fr);
|
||||||
|
width: 100%;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: $white;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border-bottom: 1px dashed $accent;
|
||||||
|
content: "";
|
||||||
|
min-width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-value {
|
||||||
|
& span {
|
||||||
|
&:first-child {
|
||||||
|
& span {
|
||||||
|
&:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.similar {
|
||||||
|
margin-top: 100px;
|
||||||
|
background-color: $white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
padding: 25px 10px 0 10px;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-inline: 15px;
|
||||||
|
border-bottom: 2px solid $accentDark;
|
||||||
|
color: $black;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__swiper {
|
||||||
|
padding-block: 25px;
|
||||||
|
padding-inline: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
cursor: pointer;
|
||||||
|
height: fit-content;
|
||||||
|
margin-block: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding-inline: 10px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: $white;
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: $accentLight;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& span {
|
||||||
|
transition: 0.2s;
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.next {
|
||||||
|
& span {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
46
storefront/pages/profile.vue
Normal file
46
storefront/pages/profile.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="profile">
|
||||||
|
<div class="container">
|
||||||
|
<div class="profile__wrapper">
|
||||||
|
<profile-navigation />
|
||||||
|
<div class="profile__inner">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.profile {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 50px;
|
||||||
|
height: calc(100vh - 125px);
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
& .container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 100px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
storefront/pages/profile/cart.vue
Normal file
87
storefront/pages/profile/cart.vue
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div class="cart">
|
||||||
|
<div class="cart__top">
|
||||||
|
<div class="cart__top-left">
|
||||||
|
<p><span>{{ t('profile.cart.quantity') }}</span> {{ productsInCartQuantity }}</p>
|
||||||
|
<p><span>{{ t('profile.cart.total') }}</span> {{ totalPrice }}</p>
|
||||||
|
</div>
|
||||||
|
<ui-button class="cart__top-button">{{ t('buttons.checkout') }}</ui-button>
|
||||||
|
</div>
|
||||||
|
<div class="cart__list">
|
||||||
|
<cards-product
|
||||||
|
v-for="product in productsInCart"
|
||||||
|
:key="product.node.uuid"
|
||||||
|
:product="product.node.product"
|
||||||
|
:isList="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {usePageTitle} from "~/composables/utils/index.js";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const cartStore = useCartStore();
|
||||||
|
|
||||||
|
const productsInCart = computed(() => {
|
||||||
|
return cartStore.currentOrder.orderProducts ? cartStore.currentOrder.orderProducts.edges : [];
|
||||||
|
});
|
||||||
|
const totalPrice = computed(() => {
|
||||||
|
return cartStore.currentOrder.totalPrice ? cartStore.currentOrder.totalPrice : [];
|
||||||
|
});
|
||||||
|
const productsInCartQuantity = computed(() => {
|
||||||
|
let count = 0;
|
||||||
|
cartStore.currentOrder.orderProducts?.edges.forEach((el) => {
|
||||||
|
count = count + el.node.quantity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { setPageTitle } = usePageTitle();
|
||||||
|
|
||||||
|
setPageTitle(t('breadcrumbs.cart'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.cart {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
& p {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
width: fit-content;
|
||||||
|
padding-inline: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: $white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
storefront/pages/profile/orders.vue
Normal file
15
storefront/pages/profile/orders.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<p>orders</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.settings {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
storefront/pages/profile/settings.vue
Normal file
15
storefront/pages/profile/settings.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<p>settings</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.settings {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
175
storefront/pages/profile/wishlist.vue
Normal file
175
storefront/pages/profile/wishlist.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
<template>
|
||||||
|
<div class="wishlist">
|
||||||
|
<div class="wishlist__top">
|
||||||
|
<div class="wishlist__top-left">
|
||||||
|
<ui-checkbox
|
||||||
|
id="choose-all"
|
||||||
|
v-model="allSelected"
|
||||||
|
:isAccent="true"
|
||||||
|
>
|
||||||
|
{{ t('checkboxes.chooseAll') }}
|
||||||
|
</ui-checkbox>
|
||||||
|
<p>{{ t('profile.wishlist.total', {quantity: productsInWishlist.length, amount: totalPrice}) }}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="wishlist__top-button"
|
||||||
|
@click="onBulkRemove"
|
||||||
|
>
|
||||||
|
<icon name="material-symbols-light:delete-rounded" size="20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wishlist__list">
|
||||||
|
<div class="wishlist__list-inner">
|
||||||
|
<div
|
||||||
|
class="wishlist__item"
|
||||||
|
v-for="product in productsInWishlist"
|
||||||
|
:key="product.node.uuid"
|
||||||
|
>
|
||||||
|
<ui-checkbox
|
||||||
|
:id="`item-${product.node.uuid}`"
|
||||||
|
:modelValue="selectedProducts.some(o => o.uuid === product.node.uuid)"
|
||||||
|
@update:modelValue="checked => toggleUuid(product.node.uuid, checked)"
|
||||||
|
class="wishlist__item-checkbox"
|
||||||
|
:isAccent="true"
|
||||||
|
/>
|
||||||
|
<cards-product
|
||||||
|
:product="product.node"
|
||||||
|
:isList="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {usePageTitle} from "~/composables/utils";
|
||||||
|
import {useWishlistOverwrite} from "~/composables/wishlist";
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const wishlistStore = useWishlistStore();
|
||||||
|
|
||||||
|
const { overwriteWishlist } = useWishlistOverwrite();
|
||||||
|
|
||||||
|
const productsInWishlist = computed(() => {
|
||||||
|
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges : [];
|
||||||
|
});
|
||||||
|
const totalPrice = computed(() => {
|
||||||
|
return productsInWishlist.value.reduce((acc, p) => acc + p.node.price, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedProducts = ref<{ uuid: string }[]>([]);
|
||||||
|
|
||||||
|
const allSelected = computed<boolean>({
|
||||||
|
get() {
|
||||||
|
const ids = productsInWishlist.value.map(p => p.node.uuid);
|
||||||
|
return ids.length > 0
|
||||||
|
&& ids.every(id => selectedProducts.value.some(o => o.uuid === id));
|
||||||
|
},
|
||||||
|
set(val: boolean) {
|
||||||
|
if (val) {
|
||||||
|
selectedProducts.value = productsInWishlist.value
|
||||||
|
.map(p => ({ uuid: p.node.uuid }));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedProducts.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleUuid(uuid: string, checked: boolean) {
|
||||||
|
if (checked) {
|
||||||
|
if (!selectedProducts.value.some(o => o.uuid === uuid)) {
|
||||||
|
selectedProducts.value.push({ uuid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedProducts.value = selectedProducts.value
|
||||||
|
.filter(o => o.uuid !== uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBulkRemove() {
|
||||||
|
overwriteWishlist({
|
||||||
|
type: 'bulk',
|
||||||
|
bulkAction: 'remove',
|
||||||
|
products: selectedProducts.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { setPageTitle } = usePageTitle();
|
||||||
|
|
||||||
|
setPageTitle(t('breadcrumbs.wishlist'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wishlist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $white;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 50px;
|
||||||
|
|
||||||
|
& p {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
cursor: pointer;
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: $default_border_radius;
|
||||||
|
background-color: rgba($error, 0.3);
|
||||||
|
border: 1px solid $error;
|
||||||
|
padding: 5px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: 0.2s;
|
||||||
|
color: $error;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background-color: $error;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
background-color: $white;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
storefront/stores/cart.ts
Normal file
13
storefront/stores/cart.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type {IOrder} from "~/types";
|
||||||
|
|
||||||
|
export const useCartStore = defineStore('cart', () => {
|
||||||
|
const currentOrder = ref<IOrder | null>(null);
|
||||||
|
const setCurrentOrders = (order: IOrder) => {
|
||||||
|
currentOrder.value = order
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentOrder,
|
||||||
|
setCurrentOrders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -6,4 +6,34 @@ export interface IOrderResponse {
|
||||||
node: IOrder
|
node: IOrder
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAddToOrderResponse {
|
||||||
|
addOrderProduct: {
|
||||||
|
order: IOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoveFromOrderResponse {
|
||||||
|
removeOrderProduct: {
|
||||||
|
order: IOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoveKindFromOrderResponse {
|
||||||
|
removeOrderProductsOfAKind: {
|
||||||
|
order: IOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoveAllFromOrderResponse {
|
||||||
|
removeAllOrderProducts: {
|
||||||
|
order: IOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBulkOrderResponse {
|
||||||
|
bulkOrderAction: {
|
||||||
|
order: IOrder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,4 +6,28 @@ export interface IWishlistResponse {
|
||||||
node: IWishlist
|
node: IWishlist
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAddToWishlistResponse {
|
||||||
|
addWishlistProduct: {
|
||||||
|
wishlist: IWishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoveFromWishlistResponse {
|
||||||
|
removeWishlistProduct: {
|
||||||
|
wishlist: IWishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoveAllFromWishlistResponse {
|
||||||
|
removeAllWishlistProducts: {
|
||||||
|
wishlist: IWishlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBulkWishlistResponse {
|
||||||
|
bulkWishlistAction: {
|
||||||
|
wishlist: IWishlist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,12 @@ export interface IProduct {
|
||||||
price: number,
|
price: number,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
slug: string,
|
slug: string,
|
||||||
|
description: string,
|
||||||
|
brand: {
|
||||||
|
smallLogo: string,
|
||||||
|
uuid: string,
|
||||||
|
name: string
|
||||||
|
}
|
||||||
category: {
|
category: {
|
||||||
name: string
|
name: string
|
||||||
slug: string,
|
slug: string,
|
||||||
|
|
@ -27,8 +33,8 @@ export interface IProduct {
|
||||||
values: {
|
values: {
|
||||||
value: string,
|
value: string,
|
||||||
uuid: string
|
uuid: string
|
||||||
}
|
}[]
|
||||||
}
|
}[]
|
||||||
}
|
}
|
||||||
}[]
|
}[]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export interface IUser {
|
||||||
firstName: string,
|
firstName: string,
|
||||||
lastName: string,
|
lastName: string,
|
||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
|
dateJoined: string,
|
||||||
balance: {
|
balance: {
|
||||||
amount: number,
|
amount: number,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue