Features: 1) Introduce handleDeposit function with validation logic and deposit transaction flow; 2) Add useDeposit composable and balance.vue page for user account balance management; 3) Enhance wishlist and cart functionality with authentication checks and notification improvements;

Fixes: 1) Replace `ElNotification` with `useNotification` across all components and composables; 2) Add missing semicolons, consistent formatting, and type annotations in multiple files; 3) Resolve non-reactive elements in wishlist and cart state management;

Extra: 1) Update i18n translations with new strings for promocodes, balance, authentication, and profile settings; 2) Refactor SCSS styles including variable additions and component-specific tweaks; 3) Remove redundant queries, unused imports, and `storePage.ts` file for cleanup.
This commit is contained in:
Alexandr SaVBaD Waltz 2025-07-08 23:41:31 +03:00
parent 761fecf67f
commit c60ac13e88
62 changed files with 1560 additions and 563 deletions

View file

@ -68,12 +68,20 @@ await Promise.all([
watch( watch(
() => appStore.activeState, () => appStore.activeState,
(state) => { (state) => {
appStore.setOverflowHidden(state !== '') appStore.setOverflowHidden(state !== '');
}, },
{ immediate: true } { immediate: true }
) );
let stopWatcher: VoidFunction = () => {} watch(locale, () => {
useHead({
htmlAttrs: {
lang: locale.value
}
});
});
let stopWatcher: VoidFunction = () => {};
onMounted( async () => { onMounted( async () => {
refreshInterval = setInterval(async () => { refreshInterval = setInterval(async () => {

View file

@ -8,5 +8,6 @@ $accentDark: #5743b5;
$accentLight: #a69cdc; $accentLight: #a69cdc;
$accentDisabled: #826fa2; $accentDisabled: #826fa2;
$accentSmooth: #656bd1; $accentSmooth: #656bd1;
$contrast: #FFC107;
$error: #f13838; $error: #f13838;
$default_border_radius: 4px; $default_border_radius: 4px;

View file

@ -39,7 +39,7 @@
@click="appStore.setActiveState('login')" @click="appStore.setActiveState('login')"
v-else v-else
> >
<icon name="material-symbols-light:person-outline-rounded" size="32" /> <icon name="material-symbols-light:person-outline-rounded" size="32" />
<p>{{ t('header.actions.login') }}</p> <p>{{ t('header.actions.login') }}</p>
</div> </div>
<template #fallback> <template #fallback>
@ -47,7 +47,7 @@
class="header__actions-item" class="header__actions-item"
@click="appStore.setActiveState('login')" @click="appStore.setActiveState('login')"
> >
<icon name="material-symbols-light:person-outline-rounded" size="32" /> <icon name="material-symbols-light:person-outline-rounded" size="32" />
<p>{{ t('header.actions.login') }}</p> <p>{{ t('header.actions.login') }}</p>
</div> </div>
</template> </template>
@ -58,8 +58,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useLogout} from "~/composables/auth";
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore(); const userStore = useUserStore();
@ -79,8 +77,6 @@ const productsInCartQuantity = computed(() => {
const productsInWishlistQuantity = computed(() => { const productsInWishlistQuantity = computed(() => {
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges.length : 0; return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges.length : 0;
}); });
const { logout } = useLogout();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -10,6 +10,7 @@
type="text" type="text"
v-model="query" v-model="query"
:placeholder="t('fields.search')" :placeholder="t('fields.search')"
inputmode="search"
/> />
<div class="search__tools"> <div class="search__tools">
<button <button

View file

@ -47,7 +47,7 @@
</div> </div>
</nuxt-link-locale> </nuxt-link-locale>
<div class="card__content"> <div class="card__content">
<div class="card__price">{{ product.price }}</div> <div class="card__price">{{ product.price }} {{ CURRENCY }}</div>
<p class="card__name">{{ product.name }}</p> <p class="card__name">{{ product.name }}</p>
<el-rate <el-rate
v-model="rating" v-model="rating"
@ -58,40 +58,65 @@
</div> </div>
</div> </div>
<div class="card__bottom"> <div class="card__bottom">
<ui-button <div class="card__bottom-inner">
class="card__bottom-button" <ui-button
v-if="isProductInCart" class="card__bottom-button"
@click="overwriteOrder({ v-if="isProductInCart"
type: 'remove', @click="overwriteOrder({
productUuid: product.uuid, type: 'remove',
productName: product.name productUuid: product.uuid,
})" productName: product.name
:isLoading="removeLoading" })"
> :isLoading="removeLoading"
{{ t('buttons.removeFromCart') }} >
</ui-button> {{ t('buttons.removeFromCart') }}
<ui-button </ui-button>
v-else <ui-button
class="card__bottom-button" v-else
@click="overwriteOrder({ class="card__bottom-button"
type: 'add', @click="overwriteOrder({
productUuid: product.uuid, type: 'add',
productName: product.name productUuid: product.uuid,
})" productName: product.name
:isLoading="addLoading" })"
> :isLoading="addLoading"
{{ t('buttons.addToCart') }} >
</ui-button> {{ t('buttons.addToCart') }}
<div </ui-button>
class="card__bottom-wishlist" <div
@click="overwriteWishlist({ class="card__bottom-wishlist"
type: (isProductInWishlist ? 'remove' : 'add'), @click="overwriteWishlist({
productUuid: product.uuid, type: (isProductInWishlist ? 'remove' : 'add'),
productName: product.name 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 /> <icon name="mdi:cards-heart" size="28" v-if="isProductInWishlist" />
<icon name="mdi:cards-heart-outline" size="28" v-else />
</div>
</div>
<div class="tools" v-if="isToolsVisible && isProductInCart">
<button
class="tools__item tools__item-button"
@click="overwriteOrder({
type: 'remove',
productUuid: product.uuid,
productName: product.name
})"
>
-
</button>
<span class="tools__item tools__item-count" v-text="'X' + productinCartQuantity" />
<button
class="tools__item tools__item-button"
@click="overwriteOrder({
type: 'add',
productUuid: product.uuid,
productName: product.name
})"
>
+
</button>
</div> </div>
</div> </div>
</div> </div>
@ -106,10 +131,12 @@ import 'swiper/css/effect-fade';
import 'swiper/css/pagination' import 'swiper/css/pagination'
import {useWishlistOverwrite} from "~/composables/wishlist"; import {useWishlistOverwrite} from "~/composables/wishlist";
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite"; import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
import {CURRENCY} from "~/config/constants";
const props = defineProps<{ const props = defineProps<{
product: IProduct; product: IProduct;
isList?: boolean; isList?: boolean;
isToolsVisible?: boolean;
}>(); }>();
const {t} = useI18n(); const {t} = useI18n();
@ -129,6 +156,9 @@ const isProductInWishlist = computed(() => {
const isProductInCart = computed(() => { const isProductInCart = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid); return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === props.product?.uuid);
}); });
const productinCartQuantity = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.filter((prod) => prod.node.product.uuid === props.product.uuid)[0].node.quantity;
});
const rating = computed(() => { const rating = computed(() => {
return props.product.feedbacks.edges[0]?.node?.rating ?? 5; return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
@ -173,7 +203,7 @@ function goTo(index: number) {
&__list { &__list {
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: stretch;
justify-content: space-between; justify-content: space-between;
padding: 10px; padding: 10px;
@ -191,10 +221,17 @@ function goTo(index: number) {
margin-top: 0; margin-top: 0;
width: fit-content; width: fit-content;
flex-shrink: 0; flex-shrink: 0;
padding-inline: 0; padding: 0;
display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between;
align-items: flex-end; align-items: flex-end;
gap: 10px;
&-inner {
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
&-button { &-button {
width: fit-content; width: fit-content;
@ -286,7 +323,7 @@ function goTo(index: number) {
&__quantity { &__quantity {
width: fit-content; width: fit-content;
background-color: rgba($accent, 0.2); background-color: rgba($contrast, 0.5);
border-radius: $default_border_radius; border-radius: $default_border_radius;
padding: 5px 10px; padding: 5px 10px;
font-size: 14px; font-size: 14px;
@ -295,12 +332,15 @@ function goTo(index: number) {
&__bottom { &__bottom {
margin-top: auto; margin-top: auto;
padding: 0 20px 20px 20px; padding: 0 20px 20px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
max-width: 100%; max-width: 100%;
&-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
}
&-button { &-button {
width: 84%; width: 84%;
} }
@ -326,6 +366,52 @@ function goTo(index: number) {
} }
} }
.tools {
width: 100%;
border-radius: 4px;
background-color: rgba($accent, 0.2);
display: grid;
grid-template-columns: 1fr 2fr 1fr;
height: 30px;
&__item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
&-count {
border-left: 1px solid $accent;
border-right: 1px solid $accent;
color: $accent;
font-size: 18px;
font-weight: 600;
}
&-button {
cursor: pointer;
background-color: rgba($accent, 0.2);
border-radius: 4px 0 0 4px;
transition: 0.2s;
color: $accent;
font-size: 20px;
font-weight: 500;
@include hover {
background-color: $accent;
color: $white;
}
&:last-child {
border-radius: 0 4px 4px 0;
}
}
}
}
:deep(.swiper-pagination) { :deep(.swiper-pagination) {
bottom: 0; bottom: 0;
display: flex; display: flex;

View file

@ -11,12 +11,14 @@
:placeholder="t('fields.email')" :placeholder="t('fields.email')"
:rules="[required]" :rules="[required]"
v-model="email" v-model="email"
:inputMode="'email'"
/> />
<ui-input <ui-input
:type="'text'" :type="'text'"
:placeholder="t('fields.phoneNumber')" :placeholder="t('fields.phoneNumber')"
:rules="[required]" :rules="[required]"
v-model="phoneNumber" v-model="phoneNumber"
:inputMode="'tel'"
/> />
<ui-input <ui-input
:type="'text'" :type="'text'"

View file

@ -1,67 +1,77 @@
<template> <template>
<!-- <form @submit.prevent="handleDeposit()" class="form">--> <form @submit.prevent="handleDeposit()" class="form">
<form @submit.prevent="" class="form">
<div class="form__box"> <div class="form__box">
<ui-input <ui-input
:type="'text'" :type="'text'"
:placeholder="''" :placeholder="''"
v-model="amount" v-model="amount"
:numberOnly="true" :numberOnly="true"
:inputMode="'decimal'"
/> />
<icon name="ic:baseline-compare-arrows" size="30" />
<ui-input <ui-input
:type="'text'" :type="'text'"
:placeholder="''" :placeholder="''"
v-model="amount" v-model="amount"
:numberOnly="true" :numberOnly="true"
:inputMode="'decimal'"
/> />
</div> </div>
<!-- <ui-button-->
<!-- class="form__button"-->
<!-- :isDisabled="!isFormValid"-->
<!-- :isLoading="loading"-->
<!-- >-->
<!-- {{ $t('buttons.topUp') }}-->
<!-- </ui-button>-->
<ui-button <ui-button
class="form__button" class="form__button"
:isDisabled="!isFormValid" :isDisabled="!isFormValid"
:isLoading="loading"
> >
{{ t('buttons.topUp') }} {{ t('buttons.topUp') }}
</ui-button> </ui-button>
</form> </form>
</template> </template>
<script setup> <script setup lang="ts">
// import {useDeposit} from "@/composables/user/useDeposit.js"; import {useDeposit} from "~/composables/user";
const { t } = useI18n() const { t } = useI18n();
const companyStore = useCompanyStore() const companyStore = useCompanyStore();
const paymentMin = computed(() => companyStore.companyInfo?.paymentGatewayMinimum) const paymentMin = computed(() => companyStore.companyInfo?.paymentGatewayMinimum || 0);
const paymentMax = computed(() => companyStore.companyInfo?.paymentGatewayMaximum) const paymentMax = computed(() => companyStore.companyInfo?.paymentGatewayMaximum || 500);
const amount = ref('') const amount = ref<string>("0");
const isFormValid = computed(() => { const isFormValid = computed(() => {
return ( return (
amount.value >= paymentMin.value && amount.value >= paymentMin.value &&
amount.value <= paymentMax.value amount.value <= paymentMax.value
) );
}) });
// const { deposit, loading } = useDeposit(); const { deposit, loading } = useDeposit();
//
// async function handleDeposit() { async function handleDeposit() {
// await deposit(amount.value); await deposit(amount.value);
// } }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.form { .form {
display: flex;
flex-direction: column;
gap: 20px;
&__box { &__box {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 20px; gap: 20px;
& span {
flex-shrink: 0;
align-self: center;
}
}
&__button {
width: fit-content;
padding-inline: 20px;
} }
} }
</style> </style>

View file

@ -6,6 +6,7 @@
:placeholder="t('fields.email')" :placeholder="t('fields.email')"
:rules="[isEmail]" :rules="[isEmail]"
v-model="email" v-model="email"
:inputMode="'email'"
/> />
<ui-input <ui-input
:type="'password'" :type="'password'"
@ -48,7 +49,7 @@ import {useValidators} from "~/composables/rules";
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
const { required, isEmail } = useValidators() const { required, isEmail } = useValidators();
const email = ref<string>(''); const email = ref<string>('');
const password = ref<string>(''); const password = ref<string>('');
@ -58,7 +59,7 @@ const isFormValid = computed(() => {
return ( return (
isEmail(email.value) === true && isEmail(email.value) === true &&
required(password.value) === true required(password.value) === true
) );
}); });
const { login, loading } = useLogin(); const { login, loading } = useLogin();

View file

@ -21,12 +21,14 @@
:placeholder="t('fields.phoneNumber')" :placeholder="t('fields.phoneNumber')"
:rules="[required]" :rules="[required]"
v-model="phoneNumber" v-model="phoneNumber"
:inputMode="'tel'"
/> />
<ui-input <ui-input
:type="'email'" :type="'email'"
:placeholder="t('fields.email')" :placeholder="t('fields.email')"
:rules="[isEmail]" :rules="[isEmail]"
v-model="email" v-model="email"
:inputMode="'email'"
/> />
</div> </div>
<ui-input <ui-input
@ -59,26 +61,29 @@
</form> </form>
</template> </template>
<script setup> <script setup lang="ts">
import {useValidators} from "~/composables/rules"; import {useValidators} from "~/composables/rules";
import {useRegister} from "~/composables/auth/index.js"; import {useRegister} from "~/composables/auth/index.js";
import {useRouteQuery} from "@vueuse/router";
const { t } = useI18n() const { t } = useI18n();
const appStore = useAppStore() const appStore = useAppStore();
const { required, isEmail, isPasswordValid } = useValidators() const { required, isEmail, isPasswordValid } = useValidators();
const firstName = ref('') const firstName = ref<string>('');
const lastName = ref('') const lastName = ref<string>('');
const phoneNumber = ref('') const phoneNumber = ref<string>('');
const email = ref('') const email = ref<string>('');
const password = ref('') const password = ref<string>('');
const confirmPassword = ref('') const confirmPassword = ref<string>('');
const compareStrings = (v) => { const referrer = useRouteQuery('referrer', '');
if (v === password.value) return true
return t('errors.compare') const compareStrings = (v: string) => {
} if (v === password.value) return true;
return t('errors.compare');
};
const isFormValid = computed(() => { const isFormValid = computed(() => {
return ( return (
@ -88,20 +93,21 @@ const isFormValid = computed(() => {
isEmail(email.value) === true && isEmail(email.value) === true &&
isPasswordValid(password.value) === true && isPasswordValid(password.value) === true &&
compareStrings(confirmPassword.value) === true compareStrings(confirmPassword.value) === true
) );
}) });
const { register, loading } = useRegister(); const { register, loading } = useRegister();
async function handleRegister() { async function handleRegister() {
await register( await register({
firstName.value, firstName: firstName.value,
lastName.value, lastName: lastName.value,
phoneNumber.value, phoneNumber: phoneNumber.value,
email.value, email: email.value,
password.value, password: password.value,
confirmPassword.value confirmPassword: confirmPassword.value,
); referrer: referrer.value
});
} }
</script> </script>
@ -118,7 +124,7 @@ async function handleRegister() {
&__box { &__box {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 20px; gap: 20px;
} }

View file

@ -6,6 +6,7 @@
:placeholder="t('fields.email')" :placeholder="t('fields.email')"
:rules="[isEmail]" :rules="[isEmail]"
v-model="email" v-model="email"
:inputMode="'email'"
/> />
<ui-button <ui-button
class="form__button" class="form__button"

View file

@ -1,101 +1,98 @@
<template> <template>
<!-- <form class="form" @submit.prevent="handleUpdate()">--> <form class="form" @submit.prevent="handleUpdate">
<form class="form" @submit.prevent=""> <div class="form__box">
<ui-input <ui-input
:type="'text'" :type="'text'"
:placeholder="t('fields.firstName')" :placeholder="t('fields.firstName')"
:rules="[required]" :rules="[required]"
v-model="firstName" v-model="firstName"
/> />
<ui-input <ui-input
:type="'text'" :type="'text'"
:placeholder="t('fields.lastName')" :placeholder="t('fields.lastName')"
:rules="[required]" :rules="[required]"
v-model="lastName" v-model="lastName"
/> />
<ui-input </div>
:type="'email'" <div class="form__box">
:placeholder="t('fields.email')" <ui-input
:rules="[isEmail]" :type="'email'"
v-model="email" :placeholder="t('fields.email')"
/> :rules="[isEmail]"
<ui-input v-model="email"
:type="'text'" />
:placeholder="t('fields.phoneNumber')" <ui-input
:rules="[required]" :type="'text'"
v-model="phoneNumber" :placeholder="t('fields.phoneNumber')"
/> :rules="[required]"
<ui-input v-model="phoneNumber"
:type="'password'" />
:placeholder="t('fields.newPassword')" </div>
:rules="[isPasswordValid]" <div class="form__box">
v-model="password" <ui-input
/> :type="'password'"
<ui-input :placeholder="t('fields.newPassword')"
:type="'password'" :rules="[isPasswordValid]"
:placeholder="t('fields.confirmNewPassword')" v-model="password"
:rules="[compareStrings]" />
v-model="confirmPassword" <ui-input
/> :type="'password'"
<!-- <ui-button--> :placeholder="t('fields.confirmNewPassword')"
<!-- class="form__button"--> :rules="[compareStrings]"
<!-- :isLoading="loading"--> v-model="confirmPassword"
<!-- >--> />
<!-- {{ t('buttons.save') }}--> </div>
<!-- </ui-button>-->
<ui-button <ui-button
class="form__button" class="form__button"
:isLoading="loading"
> >
{{ t('buttons.save') }} {{ t('buttons.save') }}
</ui-button> </ui-button>
</form> </form>
</template> </template>
<script setup> <script setup lang="ts">
import {useValidators} from "~/composables/rules"; import {useValidators} from "~/composables/rules";
// import {useUserUpdating} from "@/composables/user"; import {useUserUpdating} from "~/composables/user/index.js";
const { t } = useI18n() const { t } = useI18n();
const userStore = useUserStore() const userStore = useUserStore();
const { required, isEmail, isPasswordValid } = useValidators() const { required, isEmail, isPasswordValid } = useValidators();
const userFirstName = computed(() => userStore.user?.firstName) const user = computed(() => userStore.user);
const userLastName = computed(() => userStore.user?.lastName)
const userEmail = computed(() => userStore.user?.email)
const userPhoneNumber = computed(() => userStore.user?.phoneNumber)
const firstName = ref('') const firstName = ref<string>('');
const lastName = ref('') const lastName = ref<string>('');
const email = ref('') const email = ref<string>('');
const phoneNumber = ref('') const phoneNumber = ref<string>('');
const password = ref('') const password = ref<string>('');
const confirmPassword = ref('') const confirmPassword = ref<string>('');
const compareStrings = (v) => { const compareStrings = (v: string) => {
if (v === password.value) return true if (v === password.value) return true;
return t('errors.compare') return t('errors.compare');
};
const { updateUser, loading } = useUserUpdating();
watchEffect(() => {
firstName.value = user.value?.firstName || '';
lastName.value = user.value?.lastName || '';
email.value = user.value?.email || '';
phoneNumber.value = user.value?.phoneNumber || '';
});
async function handleUpdate() {
await updateUser(
firstName.value,
lastName.value,
email.value,
phoneNumber.value,
password.value,
confirmPassword.value,
);
} }
// const { updateUser, loading } = useUserUpdating();
//
// watchEffect(() => {
// firstName.value = userFirstName.value || ''
// lastName.value = userLastName.value || ''
// email.value = userEmail.value || ''
// phoneNumber.value = userPhoneNumber.value || ''
// })
//
// async function handleUpdate() {
// await updateUser(
// firstName.value,
// lastName.value,
// email.value,
// phoneNumber.value,
// password.value,
// confirmPassword.value,
// );
// }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -103,5 +100,16 @@ const compareStrings = (v) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
&__box {
display: flex;
align-items: flex-start;
gap: 20px;
}
&__button {
width: fit-content;
padding-inline: 20px;
}
} }
</style> </style>

View file

@ -1,62 +1,91 @@
<template> <template>
<nav class="nav"> <nav class="nav">
<nuxt-link-locale <div class="nav__inner">
class="nav__item" <nuxt-link-locale
:class="[{ active: route.path.includes('settings') }]" class="nav__item"
to="/profile/settings" :class="[{ active: route.path.includes('settings') }]"
> to="/profile/settings"
<icon name="ic:baseline-settings" size="20" /> >
{{ t('profile.settings.title') }} <icon name="ic:baseline-settings" size="20" />
</nuxt-link-locale> {{ t('profile.settings.title') }}
<nuxt-link-locale </nuxt-link-locale>
class="nav__item" <nuxt-link-locale
:class="[{ active: route.path.includes('orders') }]" class="nav__item"
to="/profile/orders" :class="[{ active: route.path.includes('orders') }]"
> to="/profile/orders"
<icon name="material-symbols:order-approve-rounded" size="20" /> >
{{ t('profile.orders.title') }} <icon name="material-symbols:order-approve-rounded" size="20" />
</nuxt-link-locale> {{ t('profile.orders.title') }}
<nuxt-link-locale </nuxt-link-locale>
class="nav__item" <nuxt-link-locale
:class="[{ active: route.path.includes('wishlist') }]" class="nav__item"
to="/profile/wishlist" :class="[{ active: route.path.includes('wishlist') }]"
> to="/profile/wishlist"
<icon name="mdi:cards-heart-outline" size="20" /> >
{{ t('profile.wishlist.title') }} <icon name="mdi:cards-heart-outline" size="20" />
</nuxt-link-locale> {{ t('profile.wishlist.title') }}
<nuxt-link-locale </nuxt-link-locale>
class="nav__item" <nuxt-link-locale
:class="[{ active: route.path.includes('cart') }]" class="nav__item"
to="/profile/cart" :class="[{ active: route.path.includes('cart') }]"
> to="/profile/cart"
<icon name="ph:shopping-cart-light" size="20" /> >
{{ t('profile.cart.title') }} <icon name="ph:shopping-cart-light" size="20" />
</nuxt-link-locale> {{ t('profile.cart.title') }}
</nuxt-link-locale>
<nuxt-link-locale
class="nav__item"
:class="[{ active: route.path.includes('balance') }]"
to="/profile/balance"
>
<icon name="ic:outline-attach-money" size="20" />
{{ t('profile.balance.title') }}
</nuxt-link-locale>
<nuxt-link-locale
class="nav__item"
:class="[{ active: route.path.includes('promocodes') }]"
to="/profile/promocodes"
>
<icon name="fluent:ticket-20-filled" size="20" />
{{ t('profile.promocodes.title') }}
</nuxt-link-locale>
</div>
<div class="nav__logout" @click="logout">
<icon name="material-symbols:power-settings-new-outline" size="20" />
{{ t('profile.logout') }}
</div>
</nav> </nav>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useLogout} from "~/composables/auth";
const {t} = useI18n(); const {t} = useI18n();
const route = useRoute(); const route = useRoute();
const { logout } = useLogout();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.nav { .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; position: sticky;
top: 141px; top: 141px;
width: max-content; width: max-content;
height: fit-content; height: fit-content;
&__inner {
background-color: $white;
border-radius: $default_border_radius;
padding-block: 7px;
display: flex;
flex-direction: column;
gap: 5px;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
}
&__item { &__item {
cursor: pointer; cursor: pointer;
padding: 5px 30px 5px 20px; padding: 7px 30px 7px 10px;
border-left: 2px solid $white; border-left: 2px solid $white;
display: flex; display: flex;
align-items: center; align-items: center;
@ -64,6 +93,7 @@ const route = useRoute();
transition: 0.2s; transition: 0.2s;
color: $accent; color: $accent;
font-size: 18px;
font-weight: 600; font-weight: 600;
@include hover { @include hover {
@ -73,6 +103,29 @@ const route = useRoute();
&.active { &.active {
border-color: $accent; border-color: $accent;
color: $accentDark; color: $accentDark;
background-color: rgba($accent, 0.2);
}
}
&__logout {
cursor: pointer;
margin-top: 25px;
border-radius: $default_border_radius;
background-color: rgba($accent, 0.2);
border: 1px solid $accent;
padding: 7px 20px;
display: flex;
align-items: center;
gap: 10px;
transition: 0.2s;
color: $accent;
font-size: 16px;
font-weight: 600;
@include hover {
background-color: $accent;
color: $white;
} }
} }
} }

View file

@ -8,6 +8,7 @@
@input="onInput" @input="onInput"
@keydown="numberOnly ? onlyNumbersKeydown($event) : null" @keydown="numberOnly ? onlyNumbersKeydown($event) : null"
class="block__input" class="block__input"
:inputmode="inputMode || 'text'"
> >
<button <button
@click.prevent="togglePasswordVisible" @click.prevent="togglePasswordVisible"
@ -31,9 +32,10 @@ const emit = defineEmits<{
const props = defineProps<{ const props = defineProps<{
type: string, type: string,
placeholder: string, placeholder: string,
modelValue?: [string, number], modelValue?: string | number,
rules?: Rule[], rules?: Rule[],
numberOnly?: boolean numberOnly?: boolean,
inputMode?: "text" | "email" | "search" | "tel" | "url" | "none" | "numeric" | "decimal"
}>(); }>();
const isPasswordVisible = ref(props.type); const isPasswordVisible = ref(props.type);
@ -108,7 +110,7 @@ function onInput(e: Event) {
line-height: 20px; line-height: 20px;
&::placeholder { &::placeholder {
color: #2B2B2B; color: #575757;
} }
} }

View file

@ -13,9 +13,9 @@
:src="currentLocale.flag" :src="currentLocale.flag"
:alt="currentLocale.code" :alt="currentLocale.code"
/> />
<skeletons-ui-language-switcher v-else /> <!-- <skeletons-ui-language-switcher v-else />-->
<template #fallback> <template #fallback>
<skeletons-ui-language-switcher /> <!-- <skeletons-ui-language-switcher />-->
</template> </template>
</client-only> </client-only>
</div> </div>
@ -31,7 +31,7 @@
:key="locale.code" :key="locale.code"
format="webp" format="webp"
densities="x1" densities="x1"
@click="switchLanguage(locale.code)" @click="uiSwitchLanguage(locale.code)"
:src="locale.flag" :src="locale.flag"
:alt="locale.code" :alt="locale.code"
/> />
@ -45,20 +45,25 @@
import {onClickOutside} from "@vueuse/core"; import {onClickOutside} from "@vueuse/core";
import {useLanguageSwitch} from "@/composables/languages/index.js"; import {useLanguageSwitch} from "@/composables/languages/index.js";
const languageStore = useLanguageStore() const languageStore = useLanguageStore();
const locales = computed(() => languageStore.languages) const locales = computed(() => languageStore.languages);
const currentLocale = computed(() => languageStore.currentLocale) const currentLocale = computed(() => languageStore.currentLocale);
const isSwitcherVisible = ref<boolean>(false) const isSwitcherVisible = ref<boolean>(false);
const setSwitcherVisible = (state) => { const setSwitcherVisible = (state: boolean) => {
isSwitcherVisible.value = state isSwitcherVisible.value = state;
} };
const switcherRef = ref(null) const switcherRef = ref(null);
onClickOutside(switcherRef, () => isSwitcherVisible.value = false) onClickOutside(switcherRef, () => isSwitcherVisible.value = false);
const { switchLanguage } = useLanguageSwitch() const { switchLanguage } = useLanguageSwitch();
const uiSwitchLanguage = (localeCode: string) => {
switchLanguage(localeCode);
setSwitcherVisible(false);
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -5,19 +5,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const router = useRouter()
const props = defineProps<{ const props = defineProps<{
routePath: string routePath: string
}>() }>();
const router = useRouter();
const redirect = () => { const redirect = () => {
if (props.routePath) { if (props.routePath) {
router.push({ router.push({
path: props.routePath path: props.routePath
}) });
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -9,6 +9,7 @@ import { useUserStore } from '~/stores/user';
import { useAppStore } from '~/stores/app'; import { useAppStore } from '~/stores/app';
import {DEFAULT_LOCALE} from "~/config/constants"; import {DEFAULT_LOCALE} from "~/config/constants";
import {useNotification} from "~/composables/notification"; import {useNotification} from "~/composables/notification";
import {usePromocodes} from "~/composables/promocodes";
export function useLogin() { export function useLogin() {
const { t } = useI18n(); const { t } = useI18n();
@ -56,12 +57,14 @@ export function useLogin() {
} }
userStore.setUser(authData.user); userStore.setUser(authData.user);
cookieAccess.value = authData.accessToken cookieAccess.value = authData.accessToken;
useNotification( appStore.unsetActiveState();
t('popup.success.login'),
'success' useNotification({
); message: t('popup.success.login'),
type: 'success'
});
if (authData.user.language !== cookieLocale.value) { if (authData.user.language !== cookieLocale.value) {
await checkAndRedirect(authData.user.language); await checkAndRedirect(authData.user.language);
@ -69,8 +72,8 @@ export function useLogin() {
await useWishlist(); await useWishlist();
await usePendingOrder(authData.user.email); await usePendingOrder(authData.user.email);
await usePromocodes();
appStore.unsetActiveState(); //TODO: combine three requests
} }
watch(error, (err) => { watch(error, (err) => {
@ -82,11 +85,11 @@ export function useLogin() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return { return {

View file

@ -26,10 +26,10 @@ export function useNewPassword() {
}); });
if (result?.data?.confirmResetPassword.success) { if (result?.data?.confirmResetPassword.success) {
useNotification( useNotification({
t('popup.success.newPassword'), message: t('popup.success.newPassword'),
'success' type: 'success'
); });
await router.push({path: '/'}) await router.push({path: '/'})
@ -46,11 +46,11 @@ export function useNewPassword() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return { return {

View file

@ -17,10 +17,10 @@ export function usePasswordReset() {
}); });
if (result?.data?.resetPassword.success) { if (result?.data?.resetPassword.success) {
useNotification( useNotification({
t('popup.success.reset'), message: t('popup.success.reset'),
'success' type: 'success'
); });
appStore.unsetActiveState(); appStore.unsetActiveState();
} }
@ -35,11 +35,11 @@ export function usePasswordReset() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return { return {

View file

@ -7,6 +7,7 @@ import { useUserStore } from '~/stores/user';
import { isGraphQLError } from '~/utils/error'; import { isGraphQLError } from '~/utils/error';
import {DEFAULT_LOCALE} from "~/config/constants"; import {DEFAULT_LOCALE} from "~/config/constants";
import {useNotification} from "~/composables/notification"; import {useNotification} from "~/composables/notification";
import {usePromocodes} from "~/composables/promocodes";
export function useRefresh() { export function useRefresh() {
const { t } = useI18n(); const { t } = useI18n();
@ -52,6 +53,8 @@ export function useRefresh() {
await useWishlist(); await useWishlist();
await usePendingOrder(data.user.email); await usePendingOrder(data.user.email);
await usePromocodes();
//TODO: combine three requests
} }
watch(error, (err) => { watch(error, (err) => {
@ -63,11 +66,11 @@ export function useRefresh() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}) })
return { return {

View file

@ -4,6 +4,16 @@ import {isGraphQLError} from "~/utils/error";
import type {IRegisterResponse} from "~/types"; import type {IRegisterResponse} from "~/types";
import {useNotification} from "~/composables/notification"; import {useNotification} from "~/composables/notification";
interface IRegisterArguments {
firstName: string,
lastName: string,
phoneNumber: string,
email: string,
password: string,
confirmPassword: string,
referrer: string
}
export function useRegister() { export function useRegister() {
const {t} = useI18n(); const {t} = useI18n();
const appStore = useAppStore(); const appStore = useAppStore();
@ -13,41 +23,37 @@ export function useRegister() {
const { mutate, loading, error } = useMutation<IRegisterResponse>(REGISTER); const { mutate, loading, error } = useMutation<IRegisterResponse>(REGISTER);
async function register( async function register(
firstName: string, payload: IRegisterArguments
lastName: string,
phoneNumber: string,
email: string,
password: string,
confirmPassword: string
) { ) {
const result = await mutate({ const result = await mutate({
firstName, firstName: payload.firstName,
lastName, lastName: payload.lastName,
phoneNumber, phoneNumber: payload.phoneNumber,
email, email: payload.email,
password, password: payload.password,
confirmPassword confirmPassword: payload.confirmPassword,
referrer: payload.referrer
}); });
if (result?.data?.createUser?.success) { if (result?.data?.createUser?.success) {
detectMailClient(email); detectMailClient(payload.email);
useNotification( useNotification({
h('div', [ message: h('div', [
h('p', t('popup.success.register')), h('p', t('popup.success.register')),
mailClientUrl.value ? h( mailClientUrl.value ? h(
'button', 'button',
{ {
class: 'el-notification__button', class: 'el-notification__button',
onClick: () => { onClick: () => {
openMailClient() openMailClient();
} }
}, },
t('buttons.goEmail') t('buttons.goEmail')
) : '' ) : ''
]), ]),
'success' type: 'success'
); });
appStore.unsetActiveState(); appStore.unsetActiveState();
} }
@ -62,11 +68,11 @@ export function useRegister() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}) })
return { return {

View file

@ -1,10 +1,12 @@
export const useAppConfig = () => { export const useAppConfig = () => {
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const APP_DOMAIN: string = runtimeConfig.public.evibesBaseDomain;
const APP_NAME: string = runtimeConfig.public.evibesProjectName; const APP_NAME: string = runtimeConfig.public.evibesProjectName;
const APP_NAME_KEY: string = APP_NAME.toLowerCase(); const APP_NAME_KEY: string = APP_NAME.toLowerCase();
return { return {
APP_DOMAIN,
APP_NAME, APP_NAME,
APP_NAME_KEY, APP_NAME_KEY,
COOKIES_LOCALE_KEY: `${APP_NAME_KEY}-locale`, COOKIES_LOCALE_KEY: `${APP_NAME_KEY}-locale`,

View file

@ -24,10 +24,10 @@ export function useContactUs() {
}); });
if (result?.data?.contactUs.received) { if (result?.data?.contactUs.received) {
useNotification( useNotification({
t('popup.success.contactUs'), message: t('popup.success.contactUs'),
'success' type: 'success'
); });
} }
} }
@ -40,11 +40,11 @@ export function useContactUs() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return { return {

View file

@ -0,0 +1 @@
export * from './useDate';

View file

@ -0,0 +1,14 @@
export function useDate(
iso: string | undefined,
locale: string = 'en-gb'
): string {
if (!iso) return '';
const date = new Date(iso);
const parsedLocale = locale.replace('-', '-').toLocaleUpperCase()
return new Intl.DateTimeFormat(parsedLocale, {
year: 'numeric',
month: 'long',
day: '2-digit'
}).format(date);
}

View file

@ -1,26 +1,32 @@
export function useNotification( import type {VNodeChild} from "vue";
message: string,
type: string,
title?: string
) {
const duration = 5000;
const createProgressBar = (duration: number, message: string) => { interface INotificationArguments {
return h('div', [ message: string | VNodeChild,
h('p', message), type: string,
h('div', { title?: string
class: 'el-notification__progress', }
style: {
animationDuration: `${duration}ms` export function useNotification(
} args: INotificationArguments
}) ) {
]); const duration = 5000
};
const progressBar = h('div', {
class: 'el-notification__progress',
style: { animationDuration: `${duration}ms` }
})
const bodyContent: VNodeChild =
typeof args.message === 'string'
? h('p', args.message)
: args.message
const messageVNode = h('div', [bodyContent, progressBar])
ElNotification({ ElNotification({
title: title, title: args.title,
duration, duration,
message: createProgressBar(duration, message), message: messageVNode,
type: type type: args.type
} as import('element-plus').NotificationOptions); } as NotificationOptions)
} }

View file

@ -26,7 +26,9 @@ interface IOverwriteOrderArguments {
export function useOrderOverwrite () { export function useOrderOverwrite () {
const {t} = useI18n(); const {t} = useI18n();
const cartStore = useCartStore(); const cartStore = useCartStore();
const userStore = useUserStore();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const orderUuid = computed(() => cartStore.currentOrder?.uuid); const orderUuid = computed(() => cartStore.currentOrder?.uuid);
const { const {
@ -58,94 +60,101 @@ export function useOrderOverwrite () {
async function overwriteOrder ( async function overwriteOrder (
args: IOverwriteOrderArguments args: IOverwriteOrderArguments
) { ) {
switch (args.type) { if (isAuthenticated.value) {
case "add": switch (args.type) {
const addResult = await addMutate({ case "add":
orderUuid: orderUuid.value, const addResult = await addMutate({
productUuid: args.productUuid orderUuid: orderUuid.value,
}); productUuid: args.productUuid
});
if (addResult?.data?.addOrderProduct?.order) { if (addResult?.data?.addOrderProduct?.order) {
cartStore.setCurrentOrders(addResult.data.addOrderProduct.order); cartStore.setCurrentOrders(addResult.data.addOrderProduct.order);
useNotification( useNotification({
t('popup.success.addToCart', { product: args.productName }), message: t('popup.success.addToCart', { product: args.productName }),
'success' type: 'success'
); });
} }
break; break;
case "remove": case "remove":
const removeResult = await removeMutate({ const removeResult = await removeMutate({
orderUuid: orderUuid.value, orderUuid: orderUuid.value,
productUuid: args.productUuid productUuid: args.productUuid
}); });
if (removeResult?.data?.removeOrderProduct?.order) { if (removeResult?.data?.removeOrderProduct?.order) {
cartStore.setCurrentOrders(removeResult.data.removeOrderProduct.order); cartStore.setCurrentOrders(removeResult.data.removeOrderProduct.order);
useNotification( useNotification({
t('popup.success.removeFromCart', { product: args.productName }), message: t('popup.success.removeFromCart', { product: args.productName }),
'success' type: 'success'
); });
} }
break; break;
case "removeKind": case "removeKind":
const removeKindResult = await removeKindMutate({ const removeKindResult = await removeKindMutate({
orderUuid: orderUuid.value, orderUuid: orderUuid.value,
productUuid: args.productUuid productUuid: args.productUuid
}); });
if (removeKindResult?.data?.removeOrderProductsOfAKind?.order) { if (removeKindResult?.data?.removeOrderProductsOfAKind?.order) {
cartStore.setCurrentOrders(removeKindResult.data.removeOrderProductsOfAKind.order); cartStore.setCurrentOrders(removeKindResult.data.removeOrderProductsOfAKind.order);
useNotification( useNotification({
t('popup.success.removeFromCart', { product: args.productName }), message: t('popup.success.removeFromCart', { product: args.productName }),
'success' type: 'success'
); });
} }
break; break;
case "removeAll": case "removeAll":
const removeAllResult = await removeAllMutate({ const removeAllResult = await removeAllMutate({
orderUuid: orderUuid.value, orderUuid: orderUuid.value,
productUuid: args.productUuid productUuid: args.productUuid
}); });
if (removeAllResult?.data?.removeAllOrderProducts?.order) { if (removeAllResult?.data?.removeAllOrderProducts?.order) {
cartStore.setCurrentOrders(removeAllResult.data.removeAllOrderProducts.order); cartStore.setCurrentOrders(removeAllResult.data.removeAllOrderProducts.order);
useNotification( useNotification({
t('popup.success.removeAllFromCart', { product: args.productName }), message: t('popup.success.removeAllFromCart', { product: args.productName }),
'success' type: 'success'
); });
} }
break; break;
case "bulk": case "bulk":
const bulkResult = await bulkMutate({ const bulkResult = await bulkMutate({
orderUuid: orderUuid.value, orderUuid: orderUuid.value,
action: args.bulkAction, action: args.bulkAction,
products: args.products products: args.products
}); });
if (bulkResult?.data?.bulkOrderAction?.order) { if (bulkResult?.data?.bulkOrderAction?.order) {
cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order); cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order);
useNotification( useNotification({
t('popup.success.bulkRemoveWishlist'), message: t('popup.success.bulkRemoveWishlist'),
'success' type: 'success'
); });
} }
break; break;
default: default:
console.error('No type provided for overwriteOrder'); console.error('No type provided for overwriteOrder');
}
} else {
useNotification({
message: t('popup.errors.loginFirst'),
type: 'error'
});
} }
} }
@ -158,11 +167,11 @@ export function useOrderOverwrite () {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return{ return{

View file

@ -0,0 +1 @@
export * from './usePromocodes'

View file

@ -0,0 +1,24 @@
import type {IPromocodesResponse} from "~/types";
import {GET_PROMOCODES} from "~/graphql/queries/standalone/promocodes";
export async function usePromocodes () {
const promocodesStore = usePromocodeStore();
const { data, error } = await useAsyncQuery<IPromocodesResponse>(
GET_PROMOCODES
);
if (!error.value && data.value?.promocodes.edges) {
promocodesStore.setPromocodes(data.value.promocodes.edges);
}
watch(error, (err) => {
if (err) {
console.error('usePromocodes error:', err);
}
});
return {
};
}

View file

@ -38,11 +38,11 @@ export function useSearch() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return { return {

View file

@ -1 +1,4 @@
export * from './useUserActivation'; export * from './useUserActivation';
export * from './useAvatarUpload';
export * from './useUserUpdating';
export * from './useDeposit';

View file

@ -0,0 +1,52 @@
import type {IAvatarUploadResponse,} from "~/types";
import {UPLOAD_AVATAR} from "~/graphql/mutations/user";
import {isGraphQLError} from "~/utils/error";
import {useNotification} from "~/composables/notification";
export function useAvatarUpload() {
const { t } = useI18n();
const userStore = useUserStore();
const { mutate, onDone, loading, error } = useMutation<IAvatarUploadResponse>(UPLOAD_AVATAR, {
context: {
hasUpload: true
}}
);
async function uploadAvatar(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
await mutate({ avatar: file });
}
onDone(({ data }) => {
const user = data?.uploadAvatar.user;
if (user) {
userStore.setUser(user);
useNotification({
message: t('popup.success.avatarUpload'),
type: 'success'
});
}
});
watch(error, (err) => {
if (!err) return;
console.error('useAvatarUpload error:', err);
let message = t('popup.errors.defaultError');
if (isGraphQLError(err)) {
message = err.graphQLErrors?.[0]?.message || message;
} else {
message = err.message;
}
useNotification({
message,
type: 'error',
title: t('popup.errors.main')
});
});
return {
uploadAvatar
};
}

View file

@ -0,0 +1,42 @@
import {DEPOSIT} from "~/graphql/mutations/deposit";
import {isGraphQLError} from "~/utils/error";
import {useNotification} from "~/composables/notification";
export function useDeposit() {
const {t} = useI18n();
const { mutate, loading, error } = useMutation(DEPOSIT);
async function deposit(
amount: string
) {
const result = await mutate(
{ amount }
);
if (result?.data?.deposit) {
window.open(result?.data.deposit.transaction.process.url)
}
}
watch(error, (err) => {
if (!err) return;
console.error('useDeposit error:', err);
let message = t('popup.errors.defaultError');
if (isGraphQLError(err)) {
message = err.graphQLErrors?.[0]?.message || message;
} else {
message = err.message;
}
useNotification({
message,
type: 'error',
title: t('popup.errors.main')
});
});
return {
deposit,
loading
};
}

View file

@ -15,10 +15,10 @@ export function useUserActivation() {
const result = await mutate({ token, uid }); const result = await mutate({ token, uid });
if (result?.data?.activateUser) { if (result?.data?.activateUser) {
useNotification( useNotification({
t("popup.activationSuccess"), message: t("popup.activationSuccess"),
'success' type: 'success'
); });
} }
} }
@ -31,11 +31,11 @@ export function useUserActivation() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return { return {

View file

@ -0,0 +1,116 @@
import {useLogout} from "@/composables/auth";
import {UPDATE_USER} from "~/graphql/mutations/user";
import type {IUserUpdatingResponse} from "~/types";
import {useAppConfig} from "~/composables/config";
import {isGraphQLError} from "~/utils/error";
import {useNotification} from "~/composables/notification";
import {DEFAULT_LOCALE} from "~/config/constants";
import {useLocaleRedirect} from "~/composables/languages";
export function useUserUpdating() {
const userStore = useUserStore();
const {t} = useI18n();
const { mutate, loading, error } = useMutation<IUserUpdatingResponse>(UPDATE_USER);
const { COOKIES_LOCALE_KEY } = useAppConfig();
const { checkAndRedirect } = useLocaleRedirect();
const { logout } = useLogout();
const cookieLocale = useCookie(
COOKIES_LOCALE_KEY,
{
default: () => DEFAULT_LOCALE,
path: '/'
}
);
const userUuid = computed(() => userStore.user?.uuid);
const userEmail = computed(() => userStore.user?.email);
async function updateUser(
firstName: string,
lastName: string,
email: string,
phoneNumber: string,
password: string,
confirmPassword: string
) {
const fields = {
uuid: userUuid.value,
firstName,
lastName,
email,
phoneNumber,
password,
confirmPassword
};
const params = Object.fromEntries(
Object.entries(fields).filter(([_, value]) =>
value !== undefined && value !== null && value !== ''
)
);
// if (('password' in params && !('passwordConfirm' in params)) ||
// (!('password' in params) && 'passwordConfirm' in params)) {
// ElNotification({
// title: t('popup.errors.main'),
// message: t('popup.errors.noDataToUpdate'),
// type: 'error'
// });
// }
if (Object.keys(params).length === 0) {
useNotification({
message: t('popup.errors.noDataToUpdate'),
type: 'error'
});
}
const result = await mutate(params);
const data = result?.data?.updateUser;
if (data) {
if (userEmail.value !== email) {
await logout();
useNotification({
message: t('popup.success.confirmEmail'),
type: 'success'
});
} else {
userStore.setUser(data.user);
useNotification({
message: t('popup.success.userUpdate'),
type: 'success'
});
if (data.user.language !== cookieLocale.value) {
await checkAndRedirect(data.user.language);
}
}
}
}
watch(error, (err) => {
if (!err) return;
console.error('useUserUpdating error:', err);
let message = t('popup.errors.defaultError');
if (isGraphQLError(err)) {
message = err.graphQLErrors?.[0]?.message || message;
} else {
message = err.message;
}
useNotification({
message,
type: 'error',
title: t('popup.errors.main')
});
});
return {
updateUser,
loading
};
}

View file

@ -9,12 +9,12 @@ export async function useWishlist() {
); );
if (!error.value && data.value?.wishlists.edges[0]) { if (!error.value && data.value?.wishlists.edges[0]) {
wishlistStore.setWishlist(data.value.wishlists.edges[0].node) wishlistStore.setWishlist(data.value.wishlists.edges[0].node);
} }
watch(error, (err) => { watch(error, (err) => {
if (err) { if (err) {
console.error('useWishlist error:', err) console.error('useWishlist error:', err);
} }
}); });

View file

@ -25,7 +25,9 @@ interface IOverwriteWishlistArguments {
export function useWishlistOverwrite() { export function useWishlistOverwrite() {
const {t} = useI18n(); const {t} = useI18n();
const wishlistStore = useWishlistStore(); const wishlistStore = useWishlistStore();
const userStore = useUserStore();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const wishlistUuid = computed(() => wishlistStore.wishlist?.uuid); const wishlistUuid = computed(() => wishlistStore.wishlist?.uuid);
const { const {
@ -52,77 +54,84 @@ export function useWishlistOverwrite() {
async function overwriteWishlist ( async function overwriteWishlist (
args: IOverwriteWishlistArguments args: IOverwriteWishlistArguments
) { ) {
switch (args.type) { if (isAuthenticated.value) {
case "add": switch (args.type) {
const addResult = await addMutate({ case "add":
wishlistUuid: wishlistUuid.value, const addResult = await addMutate({
productUuid: args.productUuid wishlistUuid: wishlistUuid.value,
}); productUuid: args.productUuid
});
if (addResult?.data?.addWishlistProduct?.wishlist) { if (addResult?.data?.addWishlistProduct?.wishlist) {
wishlistStore.setWishlist(addResult.data.addWishlistProduct.wishlist); wishlistStore.setWishlist(addResult.data.addWishlistProduct.wishlist);
useNotification( useNotification({
t('popup.success.addToWishlist', { product: args.productName }), message: t('popup.success.addToWishlist', { product: args.productName }),
'success' type: 'success'
); });
} }
break; break;
case "remove": case "remove":
const removeResult = await removeMutate({ const removeResult = await removeMutate({
wishlistUuid: wishlistUuid.value, wishlistUuid: wishlistUuid.value,
productUuid: args.productUuid productUuid: args.productUuid
}); });
if (removeResult?.data?.removeWishlistProduct?.wishlist) { if (removeResult?.data?.removeWishlistProduct?.wishlist) {
wishlistStore.setWishlist(removeResult.data.removeWishlistProduct.wishlist); wishlistStore.setWishlist(removeResult.data.removeWishlistProduct.wishlist);
useNotification( useNotification({
t('popup.success.removeFromWishlist', { product: args.productName }), message: t('popup.success.removeFromWishlist', { product: args.productName }),
'success' type: 'success'
); });
} }
break; break;
case "removeAll": case "removeAll":
const removeAllResult = await removeAllMutate({ const removeAllResult = await removeAllMutate({
wishlistUuid: wishlistUuid.value, wishlistUuid: wishlistUuid.value,
productUuid: args.productUuid productUuid: args.productUuid
}); });
if (removeAllResult?.data?.removeAllWishlistProducts?.wishlist) { if (removeAllResult?.data?.removeAllWishlistProducts?.wishlist) {
wishlistStore.setWishlist(removeAllResult.data.removeAllWishlistProducts.wishlist); wishlistStore.setWishlist(removeAllResult.data.removeAllWishlistProducts.wishlist);
useNotification( useNotification({
t('popup.success.removeAllFromWishlist'), message: t('popup.success.removeAllFromWishlist'),
'success' type: 'success'
); });
} }
break; break;
case "bulk": case "bulk":
const bulkResult = await bulkMutate({ const bulkResult = await bulkMutate({
wishlistUuid: wishlistUuid.value, wishlistUuid: wishlistUuid.value,
action: args.bulkAction, action: args.bulkAction,
products: args.products products: args.products
}); });
if (bulkResult?.data?.bulkWishlistAction?.wishlist) { if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist); wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist);
useNotification( useNotification({
t('popup.success.bulkRemoveWishlist'), message: t('popup.success.bulkRemoveWishlist'),
'success' type: 'success'
); });
} }
break; break;
default: default:
console.error('No type provided for overwriteWishlist'); console.error('No type provided for overwriteWishlist');
}
} else {
useNotification({
message: t('popup.errors.loginFirst'),
type: 'error'
});
} }
} }
@ -135,11 +144,11 @@ export function useWishlistOverwrite() {
} else { } else {
message = err.message; message = err.message;
} }
useNotification( useNotification({
message, message,
'error', type: 'error',
t('popup.errors.main') title: t('popup.errors.main')
); });
}); });
return{ return{

View file

@ -85,4 +85,6 @@ export const SUPPORTED_LOCALES: LocaleDefinition[] = [
} }
]; ];
export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.code || 'en-gb'; export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.code || 'en-gb';
export const CURRENCY = '$'

View file

@ -0,0 +1,12 @@
export const PROMOCODE_FRAGMENT = gql`
fragment Promocode on PromoCodeType {
code
discount
discountType
endTime
id
startTime
usedOn
uuid
}
`

View file

@ -12,5 +12,10 @@ export const USER_FRAGMENT = gql`
balance { balance {
amount amount
} }
orders {
uuid
humanReadableId
status
}
} }
` `

View file

@ -7,7 +7,8 @@ export const REGISTER = gql`
$email: String!, $email: String!,
$phoneNumber: String!, $phoneNumber: String!,
$password: String!, $password: String!,
$confirmPassword: String! $confirmPassword: String!,
$referrer: String!,
) { ) {
createUser( createUser(
firstName: $firstName, firstName: $firstName,
@ -15,7 +16,8 @@ export const REGISTER = gql`
email: $email, email: $email,
phoneNumber: $phoneNumber, phoneNumber: $phoneNumber,
password: $password, password: $password,
confirmPassword: $confirmPassword confirmPassword: $confirmPassword,
referrer: $referrer
) { ) {
success success
} }

View file

@ -39,4 +39,19 @@ export const UPDATE_USER = gql`
} }
} }
${USER_FRAGMENT} ${USER_FRAGMENT}
`
export const UPLOAD_AVATAR = gql`
mutation uploadAvatar(
$avatar: Upload!
) {
uploadAvatar(
avatar: $avatar
) {
user {
...User
}
}
}
${USER_FRAGMENT}
` `

View file

@ -1,24 +0,0 @@
import combineQuery from 'graphql-combine-query'
import {GET_PRODUCTS} from "~/graphql/queries/standalone/products";
import {GET_CATEGORY_BY_SLUG} from "~/graphql/queries/standalone/categories";
export const getStore = (
productsVariables?: {
first?: number;
productAfter?: string;
categoriesSlug?: string;
orderby?: string;
minPrice?: number;
maxPrice?: number;
attributes?: string;
},
categoryVariables?: {
categorySlug?: string;
}
) => {
const { document, variables } = combineQuery('getStoreData')
.add(GET_PRODUCTS, productsVariables || {})
.add(GET_CATEGORY_BY_SLUG, categoryVariables || {})
return { document, variables };
};

View file

@ -0,0 +1,14 @@
import {PROMOCODE_FRAGMENT} from "~/graphql/fragments/promocodes.fragment";
export const GET_PROMOCODES = gql`
query getPromocodes {
promocodes {
edges {
node {
...Promocode
}
}
}
}
${PROMOCODE_FRAGMENT}
`

View file

@ -45,7 +45,8 @@
"errors": { "errors": {
"main": "Error!", "main": "Error!",
"defaultError": "Something went wrong..", "defaultError": "Something went wrong..",
"noDataToUpdate": "There is no data to update." "noDataToUpdate": "There is no data to update.",
"loginFirst": "You should be logged in to do this action!"
}, },
"success": { "success": {
"login": "Sign in successes", "login": "Sign in successes",
@ -60,7 +61,11 @@
"addToWishlist": "{product} has been added to the wishlist!", "addToWishlist": "{product} has been added to the wishlist!",
"removeFromWishlist": "{product} has been removed from the wishlist!", "removeFromWishlist": "{product} has been removed from the wishlist!",
"removeAllFromWishlist": "You have successfully emptied the wishlist!", "removeAllFromWishlist": "You have successfully emptied the wishlist!",
"bulkRemoveWishlist": "Selected items have been successfully removed from the wishlist!" "bulkRemoveWishlist": "Selected items have been successfully removed from the wishlist!",
"avatarUpload": "You have successfully uploaded an avatar!",
"userUpdate": "Profile successfully updated!",
"referralCopy": "You copied your referal link!",
"promocodeCopy": "You copied your promocode!"
}, },
"addToCartLimit": "Total quantity limit is {quantity}!", "addToCartLimit": "Total quantity limit is {quantity}!",
"failAdd": "Please log in to make a purchase", "failAdd": "Please log in to make a purchase",
@ -122,7 +127,10 @@
"catalog": "Catalog", "catalog": "Catalog",
"contact": "Contact", "contact": "Contact",
"wishlist": "Wishlist", "wishlist": "Wishlist",
"cart": "Cart" "cart": "Cart",
"settings": "Settings",
"balance": "Balance",
"promocodes": "Promocodes"
}, },
"contact": { "contact": {
"title": "Contact us" "title": "Contact us"
@ -148,19 +156,31 @@
}, },
"profile": { "profile": {
"settings": { "settings": {
"title": "Settings" "title": "Settings",
"joinData": "Date of registration",
"accountInfo": "Account info",
"copyReferral": "Copy my referral link",
"referralTooltip": "You will get a referral link after a successful purchase"
}, },
"orders": { "orders": {
"title": "Orders" "title": "Orders"
}, },
"wishlist": { "wishlist": {
"title": "Wishlist", "title": "Wishlist",
"total": "{quantity} items worth {amount}" "total": "{quantity} items worth {amount}",
"deleteTooltip": "Delete all from wishlist"
}, },
"cart": { "cart": {
"title": "Cart", "title": "Cart",
"quantity": "Quantity: ", "quantity": "Quantity: ",
"total": "Total: " "total": "Total: "
} },
"balance": {
"title": "Balance"
},
"promocodes": {
"title": "Promocodes"
},
"logout": "Logout"
} }
} }

View file

@ -27,9 +27,9 @@ export default defineNuxtConfig({
authType: 'Bearer', authType: 'Bearer',
authHeader: 'X-EVIBES-AUTH', authHeader: 'X-EVIBES-AUTH',
tokenStorage: 'cookie', tokenStorage: 'cookie',
tokenName: `${process.env.EVIBES_PROJECT_NAME?.toLowerCase()}-access` tokenName: `${process.env.EVIBES_PROJECT_NAME?.toLowerCase()}-access`,
} }
}, }
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {

View file

@ -15,6 +15,7 @@
"@vueuse/integrations": "^13.3.0", "@vueuse/integrations": "^13.3.0",
"@vueuse/nuxt": "^13.3.0", "@vueuse/nuxt": "^13.3.0",
"@vueuse/router": "^13.3.0", "@vueuse/router": "^13.3.0",
"apollo-upload-client": "17.0.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"graphql-combine-query": "^1.2.4", "graphql-combine-query": "^1.2.4",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
@ -75,7 +76,6 @@
"version": "3.13.8", "version": "3.13.8",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.8.tgz", "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.8.tgz",
"integrity": "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg==", "integrity": "sha512-YM9lQpm0VfVco4DSyKooHS/fDTiKQcCHfxr7i3iL6a0kP/jNO5+4NFK6vtRDxaYisd5BrwOZHLJpPBnvRVpKPg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-typed-document-node/core": "^3.1.1", "@graphql-typed-document-node/core": "^3.1.1",
@ -896,7 +896,6 @@
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
@ -3488,7 +3487,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
"integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -3501,7 +3499,6 @@
"version": "0.7.4", "version": "0.7.4",
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz",
"integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -3514,7 +3511,6 @@
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz",
"integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -3527,7 +3523,6 @@
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz",
"integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -3678,6 +3673,25 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/apollo-upload-client": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-17.0.0.tgz",
"integrity": "sha512-pue33bWVbdlXAGFPkgz53TTmxVMrKeQr0mdRcftNY+PoHIdbGZD0hoaXHvO6OePJAkFz7OiCFUf98p1G/9+Ykw==",
"license": "MIT",
"dependencies": {
"extract-files": "^11.0.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >= 16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/jaydenseric"
},
"peerDependencies": {
"@apollo/client": "^3.0.0",
"graphql": "14 - 16"
}
},
"node_modules/archiver": { "node_modules/archiver": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
@ -6208,6 +6222,18 @@
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/extract-files": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz",
"integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==",
"license": "MIT",
"engines": {
"node": "^12.20 || >= 14.13"
},
"funding": {
"url": "https://github.com/sponsors/jaydenseric"
}
},
"node_modules/extract-zip": { "node_modules/extract-zip": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@ -6888,7 +6914,7 @@
"version": "5.16.2", "version": "5.16.2",
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz",
"integrity": "sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==", "integrity": "sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -6998,7 +7024,6 @@
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"react-is": "^16.7.0" "react-is": "^16.7.0"
@ -8229,7 +8254,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@ -9110,7 +9134,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -9250,7 +9273,6 @@
"version": "0.18.1", "version": "0.18.1",
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz",
"integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@wry/caches": "^1.0.0", "@wry/caches": "^1.0.0",
@ -10290,7 +10312,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@ -10458,7 +10479,6 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/read-package-up": { "node_modules/read-package-up": {
@ -10572,7 +10592,6 @@
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz",
"integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -11606,7 +11625,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
"integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
@ -11870,7 +11888,6 @@
"version": "0.10.3", "version": "0.10.3",
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz",
"integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
@ -13305,14 +13322,12 @@
"version": "0.8.15", "version": "0.8.15",
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/zen-observable-ts": { "node_modules/zen-observable-ts": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz",
"integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"zen-observable": "0.8.15" "zen-observable": "0.8.15"

View file

@ -18,6 +18,7 @@
"@vueuse/integrations": "^13.3.0", "@vueuse/integrations": "^13.3.0",
"@vueuse/nuxt": "^13.3.0", "@vueuse/nuxt": "^13.3.0",
"@vueuse/router": "^13.3.0", "@vueuse/router": "^13.3.0",
"apollo-upload-client": "17.0.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"graphql-combine-query": "^1.2.4", "graphql-combine-query": "^1.2.4",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",

View file

@ -13,7 +13,7 @@
<script setup> <script setup>
import {useBrandByUuid} from "~/composables/brands"; import {useBrandByUuid} from "~/composables/brands";
import {usePageTitle} from "~/composables/utils/index.js"; import {usePageTitle} from "~/composables/utils";
const route = useRoute() const route = useRoute()

View file

@ -54,16 +54,42 @@
allow-half allow-half
disabled disabled
/> />
<div class="product__info-price">{{ product.price }}</div> <div class="product__info-price">{{ product.price }} {{ CURRENCY }}</div>
<div class="product__info-bottom"> <div class="product__info-bottom">
<ui-button <ui-button
class="product__info-button" class="product__info-button"
v-if="isProductInCart"
@click="overwriteOrder({
type: 'remove',
productUuid: product.uuid,
productName: product.name
})"
:isLoading="removeLoading"
>
{{ t('buttons.removeFromCart') }}
</ui-button>
<ui-button
class="product__info-button"
v-else
@click="overwriteOrder({
type: 'add',
productUuid: product.uuid,
productName: product.name
})"
:isLoading="addLoading"
> >
{{ t('buttons.addToCart') }} {{ t('buttons.addToCart') }}
</ui-button> </ui-button>
<div class="product__info-wishlist"> <div
<icon name="mdi:cards-heart-outline" size="28" /> class="product__info-wishlist"
<!-- <icon name="mdi:cards-heart" size="28" />--> @click="overwriteWishlist({
type: (isProductInWishlist ? 'remove' : 'add'),
productUuid: product.uuid,
productName: product.name
})"
>
<icon name="mdi:cards-heart" size="28" v-if="isProductInWishlist" />
<icon name="mdi:cards-heart-outline" size="28" v-else />
</div> </div>
</div> </div>
</div> </div>
@ -109,7 +135,7 @@
slidesPerView: 4 slidesPerView: 4
} }
}" }"
:navigation="{ prevEl: prevButton, nextEl: nextButton }" :navigation="{ prevEl: '.prev', nextEl: '.next' }"
> >
<swiper-slide <swiper-slide
v-for="prod in products" v-for="prod in products"
@ -138,20 +164,38 @@ import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css'; import 'swiper/css';
import 'swiper/css/navigation'; import 'swiper/css/navigation';
import {Navigation} from "swiper/modules"; import {Navigation} from "swiper/modules";
import {CURRENCY} from "~/config/constants";
import {useWishlistOverwrite} from "~/composables/wishlist";
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
const route = useRoute(); const route = useRoute();
const {t} = useI18n(); const {t} = useI18n();
const wishlistStore = useWishlistStore();
const cartStore = useCartStore();
const { setPageTitle } = usePageTitle(); const { setPageTitle } = usePageTitle();
const { scrollTo } = useScrollTo(); const { scrollTo } = useScrollTo();
const slug = useRouteParams<string>('slug'); const slug = useRouteParams<string>('slug');
const { overwriteWishlist } = useWishlistOverwrite();
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
const { product } = await useProductBySlug(slug.value); const { product } = await useProductBySlug(slug.value);
const { products, getProducts } = await useProducts(); const { products, getProducts } = await useProducts();
await getProducts({ await getProducts({
categoriesSlugs: product.value?.category.slug categoriesSlugs: product.value?.category.slug
}) });
const isProductInWishlist = computed(() => {
const el = wishlistStore.wishlist?.products?.edges.find(
(el) => el?.node?.uuid === product.value?.uuid
);
return !!el;
});
const isProductInCart = computed(() => {
return cartStore.currentOrder?.orderProducts?.edges.find((prod) => prod.node.product.uuid === product.value?.uuid);
});
const images = computed<string[]>(() => const images = computed<string[]>(() =>
product.value product.value

View file

@ -0,0 +1,33 @@
<template>
<div class="balance">
<h2>{{ t('profile.balance.title') }}: {{ user?.balance.amount }}</h2>
<forms-deposit />
</div>
</template>
<script setup lang="ts">
import {usePageTitle} from "~/composables/utils";
const {t} = useI18n();
const userStore = useUserStore();
const user = computed(() => userStore.user);
const { setPageTitle } = usePageTitle();
setPageTitle(t('breadcrumbs.balance'));
</script>
<style lang="scss" scoped>
.balance {
background-color: $white;
width: 100%;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: $default_border_radius;
height: fit-content;
display: flex;
flex-direction: column;
gap: 50px;
}
</style>

View file

@ -13,13 +13,14 @@
:key="product.node.uuid" :key="product.node.uuid"
:product="product.node.product" :product="product.node.product"
:isList="true" :isList="true"
:isToolsVisible="true"
/> />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import {usePageTitle} from "~/composables/utils/index.js"; import {usePageTitle} from "~/composables/utils";
const {t} = useI18n(); const {t} = useI18n();
const cartStore = useCartStore(); const cartStore = useCartStore();
@ -61,6 +62,7 @@ setPageTitle(t('breadcrumbs.cart'));
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
border-radius: $default_border_radius;
& p { & p {
font-weight: 600; font-weight: 600;
@ -80,6 +82,7 @@ setPageTitle(t('breadcrumbs.cart'));
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
border-radius: $default_border_radius;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }

View file

@ -0,0 +1,88 @@
<template>
<div class="promocodes">
<h2>{{ t('profile.promocodes.title') }}</h2>
<div class="promocodes__list">
<div
class="promocodes__item"
v-for="promocode in promocodes"
:key="promocode.node.uuid"
>
<icon
name="material-symbols:content-copy"
size="20"
class="promocodes__item-button"
@click="copyCode(promocode.node.code)"
/>
<p>{{ promocode.node.code }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {usePageTitle} from "~/composables/utils";
import {useNotification} from "~/composables/notification/index.js";
const {t} = useI18n();
const promocodesStore = usePromocodeStore();
const promocodes = computed(() => promocodesStore.promocodes);
const copyCode = (code: string) => {
navigator.clipboard.writeText(code)
.then(() => {
useNotification({
message: t('popup.success.promocodeCopy'),
type: 'success'
});
})
.catch(err => {
console.error(err);
});
};
// TODO: display more info about promo
const { setPageTitle } = usePageTitle();
setPageTitle(t('breadcrumbs.promocodes'));
</script>
<style lang="scss" scoped>
.promocodes {
background-color: $white;
width: 100%;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: $default_border_radius;
height: fit-content;
&__list {
margin-top: 50px;
display: flex;
flex-direction: column;
gap: 20px;
}
&__item {
border-radius: $default_border_radius;
border: 1px solid $accent;
padding: 7px 15px;
display: flex;
align-items: center;
gap: 20px;
&-button {
cursor: pointer;
transition: 0.2s;
color: $accentDark;
@include hover {
color: $accent;
}
}
& p {
font-weight: 600;
}
}
}
</style>

View file

@ -1,15 +1,239 @@
<template> <template>
<div class="settings"> <div class="settings">
<p>settings</p> <div class="settings__top">
<div class="settings__top-left">
<div class="settings__avatar">
<input type="file" id="avatar" @change="uploadAvatar" />
<label for="avatar">
<img class="settings__avatar-image" v-if="user?.avatar" :src="user?.avatar" alt="">
<icon name="material-symbols-light:person-outline-rounded" size="100" />
<span class="settings__avatar-inner">
<icon name="material-symbols:upload" size="40" />
</span>
</label>
</div>
<div class="settings__top-inner">
<h2>{{ user?.firstName }} {{ user?.lastName }}</h2>
<p>{{ t('profile.settings.joinData') }}: {{ joinData }}</p>
</div>
</div>
<el-tooltip
:content="t('profile.settings.referralTooltip')"
placement="top-end"
:disabled="finishedOrdersQuantity > 0"
>
<button
class="settings__button"
@click="copyReferral"
:disabled="finishedOrdersQuantity === 0"
>
<icon name="material-symbols:content-copy" size="20" />
{{ t('profile.settings.copyReferral') }}
</button>
</el-tooltip>
</div>
<div class="settings__main">
<p>{{ t('profile.settings.accountInfo') }}</p>
<forms-update />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {usePageTitle} from "~/composables/utils";
import { useDate } from '~/composables/date';
import {DEFAULT_LOCALE} from "~/config/constants";
import { useAppConfig } from "~/composables/config";
import {useAvatarUpload} from "~/composables/user";
import {useNotification} from "~/composables/notification";
const {t} = useI18n();
const userStore = useUserStore();
const { APP_DOMAIN, COOKIES_LOCALE_KEY } = useAppConfig();
const { uploadAvatar } = useAvatarUpload();
const cookieLocale = useCookie(
COOKIES_LOCALE_KEY,
{
default: () => DEFAULT_LOCALE,
path: '/'
}
);
const user = computed(() => userStore.user);
const finishedOrdersQuantity = computed(() => userStore.finishedOrdersQuantity);
const joinData = computed(() => {
return useDate(
user.value?.dateJoined, cookieLocale.value
);
});
const referralLink = computed(() => {
if (finishedOrdersQuantity.value > 0) {
return `https://${APP_DOMAIN}/${DEFAULT_LOCALE}/?referrer=` + user.value?.uuid;
} else {
return `https://${APP_DOMAIN}/${DEFAULT_LOCALE}/`;
}
});
const copyReferral = () => {
if (finishedOrdersQuantity.value > 0) {
navigator.clipboard.writeText(referralLink.value)
.then(() => {
useNotification({
message: t('popup.success.referralCopy'),
type: 'success'
});
})
.catch(err => {
console.error(err);
});
}
};
const { setPageTitle } = usePageTitle();
setPageTitle(t('breadcrumbs.settings'));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.settings { .settings {
background-color: $white;
width: 100%;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: $default_border_radius;
height: fit-content;
&__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 25px;
&-left {
display: flex;
align-items: center;
gap: 20px;
}
&-inner {
display: flex;
flex-direction: column;
gap: 5px;
& h2 {
font-size: 20px;
font-weight: 700;
}
& p {
font-size: 14px;
}
}
}
&__avatar {
width: 100px;
height: 100px;
border-radius: $default_border_radius;
border: 2px solid $accent;
background-color: rgba($accent, 0.2);
box-shadow: 0 0 9.1px 0 rgba(0, 0, 0, 0.20);
position: relative;
overflow: hidden;
@include hover {
.settings__avatar-inner {
opacity: 1;
}
}
& label {
cursor: pointer;
display: flex;
align-items: flex-end;
justify-content: center;
height: 100%;
& span {
color: $accent;
}
}
&-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
&-inner {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
inset: 0;
display: grid;
place-items: center;
opacity: 0;
transition: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
& span {
font-size: 60px !important;
color: $white !important;
}
}
& input {
display: none;
}
}
&__button {
cursor: pointer;
border-radius: $default_border_radius;
background-color: rgba($accent, 0.2);
border: 1px solid $accent;
padding: 5px 15px;
display: flex;
align-items: center;
gap: 10px;
transition: 0.2s;
color: $accent;
font-size: 16px;
font-weight: 600;
@include hover {
background-color: $accent;
color: $white;
}
&:disabled {
background-color: #c0c0c0;
cursor: not-allowed;
@include hover {
background-color: #c0c0c0;
color: $accent;
}
}
}
&__main {
margin-top: 50px;
display: flex;
flex-direction: column;
gap: 25px;
& p {
font-size: 24px;
font-weight: 600;
color: $accentDark;
}
}
} }
</style> </style>

View file

@ -11,12 +11,17 @@
</ui-checkbox> </ui-checkbox>
<p>{{ t('profile.wishlist.total', {quantity: productsInWishlist.length, amount: totalPrice}) }}</p> <p>{{ t('profile.wishlist.total', {quantity: productsInWishlist.length, amount: totalPrice}) }}</p>
</div> </div>
<div <el-tooltip
class="wishlist__top-button" :content="t('profile.wishlist.deleteTooltip')"
@click="onBulkRemove" placement="top-end"
> >
<icon name="material-symbols-light:delete-rounded" size="20" /> <div
</div> class="wishlist__top-button"
@click="onBulkRemove"
>
<icon name="material-symbols-light:delete-rounded" size="20" />
</div>
</el-tooltip>
</div> </div>
<div class="wishlist__list"> <div class="wishlist__list">
<div class="wishlist__list-inner"> <div class="wishlist__list-inner">
@ -118,6 +123,7 @@ setPageTitle(t('breadcrumbs.wishlist'));
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
border-radius: $default_border_radius;
&-left { &-left {
display: flex; display: flex;
@ -155,6 +161,7 @@ setPageTitle(t('breadcrumbs.wishlist'));
background-color: $white; background-color: $white;
padding: 20px; padding: 20px;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
border-radius: $default_border_radius;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;

View file

@ -1,9 +1,18 @@
import { provideApolloClient } from '@vue/apollo-composable' import { useAppConfig } from '~/composables/config';
import type { ApolloClient } from '@apollo/client/core'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const apollo = useApollo() const { COOKIES_LOCALE_KEY } = useAppConfig();
const defaultClient = apollo.clients!.default as ApolloClient<unknown> const localeCookie = useCookie(COOKIES_LOCALE_KEY);
const originalFetch = globalThis.fetch;
provideApolloClient(defaultClient) globalThis.fetch = (input, init = {}) => {
}) const lang = localeCookie.value || 'en-gb';
const headers = new Headers(init.headers as any);
headers.set('Accept-Language', lang);
return originalFetch(input, {
...init,
headers
});
};
});

View file

@ -0,0 +1,13 @@
import type {IPromocode} from "~/types";
export const usePromocodeStore = defineStore('promocode', () => {
const promocodes = ref<{ node: IPromocode }[] | null>(null);
const setPromocodes = (promo: { node: IPromocode }[]) => {
promocodes.value = promo
};
return {
promocodes,
setPromocodes
}
})

View file

@ -12,7 +12,11 @@ export const useUserStore = defineStore('user', () => {
); );
const user = ref<IUser | null>(null); const user = ref<IUser | null>(null);
const isAuthenticated = computed(() => Boolean(cookieAccess.value && user.value)); const isAuthenticated = computed(() => Boolean(cookieAccess.value && user.value));
const finishedOrdersQuantity = computed(() => {
return user.value?.orders.filter((order) => order.status === 'FINISHED').length || 0;
});
const setUser = (data: IUser | null) => { const setUser = (data: IUser | null) => {
user.value = data; user.value = data;
@ -21,6 +25,7 @@ export const useUserStore = defineStore('user', () => {
return { return {
user, user,
setUser, setUser,
isAuthenticated isAuthenticated,
finishedOrdersQuantity
}; };
}) });

View file

@ -0,0 +1,9 @@
import type {IPromocode} from "~/types";
export interface IPromocodesResponse {
promocodes: {
edges: {
node: IPromocode
}[]
}
}

View file

@ -10,4 +10,16 @@ export interface IUserActivationResponse {
activateUser: { activateUser: {
success: boolean success: boolean
} }
}
export interface IUserUpdatingResponse {
updateUser: {
user: IUser
}
}
export interface IAvatarUploadResponse {
uploadAvatar: {
user: IUser
}
} }

View file

@ -0,0 +1,10 @@
export interface IPromocode {
code: string,
discount: string,
discountType: string,
endTime: string,
id: string,
startTime: string,
usedOn: string,
uuid: string
}

View file

@ -1,7 +1,7 @@
export interface IUser { export interface IUser {
avatar: string, avatar: string,
uuid: string, uuid: string,
attributes: string | null attributes: string | null,
language: string, language: string,
email: string, email: string,
firstName: string, firstName: string,
@ -10,5 +10,10 @@ export interface IUser {
dateJoined: string, dateJoined: string,
balance: { balance: {
amount: number, amount: number,
} },
orders: {
uuid: string,
humanReadableId: string,
status: string
}[]
} }

View file

@ -10,6 +10,7 @@ export * from './app/wishlist'
export * from './app/orders' export * from './app/orders'
export * from './app/category' export * from './app/category'
export * from './app/store' export * from './app/store'
export * from './app/promocodes'
@ -26,4 +27,5 @@ export * from './api/user'
export * from './api/categories' export * from './api/categories'
export * from './api/brands' export * from './api/brands'
export * from './api/contact' export * from './api/contact'
export * from './api/store' export * from './api/store'
export * from './api/promocodes'