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 {useCompanyInfo} from "~/composables/company";
|
||||
import {useCategories} from "~/composables/categories";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
const { locale } = useI18n();
|
||||
const route = useRoute();
|
||||
|
|
@ -32,7 +33,13 @@ const switchLocalePath = useSwitchLocalePath();
|
|||
|
||||
const showBreadcrumbs = computed(() => {
|
||||
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);
|
||||
|
|
@ -66,7 +73,7 @@ watch(
|
|||
{ immediate: true }
|
||||
)
|
||||
|
||||
let stopWatcher: () => void;
|
||||
let stopWatcher: VoidFunction = () => {}
|
||||
|
||||
onMounted( async () => {
|
||||
refreshInterval = setInterval(async () => {
|
||||
|
|
|
|||
|
|
@ -2,4 +2,8 @@
|
|||
@use "modules/transitions";
|
||||
@use "global/mixins";
|
||||
@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-badge__content {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1680px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,6 @@
|
|||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.el-icon {
|
||||
.el-collapse .el-icon {
|
||||
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: fix displaying main part (children categories)
|
||||
|
||||
const blockRef = ref(null)
|
||||
onClickOutside(blockRef, () => setBlock(false))
|
||||
|
|
@ -148,7 +147,7 @@ const setActiveCategory = (category: ICategory) => {
|
|||
&__block {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 80%;
|
||||
max-height: 60vh;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
&__columns {
|
||||
|
|
|
|||
|
|
@ -13,37 +13,33 @@
|
|||
<base-header-catalog />
|
||||
<base-header-search />
|
||||
<div class="header__actions">
|
||||
<nuxt-link-locale to="/wishlist" class="header__actions-item">
|
||||
<div>
|
||||
<!-- <ui-counter>0</ui-counter>-->
|
||||
<!-- <skeletons-ui-counter />-->
|
||||
<Icon name="mdi:cards-heart-outline" size="28" />
|
||||
</div>
|
||||
<nuxt-link-locale to="/profile/wishlist" class="header__actions-item">
|
||||
<el-badge :value="productsInWishlistQuantity" type="primary">
|
||||
<icon name="mdi:cards-heart-outline" size="28" />
|
||||
</el-badge>
|
||||
<p>{{ t('header.actions.wishlist') }}</p>
|
||||
</nuxt-link-locale>
|
||||
<nuxt-link-locale to="/cart" class="header__actions-item">
|
||||
<div>
|
||||
<!-- <ui-counter>0</ui-counter>-->
|
||||
<!-- <skeletons-ui-counter />-->
|
||||
<Icon name="ph:shopping-cart-light" size="28" />
|
||||
</div>
|
||||
<nuxt-link-locale to="/profile/cart" class="header__actions-item">
|
||||
<el-badge :value="productsInCartQuantity" type="primary">
|
||||
<icon name="ph:shopping-cart-light" size="28" />
|
||||
</el-badge>
|
||||
<p>{{ t('header.actions.cart') }}</p>
|
||||
</nuxt-link-locale>
|
||||
<client-only>
|
||||
<nuxt-link-locale
|
||||
to="/"
|
||||
to="/profile/settings"
|
||||
class="header__actions-item"
|
||||
v-if="isAuthenticated"
|
||||
>
|
||||
<Icon name="material-symbols-light:person-outline-rounded" size="32" />
|
||||
<p @click="logout">{{ t('header.actions.profile') }}</p>
|
||||
<icon name="material-symbols-light:person-outline-rounded" size="32" />
|
||||
<p>{{ t('header.actions.profile') }}</p>
|
||||
</nuxt-link-locale>
|
||||
<div
|
||||
class="header__actions-item"
|
||||
@click="appStore.setActiveState('login')"
|
||||
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>
|
||||
</div>
|
||||
<template #fallback>
|
||||
|
|
@ -51,7 +47,7 @@
|
|||
class="header__actions-item"
|
||||
@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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -64,20 +60,34 @@
|
|||
<script setup lang="ts">
|
||||
import {useLogout} from "~/composables/auth";
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
box-shadow: 0 1px 2px #0000001a;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
@click="clearSearch"
|
||||
v-if="query"
|
||||
>
|
||||
<Icon name="gridicons:cross" size="16" />
|
||||
<icon name="gridicons:cross" size="16" />
|
||||
</button>
|
||||
<div class="search__tools-line" v-if="query"></div>
|
||||
<button type="submit">
|
||||
<Icon name="tabler:search" size="16" />
|
||||
<icon name="tabler:search" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -29,22 +29,23 @@
|
|||
<skeletons-header-search v-if="loading" />
|
||||
<div
|
||||
class="search__results-inner"
|
||||
v-for="(blocks, item) in filteredSearchResults"
|
||||
:key="item"
|
||||
v-for="(blocks, category) in filteredSearchResults"
|
||||
:key="category"
|
||||
>
|
||||
<div class="search__results-title">
|
||||
<p>{{ getBlockTitle(item) }}:</p>
|
||||
<p>{{ getBlockTitle(category) }}:</p>
|
||||
</div>
|
||||
<div
|
||||
class="search__item"
|
||||
v-for="item in blocks"
|
||||
:key="item.uuid"
|
||||
@click.stop="goTo(category, item)"
|
||||
>
|
||||
<div class="search__item-left">
|
||||
<Icon name="ic:twotone-search" size="18" />
|
||||
<icon name="ic:twotone-search" size="18" />
|
||||
<p>{{ item.name }}</p>
|
||||
</div>
|
||||
<Icon name="line-md:external-link" size="18" />
|
||||
<icon name="line-md:external-link" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="search__results-empty" v-if="!hasResults && query && !loading">
|
||||
|
|
@ -90,6 +91,29 @@ function submitSearch() {
|
|||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="card"
|
||||
:class="{ 'card__list': productView === 'list' }"
|
||||
:class="{ 'card__list': isList }"
|
||||
>
|
||||
<div class="card__wrapper">
|
||||
<nuxt-link-locale
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
>
|
||||
<div class="card__block">
|
||||
<client-only>
|
||||
<Swiper
|
||||
<swiper
|
||||
v-if="images.length"
|
||||
@swiper="onSwiper"
|
||||
:modules="[EffectFade, Pagination]"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
:pagination="paginationOptions"
|
||||
class="card__swiper"
|
||||
>
|
||||
<SwiperSlide
|
||||
<swiper-slide
|
||||
v-for="(img, i) in images"
|
||||
:key="i"
|
||||
class="card__swiper-slide"
|
||||
|
|
@ -29,16 +29,18 @@
|
|||
:alt="product.name"
|
||||
loading="lazy"
|
||||
class="card__swiper-image"
|
||||
format="webp"
|
||||
densities="x1"
|
||||
/>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
<div class="card__image-placeholder" />
|
||||
<div
|
||||
v-for="(_, i) in images"
|
||||
:key="i"
|
||||
v-for="(image, idx) in images"
|
||||
:key="idx"
|
||||
class="card__block-hover"
|
||||
:style="{ left: `${(100/ images.length) * i}%`, width: `${100/ images.length}%` }"
|
||||
@mouseenter="goTo(i)"
|
||||
:style="{ left: `${(100/ images.length) * idx}%`, width: `${100/ images.length}%` }"
|
||||
@mouseenter="goTo(idx)"
|
||||
@mouseleave="goTo(0)"
|
||||
/>
|
||||
</client-only>
|
||||
|
|
@ -50,19 +52,46 @@
|
|||
<el-rate
|
||||
v-model="rating"
|
||||
size="large"
|
||||
allow-half
|
||||
disabled
|
||||
/>
|
||||
<div class="card__quantity">{{ t('cards.product.stock') }} {{ product.quantity }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<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') }}
|
||||
</ui-button>
|
||||
<div class="card__bottom-wishlist">
|
||||
<Icon name="mdi:cards-heart-outline" size="28" />
|
||||
<!-- <Icon name="mdi:cards-heart" size="28" />-->
|
||||
<div
|
||||
class="card__bottom-wishlist"
|
||||
@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>
|
||||
|
|
@ -70,28 +99,36 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type {IProduct} from "~/types/app/products";
|
||||
import { useAppConfig } from '~/composables/config';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import { EffectFade, Pagination } from 'swiper/modules';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/effect-fade';
|
||||
import 'swiper/css/pagination'
|
||||
import {useWishlistOverwrite} from "~/composables/wishlist";
|
||||
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
|
||||
|
||||
const props = defineProps<{
|
||||
product: IProduct;
|
||||
isList?: boolean;
|
||||
}>();
|
||||
|
||||
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>(
|
||||
COOKIES_PRODUCT_VIEW_KEY as string,
|
||||
{
|
||||
default: () => 'grid',
|
||||
path: '/',
|
||||
}
|
||||
)
|
||||
const isProductInWishlist = computed(() => {
|
||||
const el = wishlistStore.wishlist?.products?.edges.find(
|
||||
(el) => el?.node?.uuid === props.product.uuid
|
||||
);
|
||||
|
||||
return !!el;
|
||||
});
|
||||
const isProductInCart = computed(() => {
|
||||
return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid);
|
||||
});
|
||||
|
||||
const rating = computed(() => {
|
||||
return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
|
||||
|
|
@ -171,7 +208,7 @@ function goTo(index: number) {
|
|||
}
|
||||
|
||||
@include hover {
|
||||
box-shadow: 0 0 30px 3px rgba($accentDark, 0.4);
|
||||
box-shadow: 0 0 20px 2px rgba($accentDark, 0.4);
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
|
|
@ -248,6 +285,10 @@ function goTo(index: number) {
|
|||
}
|
||||
|
||||
&__quantity {
|
||||
width: fit-content;
|
||||
background-color: rgba($accent, 0.2);
|
||||
border-radius: $default_border_radius;
|
||||
padding: 5px 10px;
|
||||
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__top">
|
||||
<h2>{{ t('store.filters.title') }}</h2>
|
||||
<Icon
|
||||
<icon
|
||||
name="line-md:close"
|
||||
size="30"
|
||||
@click="closeFilters"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<template #title="{ isActive }">
|
||||
<div :class="['filter__collapse-title', { 'is-active': isActive }]">
|
||||
{{ attribute.attributeName }}
|
||||
<Icon
|
||||
<icon
|
||||
name="material-symbols:keyboard-arrow-down"
|
||||
size="22"
|
||||
class="filter__collapse-icon"
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
:id="attribute.attributeName + '-all'"
|
||||
v-model="selectedAllMap[attribute.attributeName]"
|
||||
@change="toggleAll(attribute.attributeName)"
|
||||
:isFilter="true"
|
||||
:isAccent="true"
|
||||
>
|
||||
{{ t('store.filters.all') }}
|
||||
</ui-checkbox>
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
:key="idx"
|
||||
:id="attribute.attributeName + idx"
|
||||
v-model="selectedMap[attribute.attributeName][value]"
|
||||
:isFilter="true"
|
||||
:isAccent="true"
|
||||
>
|
||||
{{ value }}
|
||||
</ui-checkbox>
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
<script setup lang="ts">
|
||||
import type {IStoreFilters} from "~/types";
|
||||
import {useFilters} from "~/composables/store";
|
||||
import {useRouteQuery} from "@vueuse/router";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
|
@ -81,7 +82,17 @@ const emit = defineEmits<{
|
|||
(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')
|
||||
);
|
||||
|
||||
|
|
@ -110,6 +121,27 @@ watch(
|
|||
},
|
||||
{ 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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
v-for="product in products"
|
||||
:key="product.node.uuid"
|
||||
:product="product.node"
|
||||
:isList="productView === 'list'"
|
||||
/>
|
||||
<skeletons-cards-product
|
||||
v-if="pending"
|
||||
|
|
@ -38,6 +39,7 @@
|
|||
<script setup lang="ts">
|
||||
import {useFilters, useStore} from "~/composables/store";
|
||||
import {useRouteQuery} from "@vueuse/router";
|
||||
import {useRouteParams} from "@vueuse/router";
|
||||
import {useCategoryBySlug} from "~/composables/categories";
|
||||
import {useAppConfig} from '~/composables/config';
|
||||
|
||||
|
|
@ -69,7 +71,7 @@ watch(
|
|||
{ immediate: true }
|
||||
);
|
||||
|
||||
const { pending, products, pageInfo, prodVars } = await useStore(
|
||||
const { pending, products, pageInfo, variables } = await useStore(
|
||||
slug.value,
|
||||
attributes.value,
|
||||
orderBy.value,
|
||||
|
|
@ -86,29 +88,31 @@ function onFilterToggle() {
|
|||
}
|
||||
|
||||
function onFiltersChange(newFilters: Record<string, string[]>) {
|
||||
attributes.value = buildAttributesString(newFilters);
|
||||
const attrString = buildAttributesString(newFilters);
|
||||
attributes.value = attrString;
|
||||
variables.attributes = attrString;
|
||||
}
|
||||
|
||||
useIntersectionObserver(
|
||||
observer,
|
||||
async ([{ isIntersecting }]) => {
|
||||
if (isIntersecting && pageInfo.value?.hasNextPage) {
|
||||
prodVars.productAfter = pageInfo.value.endCursor;
|
||||
if (isIntersecting && pageInfo.value?.hasNextPage && !pending.value) {
|
||||
variables.productAfter = pageInfo.value.endCursor;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(orderBy, newVal => {
|
||||
prodVars.orderBy = newVal || '';
|
||||
variables.orderBy = newVal || '';
|
||||
});
|
||||
watch(attributes, newVal => {
|
||||
prodVars.attributes = newVal || '';
|
||||
variables.attributes = newVal;
|
||||
});
|
||||
watch(minPrice, newVal => {
|
||||
prodVars.minPrice = newVal || 0;
|
||||
variables.minPrice = newVal || 0;
|
||||
});
|
||||
watch(maxPrice, newVal => {
|
||||
prodVars.maxPrice = newVal || 500000;
|
||||
variables.maxPrice = newVal || 500000;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@
|
|||
:class="{ active: productView === 'list' }"
|
||||
@click="setView('list')"
|
||||
>
|
||||
<Icon name="material-symbols:view-list-sharp" size="16" />
|
||||
<icon name="material-symbols:view-list-sharp" size="16" />
|
||||
</button>
|
||||
<button
|
||||
class="top__view-button"
|
||||
:class="{ active: productView === 'grid' }"
|
||||
@click="setView('grid')"
|
||||
>
|
||||
<Icon name="material-symbols:grid-view" size="16" />
|
||||
<icon name="material-symbols:grid-view" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
@click="$emit('toggle-filter')"
|
||||
>
|
||||
{{ t('store.filters.title') }}
|
||||
<Icon name="line-md:filter" size="16" />
|
||||
<icon name="line-md:filter" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -95,7 +95,7 @@ watch(select, value => {
|
|||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const { breadcrumbs } = useBreadcrumbs()
|
|||
<style scoped lang="scss">
|
||||
.breadcrumbs {
|
||||
padding: 15px 250px 15px 50px;
|
||||
line-height: 140%;
|
||||
|
||||
&__link {
|
||||
cursor: pointer !important;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<label class="checkbox" :class="{ isFilter }">
|
||||
<label class="checkbox" :class="{ isAccent }">
|
||||
<input
|
||||
:id="id"
|
||||
class="checkbox__input"
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
id: string,
|
||||
id?: string,
|
||||
modelValue: boolean,
|
||||
isFilter: boolean
|
||||
isAccent?: boolean
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
|
|
@ -37,14 +37,15 @@ function onChange(e: Event) {
|
|||
gap: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&.isFilter {
|
||||
&.isAccent {
|
||||
& .checkbox__block {
|
||||
border: 2px solid $accent;
|
||||
border: 2px solid $accentDark;
|
||||
border-radius: $default_border_radius;
|
||||
}
|
||||
|
||||
& .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"
|
||||
>
|
||||
<button
|
||||
@click.prevent="setPasswordVisible"
|
||||
@click.prevent="togglePasswordVisible"
|
||||
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-else name="mdi:eye-outline" />
|
||||
<icon v-if="isPasswordVisible === 'password'" name="mdi:eye-off-outline" />
|
||||
<icon v-else name="mdi:eye-outline" />
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
||||
<p v-if="!isValid" class="block__error">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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<{
|
||||
type: string,
|
||||
placeholder: string,
|
||||
isError?: boolean,
|
||||
error?: string,
|
||||
modelValue?: [string, number],
|
||||
rules?: array,
|
||||
numberOnly: boolean
|
||||
rules?: Rule[],
|
||||
numberOnly?: boolean
|
||||
}>();
|
||||
|
||||
const isPasswordVisible = ref<string>(props.type);
|
||||
const setPasswordVisible = () => {
|
||||
if (isPasswordVisible.value === 'password') {
|
||||
isPasswordVisible.value = 'text';
|
||||
return;
|
||||
}
|
||||
isPasswordVisible.value = 'password';
|
||||
};
|
||||
const isPasswordVisible = ref(props.type);
|
||||
const isValid = ref(true);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const onlyNumbersKeydown = (event) => {
|
||||
function togglePasswordVisible() {
|
||||
isPasswordVisible.value =
|
||||
isPasswordVisible.value === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
const onlyNumbersKeydown = (event: KeyboardEvent) => {
|
||||
if (!/^\d$/.test(event.key) &&
|
||||
!['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'Tab', 'Home', 'End'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const validate = ref<boolean>(true);
|
||||
const errorMessage = ref<string>('');
|
||||
const onInput = (e: Event) => {
|
||||
let value = e.target.value;
|
||||
function onInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let value = target.value;
|
||||
|
||||
if (props.numberOnly) {
|
||||
const newValue = value.replace(/\D/g, '');
|
||||
if (newValue !== value) {
|
||||
e.target.value = newValue;
|
||||
value = newValue;
|
||||
const digitsOnly = value.replace(/\D/g, '');
|
||||
if (digitsOnly !== value) {
|
||||
target.value = digitsOnly;
|
||||
value = digitsOnly;
|
||||
}
|
||||
}
|
||||
|
||||
let result = true;
|
||||
let valid = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
props.rules?.forEach((rule) => {
|
||||
result = rule((e.target).value);
|
||||
|
||||
if (!result) {
|
||||
const result = rule(value);
|
||||
if (result !== true) {
|
||||
valid = false;
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -98,7 +99,6 @@ const onInput = (e: Event) => {
|
|||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
//border: 1px solid #b2b2b2;
|
||||
border-radius: $default_border_radius;
|
||||
background-color: $white;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,38 +6,42 @@
|
|||
@input="onInput"
|
||||
class="block__textarea"
|
||||
/>
|
||||
<p v-if="!validate" class="block__error">{{ errorMessage }}</p>
|
||||
<p v-if="!isValid" class="block__error">{{ errorMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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<{
|
||||
placeholder: string,
|
||||
isError?: boolean,
|
||||
error?: string,
|
||||
modelValue?: [string, number],
|
||||
rules?: array
|
||||
modelValue?: string,
|
||||
rules?: Rule[]
|
||||
}>();
|
||||
|
||||
const validate = ref<boolean>(true)
|
||||
const errorMessage = ref<string>('')
|
||||
const isValid = ref(true);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
let result = true
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const value = target.value;
|
||||
|
||||
props.rules?.forEach((rule) => {
|
||||
result = rule((e.target).value)
|
||||
isValid.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
if (!result) {
|
||||
errorMessage.value = String(result)
|
||||
result = false
|
||||
props.rules?.forEach(rule => {
|
||||
const result = rule(value);
|
||||
if (result !== true) {
|
||||
isValid.value = false;
|
||||
errorMessage.value = String(result);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
validate.value = result
|
||||
|
||||
return $emit('update:modelValue', (e.target).value)
|
||||
}
|
||||
emit('update:modelValue', value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export * from './useLogin'
|
||||
export * from './useRefresh'
|
||||
export * from './useRegister'
|
||||
export * from './useLogout'
|
||||
export * from './usePasswordReset'
|
||||
export * from './useNewPassword'
|
||||
export * from './useLogin';
|
||||
export * from './useRefresh';
|
||||
export * from './useRegister';
|
||||
export * from './useLogout';
|
||||
export * from './usePasswordReset';
|
||||
export * from './useNewPassword';
|
||||
|
|
@ -8,6 +8,7 @@ import { usePendingOrder } from '~/composables/orders';
|
|||
import { useUserStore } from '~/stores/user';
|
||||
import { useAppStore } from '~/stores/app';
|
||||
import {DEFAULT_LOCALE} from "~/config/constants";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useLogin() {
|
||||
const { t } = useI18n();
|
||||
|
|
@ -57,7 +58,10 @@ export function useLogin() {
|
|||
userStore.setUser(authData.user);
|
||||
cookieAccess.value = authData.accessToken
|
||||
|
||||
ElNotification({ message: t('popup.success.login'), type: 'success' });
|
||||
useNotification(
|
||||
t('popup.success.login'),
|
||||
'success'
|
||||
);
|
||||
|
||||
if (authData.user.language !== cookieLocale.value) {
|
||||
await checkAndRedirect(authData.user.language);
|
||||
|
|
@ -78,11 +82,11 @@ export function useLogin() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {NEW_PASSWORD} from "@/graphql/mutations/auth.js";
|
|||
import {isGraphQLError} from "~/utils/error";
|
||||
import type {INewPasswordResponse} from "~/types";
|
||||
import { useRouteQuery } from '@vueuse/router';
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useNewPassword() {
|
||||
const {t} = useI18n();
|
||||
|
|
@ -25,10 +26,10 @@ export function useNewPassword() {
|
|||
});
|
||||
|
||||
if (result?.data?.confirmResetPassword.success) {
|
||||
ElNotification({
|
||||
message: t('popup.success.newPassword'),
|
||||
type: 'success'
|
||||
})
|
||||
useNotification(
|
||||
t('popup.success.newPassword'),
|
||||
'success'
|
||||
);
|
||||
|
||||
await router.push({path: '/'})
|
||||
|
||||
|
|
@ -45,11 +46,11 @@ export function useNewPassword() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {RESET_PASSWORD} from "@/graphql/mutations/auth.js";
|
||||
import {isGraphQLError} from "~/utils/error";
|
||||
import type {IPasswordResetResponse} from "~/types";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function usePasswordReset() {
|
||||
const {t} = useI18n();
|
||||
|
|
@ -16,10 +17,10 @@ export function usePasswordReset() {
|
|||
});
|
||||
|
||||
if (result?.data?.resetPassword.success) {
|
||||
ElNotification({
|
||||
message: t('popup.success.reset'),
|
||||
type: 'success'
|
||||
})
|
||||
useNotification(
|
||||
t('popup.success.reset'),
|
||||
'success'
|
||||
);
|
||||
|
||||
appStore.unsetActiveState();
|
||||
}
|
||||
|
|
@ -34,11 +35,11 @@ export function usePasswordReset() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { usePendingOrder } from '~/composables/orders';
|
|||
import { useUserStore } from '~/stores/user';
|
||||
import { isGraphQLError } from '~/utils/error';
|
||||
import {DEFAULT_LOCALE} from "~/config/constants";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useRefresh() {
|
||||
const { t } = useI18n();
|
||||
|
|
@ -62,11 +63,11 @@ export function useRefresh() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {REGISTER} from "@/graphql/mutations/auth.js";
|
|||
import {useMailClient} from "@/composables/utils";
|
||||
import {isGraphQLError} from "~/utils/error";
|
||||
import type {IRegisterResponse} from "~/types";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useRegister() {
|
||||
const {t} = useI18n();
|
||||
|
|
@ -31,48 +32,41 @@ export function useRegister() {
|
|||
if (result?.data?.createUser?.success) {
|
||||
detectMailClient(email);
|
||||
|
||||
ElNotification({
|
||||
message: h('div', [
|
||||
h('p', t('popup.success.register')),
|
||||
mailClientUrl.value ? h(
|
||||
'button',
|
||||
{
|
||||
style: {
|
||||
marginTop: '10px',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#000000',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
useNotification(
|
||||
h('div', [
|
||||
h('p', t('popup.success.register')),
|
||||
mailClientUrl.value ? h(
|
||||
'button',
|
||||
{
|
||||
class: 'el-notification__button',
|
||||
onClick: () => {
|
||||
openMailClient()
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
openMailClient()
|
||||
}
|
||||
},
|
||||
t('buttons.goEmail')
|
||||
) : ''
|
||||
]),
|
||||
type: 'success'
|
||||
})
|
||||
t('buttons.goEmail')
|
||||
) : ''
|
||||
]),
|
||||
'success'
|
||||
);
|
||||
|
||||
appStore.unsetActiveState();
|
||||
}
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (!err) return
|
||||
console.error('useRegister error:', err)
|
||||
let message = t('popup.errors.defaultError')
|
||||
if (!err) return;
|
||||
console.error('useRegister error:', err);
|
||||
let message = t('popup.errors.defaultError');
|
||||
if (isGraphQLError(err)) {
|
||||
message = err.graphQLErrors?.[0]?.message || message
|
||||
message = err.graphQLErrors?.[0]?.message || message;
|
||||
} else {
|
||||
message = err.message
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
})
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './useBrands'
|
||||
export * from './useBrandByUuid'
|
||||
export * from './useBrands';
|
||||
export * from './useBrandByUuid';
|
||||
|
|
@ -1 +1 @@
|
|||
export * from './useBreadcrumbs'
|
||||
export * from './useBreadcrumbs';
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export * from './useCategories'
|
||||
export * from './useCategoryTags'
|
||||
export * from './useCategoryBySlug'
|
||||
export * from './useCategories';
|
||||
export * from './useCategoryTags';
|
||||
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 type {IContactUsResponse} from "~/types";
|
||||
import {CONTACT_US} from "~/graphql/mutations/contact";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useContactUs() {
|
||||
const {t} = useI18n();
|
||||
|
|
@ -23,10 +24,10 @@ export function useContactUs() {
|
|||
});
|
||||
|
||||
if (result?.data?.contactUs.received) {
|
||||
ElNotification({
|
||||
message: t('popup.success.contactUs'),
|
||||
type: 'success'
|
||||
})
|
||||
useNotification(
|
||||
t('popup.success.contactUs'),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,11 +40,11 @@ export function useContactUs() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export * from './useLocaleRedirect'
|
||||
export * from './useLanguage'
|
||||
export * from './useLanguageSwitch'
|
||||
export * from './useLocaleRedirect';
|
||||
export * from './useLanguage';
|
||||
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 './useProductBySlug'
|
||||
export * from './useProductTags'
|
||||
export * from './useProducts';
|
||||
export * from './useProductBySlug';
|
||||
export * from './useProductTags';
|
||||
|
|
@ -1,26 +1,26 @@
|
|||
import { GET_PRODUCT_BY_SLUG } from '~/graphql/queries/standalone/products'
|
||||
import type { IProduct, IProductResponse } from '~/types'
|
||||
import { GET_PRODUCT_BY_SLUG } from '~/graphql/queries/standalone/products';
|
||||
import type { IProduct, IProductResponse } from '~/types';
|
||||
|
||||
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>(
|
||||
GET_PRODUCT_BY_SLUG,
|
||||
{ slug }
|
||||
)
|
||||
);
|
||||
|
||||
const result = data.value?.products?.edges[0]?.node ?? null
|
||||
const result = data.value?.products?.edges[0]?.node ?? null;
|
||||
if (result) {
|
||||
product.value = result
|
||||
product.value = result;
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useProductBySlug error:', err)
|
||||
console.error('useProductBySlug error:', err);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
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 './useSearchCombined'
|
||||
export * from './useSearchUi'
|
||||
export * from './useSearch';
|
||||
export * from './useSearchCombined';
|
||||
export * from './useSearchUi';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {SEARCH} from "~/graphql/mutations/search";
|
||||
import type {ISearchResponse, ISearchResults} from "~/types";
|
||||
import {isGraphQLError} from "~/utils/error";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useSearch() {
|
||||
const {t} = useI18n();
|
||||
|
|
@ -37,11 +38,11 @@ export function useSearch() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './useStore'
|
||||
export * from './useFilters'
|
||||
export * from './useStore';
|
||||
export * from './useFilters';
|
||||
|
|
@ -79,6 +79,29 @@ export function useFilters(filterableAttributes: Ref<IStoreFilters[]>) {
|
|||
.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 {
|
||||
selectedMap,
|
||||
selectedAllMap,
|
||||
|
|
@ -86,6 +109,7 @@ export function useFilters(filterableAttributes: Ref<IStoreFilters[]>) {
|
|||
toggleAll,
|
||||
resetFilters,
|
||||
applyFilters,
|
||||
buildAttributesString
|
||||
buildAttributesString,
|
||||
parseAttributesString
|
||||
};
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export async function useStore(
|
|||
maxPrice?: number,
|
||||
productAfter?: string
|
||||
) {
|
||||
const prodVars = reactive<ProdVars>({
|
||||
const variables = reactive<ProdVars>({
|
||||
first: 15,
|
||||
categoriesSlugs: slug,
|
||||
attributes,
|
||||
|
|
@ -29,7 +29,10 @@ export async function useStore(
|
|||
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 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(
|
||||
() => prodVars.productAfter,
|
||||
() => variables.productAfter,
|
||||
async (newCursor, oldCursor) => {
|
||||
if (!newCursor || newCursor === oldCursor) return;
|
||||
await refresh();
|
||||
|
|
@ -48,13 +51,13 @@ export async function useStore(
|
|||
|
||||
watch(
|
||||
[
|
||||
() => prodVars.attributes,
|
||||
() => prodVars.orderBy,
|
||||
() => prodVars.minPrice,
|
||||
() => prodVars.maxPrice
|
||||
() => variables.attributes,
|
||||
() => variables.orderBy,
|
||||
() => variables.minPrice,
|
||||
() => variables.maxPrice
|
||||
],
|
||||
async () => {
|
||||
prodVars.productAfter = '';
|
||||
variables.productAfter = '';
|
||||
await refresh();
|
||||
products.value = data.value?.products.edges ?? [];
|
||||
}
|
||||
|
|
@ -64,6 +67,6 @@ export async function useStore(
|
|||
pending,
|
||||
products,
|
||||
pageInfo,
|
||||
prodVars
|
||||
variables
|
||||
};
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
export * from './useUserActivation'
|
||||
export * from './useUserActivation';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {ACTIVATE_USER} from "@/graphql/mutations/user.js";
|
||||
import {isGraphQLError} from "~/utils/error";
|
||||
import type {IUserActivationResponse} from "~/types";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
|
||||
export function useUserActivation() {
|
||||
const {t} = useI18n();
|
||||
|
|
@ -14,10 +15,10 @@ export function useUserActivation() {
|
|||
const result = await mutate({ token, uid });
|
||||
|
||||
if (result?.data?.activateUser) {
|
||||
ElNotification({
|
||||
message: t("popup.activationSuccess"),
|
||||
type: "success"
|
||||
});
|
||||
useNotification(
|
||||
t("popup.activationSuccess"),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -30,11 +31,11 @@ export function useUserActivation() {
|
|||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
ElNotification({
|
||||
title: t('popup.errors.main'),
|
||||
message,
|
||||
type: 'error'
|
||||
});
|
||||
useNotification(
|
||||
message,
|
||||
'error',
|
||||
t('popup.errors.main')
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './useMailClient'
|
||||
export * from './usePageTitle'
|
||||
export * from './useMailClient';
|
||||
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
|
||||
quantity
|
||||
slug
|
||||
description
|
||||
brand {
|
||||
smallLogo
|
||||
uuid
|
||||
name
|
||||
}
|
||||
category {
|
||||
name
|
||||
slug
|
||||
uuid
|
||||
}
|
||||
images {
|
||||
edges {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const USER_FRAGMENT = gql`
|
|||
firstName
|
||||
lastName
|
||||
phoneNumber
|
||||
dateJoined
|
||||
balance {
|
||||
amount
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
|
|||
|
||||
export const ADD_TO_CART = gql`
|
||||
mutation addToCart(
|
||||
$orderUuid: String!,
|
||||
$productUuid: String!
|
||||
$orderUuid: UUID!,
|
||||
$productUuid: UUID!
|
||||
) {
|
||||
addOrderProduct(
|
||||
orderUuid: $orderUuid,
|
||||
|
|
@ -19,8 +19,8 @@ export const ADD_TO_CART = gql`
|
|||
|
||||
export const REMOVE_FROM_CART = gql`
|
||||
mutation removeFromCart(
|
||||
$orderUuid: String!,
|
||||
$productUuid: String!
|
||||
$orderUuid: UUID!,
|
||||
$productUuid: UUID!
|
||||
) {
|
||||
removeOrderProduct(
|
||||
orderUuid: $orderUuid,
|
||||
|
|
@ -36,8 +36,8 @@ export const REMOVE_FROM_CART = gql`
|
|||
|
||||
export const REMOVE_KIND_FROM_CART = gql`
|
||||
mutation removeKindFromCart(
|
||||
$orderUuid: String!,
|
||||
$productUuid: String!
|
||||
$orderUuid: UUID!,
|
||||
$productUuid: UUID!
|
||||
) {
|
||||
removeOrderProductsOfAKind(
|
||||
orderUuid: $orderUuid,
|
||||
|
|
@ -53,7 +53,7 @@ export const REMOVE_KIND_FROM_CART = gql`
|
|||
|
||||
export const REMOVE_ALL_FROM_CART = gql`
|
||||
mutation removeAllFromCart(
|
||||
$orderUuid: String!
|
||||
$orderUuid: UUID!
|
||||
) {
|
||||
removeAllOrderProducts(
|
||||
orderUuid: $orderUuid
|
||||
|
|
@ -64,4 +64,23 @@ export const REMOVE_ALL_FROM_CART = gql`
|
|||
}
|
||||
}
|
||||
${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`
|
||||
mutation setlanguage(
|
||||
$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`
|
||||
mutation addToWishlist(
|
||||
$wishlistUuid: String!,
|
||||
$productUuid: String!
|
||||
$wishlistUuid: UUID!,
|
||||
$productUuid: UUID!
|
||||
) {
|
||||
addWishlistProduct(
|
||||
wishlistUuid: $wishlistUuid,
|
||||
|
|
@ -19,8 +19,8 @@ export const ADD_TO_WISHLIST = gql`
|
|||
|
||||
export const REMOVE_FROM_WISHLIST = gql`
|
||||
mutation removeFromWishlist(
|
||||
$wishlistUuid: String!,
|
||||
$productUuid: String!
|
||||
$wishlistUuid: UUID!,
|
||||
$productUuid: UUID!
|
||||
) {
|
||||
removeWishlistProduct(
|
||||
wishlistUuid: $wishlistUuid,
|
||||
|
|
@ -36,7 +36,7 @@ export const REMOVE_FROM_WISHLIST = gql`
|
|||
|
||||
export const REMOVE_ALL_FROM_WISHLIST = gql`
|
||||
mutation removeAllFromWishlist(
|
||||
$wishlistUuid: String!
|
||||
$wishlistUuid: UUID!
|
||||
) {
|
||||
removeAllWishlistProducts(
|
||||
wishlistUuid: $wishlistUuid
|
||||
|
|
@ -47,4 +47,23 @@ export const REMOVE_ALL_FROM_WISHLIST = gql`
|
|||
}
|
||||
}
|
||||
${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`
|
||||
query getBrandbyUuid(
|
||||
$uuid: String!
|
||||
$uuid: UUID!
|
||||
) {
|
||||
brands(
|
||||
uuid: $uuid
|
||||
|
|
|
|||
|
|
@ -2,22 +2,24 @@ import {PRODUCT_FRAGMENT} from "@/graphql/fragments/products.fragment.js";
|
|||
|
||||
export const GET_PRODUCTS = gql`
|
||||
query getProducts(
|
||||
$after: String,
|
||||
$productAfter: String,
|
||||
$first: Int,
|
||||
$categoriesSlugs: String,
|
||||
$orderBy: String,
|
||||
$minPrice: Decimal,
|
||||
$maxPrice: Decimal,
|
||||
$productName: String
|
||||
$productName: String,
|
||||
$attributes: String
|
||||
) {
|
||||
products(
|
||||
after: $after,
|
||||
after: $productAfter,
|
||||
first: $first,
|
||||
categoriesSlugs: $categoriesSlugs,
|
||||
orderBy: $orderBy,
|
||||
minPrice: $minPrice,
|
||||
maxPrice: $maxPrice,
|
||||
name: $productName
|
||||
name: $productName,
|
||||
attributes: $attributes
|
||||
) {
|
||||
edges {
|
||||
cursor
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
"buttons": {
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"addToCart": "Add To Cart",
|
||||
"addToCart": "Add to cart",
|
||||
"removeFromCart": "Remove from cart",
|
||||
"send": "Send",
|
||||
"goEmail": "Take me to my inbox",
|
||||
"logout": "Log Out",
|
||||
"buy": "Buy Now",
|
||||
"checkout": "Checkout",
|
||||
"save": "Save",
|
||||
"sendLink": "Send link",
|
||||
"topUp": "Top up"
|
||||
|
|
@ -37,7 +38,8 @@
|
|||
"confirmNewPassword": "Confirm new password"
|
||||
},
|
||||
"checkboxes": {
|
||||
"remember": "Remember me"
|
||||
"remember": "Remember me",
|
||||
"chooseAll": "Choose all"
|
||||
},
|
||||
"popup": {
|
||||
"errors": {
|
||||
|
|
@ -51,17 +53,21 @@
|
|||
"confirmEmail": "Verification E-mail link successfully sent!",
|
||||
"reset": "If specified email exists in our system, we will send a password recovery email!",
|
||||
"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}!",
|
||||
"failAdd": "Please log in to make a purchase",
|
||||
"activationSuccess": "E-mail successfully verified. Please Sign In!",
|
||||
"successUpdate": "Profile successfully updated!",
|
||||
|
||||
"payment": "Your purchase is being processed! Please stand by",
|
||||
"successCheckout": "Order purchase successful!",
|
||||
"addToWishlist": "{product} has been added to the wishlist!"
|
||||
"successCheckout": "Order purchase successful!"
|
||||
},
|
||||
"header": {
|
||||
"actions": {
|
||||
|
|
@ -113,7 +119,10 @@
|
|||
},
|
||||
"breadcrumbs": {
|
||||
"home": "Home",
|
||||
"catalog": "Catalog"
|
||||
"catalog": "Catalog",
|
||||
"contact": "Contact",
|
||||
"wishlist": "Wishlist",
|
||||
"cart": "Cart"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact us"
|
||||
|
|
@ -123,7 +132,8 @@
|
|||
"filters": {
|
||||
"title": "Filters",
|
||||
"apply": "Apply",
|
||||
"reset": "Reset"
|
||||
"reset": "Reset",
|
||||
"all": "All"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
|
@ -131,5 +141,26 @@
|
|||
"categories": "Categories",
|
||||
"brands": "Brands",
|
||||
"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',
|
||||
authHeader: 'X-EVIBES-AUTH',
|
||||
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"
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
|
||||
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz",
|
||||
"integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
|
||||
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.1",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
"@floating-ui/core": "^1.7.2",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -969,9 +969,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@iconify/collections": {
|
||||
"version": "1.0.557",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.557.tgz",
|
||||
"integrity": "sha512-283MvzQA7NgWkjt2AiMURoyW2hUWu0C4c0PjgAOXiQ4lEVQlwIYP8a9FR3xSECfc+EAMq2+M+1kbUlq09doRtw==",
|
||||
"version": "1.0.563",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.563.tgz",
|
||||
"integrity": "sha512-Y5GNkqMQW4RfF5nG7GN9zht2IexXpygFb01T1+xvm/hvEBgn7CDnxO2v3D586VhcA5N93iEBpLVGyV4gaHnZOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iconify/types": "*"
|
||||
|
|
@ -1765,17 +1765,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nuxt/icon": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-1.13.0.tgz",
|
||||
"integrity": "sha512-Sft1DZj/U5Up60DMnhp+hRDNDXRkMhwHocxgDkDkAPBxqR8PRyvzxcMIy3rjGMu0s+fB6ZdLs6vtaWzjWuerQQ==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-1.14.0.tgz",
|
||||
"integrity": "sha512-4kb2rbvbSll784LUme2fDm62NW0Tryr8wADFEU3vIoOj4TZywcwPafIl0MT6ah3RNgbPd174EFVOaUdPSUQENA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iconify/collections": "^1.0.548",
|
||||
"@iconify/collections": "^1.0.560",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@iconify/utils": "^2.3.0",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@nuxt/devtools-kit": "^2.4.1",
|
||||
"@nuxt/kit": "^3.17.3",
|
||||
"@nuxt/devtools-kit": "^2.5.0",
|
||||
"@nuxt/kit": "^3.17.5",
|
||||
"consola": "^3.4.2",
|
||||
"local-pkg": "^1.1.1",
|
||||
"mlly": "^1.7.4",
|
||||
|
|
@ -1783,7 +1783,7 @@
|
|||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"tinyglobby": "^0.2.13"
|
||||
"tinyglobby": "^0.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@nuxt/image": {
|
||||
|
|
@ -3251,13 +3251,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.3.0.tgz",
|
||||
"integrity": "sha512-h5mGRYPbiTZTFP/AKELLPGnUDBly7z7Qd1pgEQlT3ItQ0NlZM0vB+8SOQycpSBOBlgg72Zgw+mi2r+4O/G8RuQ==",
|
||||
"version": "13.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.4.0.tgz",
|
||||
"integrity": "sha512-rwNoE0MNJBUuSzTZcUVrkovtHvpWIySOcC6XpcS33ZarHDNhd9CPvCD4eNl3N0Phz1he1JV0iYULRyPQ5HCbFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "13.3.0",
|
||||
"@vueuse/shared": "13.3.0"
|
||||
"@vueuse/core": "13.4.0",
|
||||
"@vueuse/shared": "13.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"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": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.3.0.tgz",
|
||||
|
|
@ -6531,6 +6569,20 @@
|
|||
"license": "MIT",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -7764,6 +7816,257 @@
|
|||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -13,13 +13,17 @@
|
|||
|
||||
<script setup>
|
||||
import {useBrandByUuid} from "~/composables/brands";
|
||||
import {usePageTitle} from "~/composables/utils/index.js";
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const slug = computed(() => route.params.uuid)
|
||||
|
||||
const { setPageTitle } = usePageTitle();
|
||||
|
||||
const { brand } = await useBrandByUuid(slug.value);
|
||||
|
||||
setPageTitle(brand.value?.name ?? 'Brand');
|
||||
// TODO: add product by this brand
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,28 +11,28 @@
|
|||
import {useUserActivation} from "~/composables/user";
|
||||
import { useRouteQuery } from '@vueuse/router';
|
||||
|
||||
const {t} = useI18n()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const {t} = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const route = useRoute();
|
||||
|
||||
useHead({
|
||||
title: t('breadcrumbs.home'),
|
||||
})
|
||||
|
||||
const token = useRouteQuery('token', '')
|
||||
const uid = useRouteQuery('uid', '')
|
||||
const token = useRouteQuery('token', '');
|
||||
const uid = useRouteQuery('uid', '');
|
||||
|
||||
const { activateUser } = useUserActivation();
|
||||
|
||||
onMounted( async () => {
|
||||
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) {
|
||||
appStore.setActiveState('new-password')
|
||||
appStore.setActiveState('new-password');
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,537 @@
|
|||
<template>
|
||||
<div class="product" v-if="product">
|
||||
<ui-title>{{ product?.name }}</ui-title>
|
||||
<div class="container">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useProductBySlug} from "~/composables/products";
|
||||
import {useProductBySlug, useProducts} from "~/composables/products";
|
||||
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)
|
||||
setPageTitle(product.value?.name ?? 'Product')
|
||||
const { product } = await useProductBySlug(slug.value);
|
||||
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(
|
||||
() => route.params.slug,
|
||||
async (newSlug) => {
|
||||
if (typeof newSlug === 'string') {
|
||||
const { product } = await useProductBySlug(newSlug)
|
||||
setPageTitle(product.value?.name ?? 'Product')
|
||||
const { product } = await useProductBySlug(newSlug);
|
||||
setPageTitle(product.value?.name ?? 'Product');
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<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>
|
||||
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
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
quantity: number,
|
||||
slug: string,
|
||||
description: string,
|
||||
brand: {
|
||||
smallLogo: string,
|
||||
uuid: string,
|
||||
name: string
|
||||
}
|
||||
category: {
|
||||
name: string
|
||||
slug: string,
|
||||
|
|
@ -27,8 +33,8 @@ export interface IProduct {
|
|||
values: {
|
||||
value: string,
|
||||
uuid: string
|
||||
}
|
||||
}
|
||||
}[]
|
||||
}[]
|
||||
}
|
||||
}[]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export interface IUser {
|
|||
firstName: string,
|
||||
lastName: string,
|
||||
phoneNumber: string,
|
||||
dateJoined: string,
|
||||
balance: {
|
||||
amount: number,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue