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(
() => appStore.activeState,
(state) => {
appStore.setOverflowHidden(state !== '')
appStore.setOverflowHidden(state !== '');
},
{ immediate: true }
)
);
let stopWatcher: VoidFunction = () => {}
watch(locale, () => {
useHead({
htmlAttrs: {
lang: locale.value
}
});
});
let stopWatcher: VoidFunction = () => {};
onMounted( async () => {
refreshInterval = setInterval(async () => {

View file

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

View file

@ -58,8 +58,6 @@
</template>
<script setup lang="ts">
import {useLogout} from "~/composables/auth";
const { t } = useI18n();
const appStore = useAppStore();
const userStore = useUserStore();
@ -79,8 +77,6 @@ const productsInCartQuantity = computed(() => {
const productsInWishlistQuantity = computed(() => {
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges.length : 0;
});
const { logout } = useLogout();
</script>
<style lang="scss" scoped>

View file

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

View file

@ -47,7 +47,7 @@
</div>
</nuxt-link-locale>
<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>
<el-rate
v-model="rating"
@ -58,6 +58,7 @@
</div>
</div>
<div class="card__bottom">
<div class="card__bottom-inner">
<ui-button
class="card__bottom-button"
v-if="isProductInCart"
@ -94,6 +95,30 @@
<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>
</template>
@ -106,10 +131,12 @@ import 'swiper/css/effect-fade';
import 'swiper/css/pagination'
import {useWishlistOverwrite} from "~/composables/wishlist";
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
import {CURRENCY} from "~/config/constants";
const props = defineProps<{
product: IProduct;
isList?: boolean;
isToolsVisible?: boolean;
}>();
const {t} = useI18n();
@ -129,6 +156,9 @@ const isProductInWishlist = computed(() => {
const isProductInCart = computed(() => {
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(() => {
return props.product.feedbacks.edges[0]?.node?.rating ?? 5;
@ -173,7 +203,7 @@ function goTo(index: number) {
&__list {
flex-direction: row;
align-items: flex-start;
align-items: stretch;
justify-content: space-between;
padding: 10px;
@ -191,10 +221,17 @@ function goTo(index: number) {
margin-top: 0;
width: fit-content;
flex-shrink: 0;
padding-inline: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
&-inner {
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
&-button {
width: fit-content;
@ -286,7 +323,7 @@ function goTo(index: number) {
&__quantity {
width: fit-content;
background-color: rgba($accent, 0.2);
background-color: rgba($contrast, 0.5);
border-radius: $default_border_radius;
padding: 5px 10px;
font-size: 14px;
@ -295,11 +332,14 @@ function goTo(index: number) {
&__bottom {
margin-top: auto;
padding: 0 20px 20px 20px;
max-width: 100%;
&-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
max-width: 100%;
}
&-button {
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) {
bottom: 0;
display: flex;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<template>
<!-- <form class="form" @submit.prevent="handleUpdate()">-->
<form class="form" @submit.prevent="">
<form class="form" @submit.prevent="handleUpdate">
<div class="form__box">
<ui-input
:type="'text'"
:placeholder="t('fields.firstName')"
@ -13,6 +13,8 @@
:rules="[required]"
v-model="lastName"
/>
</div>
<div class="form__box">
<ui-input
:type="'email'"
:placeholder="t('fields.email')"
@ -25,6 +27,8 @@
:rules="[required]"
v-model="phoneNumber"
/>
</div>
<div class="form__box">
<ui-input
:type="'password'"
:placeholder="t('fields.newPassword')"
@ -37,65 +41,58 @@
:rules="[compareStrings]"
v-model="confirmPassword"
/>
<!-- <ui-button-->
<!-- class="form__button"-->
<!-- :isLoading="loading"-->
<!-- >-->
<!-- {{ t('buttons.save') }}-->
<!-- </ui-button>-->
</div>
<ui-button
class="form__button"
:isLoading="loading"
>
{{ t('buttons.save') }}
</ui-button>
</form>
</template>
<script setup>
<script setup lang="ts">
import {useValidators} from "~/composables/rules";
// import {useUserUpdating} from "@/composables/user";
import {useUserUpdating} from "~/composables/user/index.js";
const { t } = useI18n()
const userStore = useUserStore()
const { t } = useI18n();
const userStore = useUserStore();
const { required, isEmail, isPasswordValid } = useValidators()
const { required, isEmail, isPasswordValid } = useValidators();
const userFirstName = computed(() => userStore.user?.firstName)
const userLastName = computed(() => userStore.user?.lastName)
const userEmail = computed(() => userStore.user?.email)
const userPhoneNumber = computed(() => userStore.user?.phoneNumber)
const user = computed(() => userStore.user);
const firstName = ref('')
const lastName = ref('')
const email = ref('')
const phoneNumber = ref('')
const password = ref('')
const confirmPassword = ref('')
const firstName = ref<string>('');
const lastName = ref<string>('');
const email = ref<string>('');
const phoneNumber = ref<string>('');
const password = ref<string>('');
const confirmPassword = ref<string>('');
const compareStrings = (v) => {
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 { 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>
<style lang="scss" scoped>
@ -103,5 +100,16 @@ const compareStrings = (v) => {
display: flex;
flex-direction: column;
gap: 20px;
&__box {
display: flex;
align-items: flex-start;
gap: 20px;
}
&__button {
width: fit-content;
padding-inline: 20px;
}
}
</style>

View file

@ -1,5 +1,6 @@
<template>
<nav class="nav">
<div class="nav__inner">
<nuxt-link-locale
class="nav__item"
:class="[{ active: route.path.includes('settings') }]"
@ -32,31 +33,59 @@
<icon name="ph:shopping-cart-light" size="20" />
{{ 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>
</template>
<script setup lang="ts">
import {useLogout} from "~/composables/auth";
const {t} = useI18n();
const route = useRoute();
const { logout } = useLogout();
</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;
&__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 {
cursor: pointer;
padding: 5px 30px 5px 20px;
padding: 7px 30px 7px 10px;
border-left: 2px solid $white;
display: flex;
align-items: center;
@ -64,6 +93,7 @@ const route = useRoute();
transition: 0.2s;
color: $accent;
font-size: 18px;
font-weight: 600;
@include hover {
@ -73,6 +103,29 @@ const route = useRoute();
&.active {
border-color: $accent;
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"
@keydown="numberOnly ? onlyNumbersKeydown($event) : null"
class="block__input"
:inputmode="inputMode || 'text'"
>
<button
@click.prevent="togglePasswordVisible"
@ -31,9 +32,10 @@ const emit = defineEmits<{
const props = defineProps<{
type: string,
placeholder: string,
modelValue?: [string, number],
modelValue?: string | number,
rules?: Rule[],
numberOnly?: boolean
numberOnly?: boolean,
inputMode?: "text" | "email" | "search" | "tel" | "url" | "none" | "numeric" | "decimal"
}>();
const isPasswordVisible = ref(props.type);
@ -108,7 +110,7 @@ function onInput(e: Event) {
line-height: 20px;
&::placeholder {
color: #2B2B2B;
color: #575757;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,10 +24,10 @@ export function useContactUs() {
});
if (result?.data?.contactUs.received) {
useNotification(
t('popup.success.contactUs'),
'success'
);
useNotification({
message: t('popup.success.contactUs'),
type: 'success'
});
}
}
@ -40,11 +40,11 @@ export function useContactUs() {
} else {
message = err.message;
}
useNotification(
useNotification({
message,
'error',
t('popup.errors.main')
);
type: 'error',
title: t('popup.errors.main')
});
});
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(
message: string,
import type {VNodeChild} from "vue";
interface INotificationArguments {
message: string | VNodeChild,
type: string,
title?: string
) {
const duration = 5000;
}
const createProgressBar = (duration: number, message: string) => {
return h('div', [
h('p', message),
h('div', {
export function useNotification(
args: INotificationArguments
) {
const duration = 5000
const progressBar = h('div', {
class: 'el-notification__progress',
style: {
animationDuration: `${duration}ms`
}
style: { animationDuration: `${duration}ms` }
})
]);
};
const bodyContent: VNodeChild =
typeof args.message === 'string'
? h('p', args.message)
: args.message
const messageVNode = h('div', [bodyContent, progressBar])
ElNotification({
title: title,
title: args.title,
duration,
message: createProgressBar(duration, message),
type: type
} as import('element-plus').NotificationOptions);
message: messageVNode,
type: args.type
} as NotificationOptions)
}

View file

@ -26,7 +26,9 @@ interface IOverwriteOrderArguments {
export function useOrderOverwrite () {
const {t} = useI18n();
const cartStore = useCartStore();
const userStore = useUserStore();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const orderUuid = computed(() => cartStore.currentOrder?.uuid);
const {
@ -58,6 +60,7 @@ export function useOrderOverwrite () {
async function overwriteOrder (
args: IOverwriteOrderArguments
) {
if (isAuthenticated.value) {
switch (args.type) {
case "add":
const addResult = await addMutate({
@ -68,10 +71,10 @@ export function useOrderOverwrite () {
if (addResult?.data?.addOrderProduct?.order) {
cartStore.setCurrentOrders(addResult.data.addOrderProduct.order);
useNotification(
t('popup.success.addToCart', { product: args.productName }),
'success'
);
useNotification({
message: t('popup.success.addToCart', { product: args.productName }),
type: 'success'
});
}
break;
@ -85,10 +88,10 @@ export function useOrderOverwrite () {
if (removeResult?.data?.removeOrderProduct?.order) {
cartStore.setCurrentOrders(removeResult.data.removeOrderProduct.order);
useNotification(
t('popup.success.removeFromCart', { product: args.productName }),
'success'
);
useNotification({
message: t('popup.success.removeFromCart', { product: args.productName }),
type: 'success'
});
}
break;
@ -102,10 +105,10 @@ export function useOrderOverwrite () {
if (removeKindResult?.data?.removeOrderProductsOfAKind?.order) {
cartStore.setCurrentOrders(removeKindResult.data.removeOrderProductsOfAKind.order);
useNotification(
t('popup.success.removeFromCart', { product: args.productName }),
'success'
);
useNotification({
message: t('popup.success.removeFromCart', { product: args.productName }),
type: 'success'
});
}
break;
@ -119,10 +122,10 @@ export function useOrderOverwrite () {
if (removeAllResult?.data?.removeAllOrderProducts?.order) {
cartStore.setCurrentOrders(removeAllResult.data.removeAllOrderProducts.order);
useNotification(
t('popup.success.removeAllFromCart', { product: args.productName }),
'success'
);
useNotification({
message: t('popup.success.removeAllFromCart', { product: args.productName }),
type: 'success'
});
}
break;
@ -136,10 +139,10 @@ export function useOrderOverwrite () {
if (bulkResult?.data?.bulkOrderAction?.order) {
cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order);
useNotification(
t('popup.success.bulkRemoveWishlist'),
'success'
);
useNotification({
message: t('popup.success.bulkRemoveWishlist'),
type: 'success'
});
}
break;
@ -147,6 +150,12 @@ export function useOrderOverwrite () {
default:
console.error('No type provided for overwriteOrder');
}
} else {
useNotification({
message: t('popup.errors.loginFirst'),
type: 'error'
});
}
}
watch(addError || removedError || removedKindError || removedAllError || bulkError, (err) => {
@ -158,11 +167,11 @@ export function useOrderOverwrite () {
} else {
message = err.message;
}
useNotification(
useNotification({
message,
'error',
t('popup.errors.main')
);
type: 'error',
title: t('popup.errors.main')
});
});
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 {
message = err.message;
}
useNotification(
useNotification({
message,
'error',
t('popup.errors.main')
);
type: 'error',
title: t('popup.errors.main')
});
});
return {

View file

@ -1 +1,4 @@
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 });
if (result?.data?.activateUser) {
useNotification(
t("popup.activationSuccess"),
'success'
);
useNotification({
message: t("popup.activationSuccess"),
type: 'success'
});
}
}
@ -31,11 +31,11 @@ export function useUserActivation() {
} else {
message = err.message;
}
useNotification(
useNotification({
message,
'error',
t('popup.errors.main')
);
type: 'error',
title: t('popup.errors.main')
});
});
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]) {
wishlistStore.setWishlist(data.value.wishlists.edges[0].node)
wishlistStore.setWishlist(data.value.wishlists.edges[0].node);
}
watch(error, (err) => {
if (err) {
console.error('useWishlist error:', err)
console.error('useWishlist error:', err);
}
});

View file

@ -25,7 +25,9 @@ interface IOverwriteWishlistArguments {
export function useWishlistOverwrite() {
const {t} = useI18n();
const wishlistStore = useWishlistStore();
const userStore = useUserStore();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const wishlistUuid = computed(() => wishlistStore.wishlist?.uuid);
const {
@ -52,6 +54,7 @@ export function useWishlistOverwrite() {
async function overwriteWishlist (
args: IOverwriteWishlistArguments
) {
if (isAuthenticated.value) {
switch (args.type) {
case "add":
const addResult = await addMutate({
@ -62,10 +65,10 @@ export function useWishlistOverwrite() {
if (addResult?.data?.addWishlistProduct?.wishlist) {
wishlistStore.setWishlist(addResult.data.addWishlistProduct.wishlist);
useNotification(
t('popup.success.addToWishlist', { product: args.productName }),
'success'
);
useNotification({
message: t('popup.success.addToWishlist', { product: args.productName }),
type: 'success'
});
}
break;
@ -79,10 +82,10 @@ export function useWishlistOverwrite() {
if (removeResult?.data?.removeWishlistProduct?.wishlist) {
wishlistStore.setWishlist(removeResult.data.removeWishlistProduct.wishlist);
useNotification(
t('popup.success.removeFromWishlist', { product: args.productName }),
'success'
);
useNotification({
message: t('popup.success.removeFromWishlist', { product: args.productName }),
type: 'success'
});
}
break;
@ -96,10 +99,10 @@ export function useWishlistOverwrite() {
if (removeAllResult?.data?.removeAllWishlistProducts?.wishlist) {
wishlistStore.setWishlist(removeAllResult.data.removeAllWishlistProducts.wishlist);
useNotification(
t('popup.success.removeAllFromWishlist'),
'success'
);
useNotification({
message: t('popup.success.removeAllFromWishlist'),
type: 'success'
});
}
break;
@ -113,10 +116,10 @@ export function useWishlistOverwrite() {
if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
wishlistStore.setWishlist(bulkResult.data.bulkWishlistAction.wishlist);
useNotification(
t('popup.success.bulkRemoveWishlist'),
'success'
);
useNotification({
message: t('popup.success.bulkRemoveWishlist'),
type: 'success'
});
}
break;
@ -124,6 +127,12 @@ export function useWishlistOverwrite() {
default:
console.error('No type provided for overwriteWishlist');
}
} else {
useNotification({
message: t('popup.errors.loginFirst'),
type: 'error'
});
}
}
watch(addError || removedError || removeAllError || bulkError, (err) => {
@ -135,11 +144,11 @@ export function useWishlistOverwrite() {
} else {
message = err.message;
}
useNotification(
useNotification({
message,
'error',
t('popup.errors.main')
);
type: 'error',
title: t('popup.errors.main')
});
});
return{

View file

@ -86,3 +86,5 @@ export const SUPPORTED_LOCALES: LocaleDefinition[] = [
];
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 {
amount
}
orders {
uuid
humanReadableId
status
}
}
`

View file

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

View file

@ -40,3 +40,18 @@ export const UPDATE_USER = gql`
}
${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": {
"main": "Error!",
"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": {
"login": "Sign in successes",
@ -60,7 +61,11 @@
"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!"
"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}!",
"failAdd": "Please log in to make a purchase",
@ -122,7 +127,10 @@
"catalog": "Catalog",
"contact": "Contact",
"wishlist": "Wishlist",
"cart": "Cart"
"cart": "Cart",
"settings": "Settings",
"balance": "Balance",
"promocodes": "Promocodes"
},
"contact": {
"title": "Contact us"
@ -148,19 +156,31 @@
},
"profile": {
"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": {
"title": "Orders"
},
"wishlist": {
"title": "Wishlist",
"total": "{quantity} items worth {amount}"
"total": "{quantity} items worth {amount}",
"deleteTooltip": "Delete all from wishlist"
},
"cart": {
"title": "Cart",
"quantity": "Quantity: ",
"total": "Total: "
}
},
"balance": {
"title": "Balance"
},
"promocodes": {
"title": "Promocodes"
},
"logout": "Logout"
}
}

View file

@ -27,9 +27,9 @@ 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`,
}
}
},
},
runtimeConfig: {
public: {

View file

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

View file

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

View file

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

View file

@ -54,16 +54,42 @@
allow-half
disabled
/>
<div class="product__info-price">{{ product.price }}</div>
<div class="product__info-price">{{ product.price }} {{ CURRENCY }}</div>
<div class="product__info-bottom">
<ui-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') }}
</ui-button>
<div class="product__info-wishlist">
<icon name="mdi:cards-heart-outline" size="28" />
<!-- <icon name="mdi:cards-heart" size="28" />-->
<div
class="product__info-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>
@ -109,7 +135,7 @@
slidesPerView: 4
}
}"
:navigation="{ prevEl: prevButton, nextEl: nextButton }"
:navigation="{ prevEl: '.prev', nextEl: '.next' }"
>
<swiper-slide
v-for="prod in products"
@ -138,20 +164,38 @@ import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
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 {t} = useI18n();
const wishlistStore = useWishlistStore();
const cartStore = useCartStore();
const { setPageTitle } = usePageTitle();
const { scrollTo } = useScrollTo();
const slug = useRouteParams<string>('slug');
const { overwriteWishlist } = useWishlistOverwrite();
const { addLoading, removeLoading, overwriteOrder } = useOrderOverwrite();
const { product } = await useProductBySlug(slug.value);
const { products, getProducts } = await useProducts();
await getProducts({
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[]>(() =>
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"
:product="product.node.product"
:isList="true"
:isToolsVisible="true"
/>
</div>
</div>
</template>
<script setup>
import {usePageTitle} from "~/composables/utils/index.js";
<script setup lang="ts">
import {usePageTitle} from "~/composables/utils";
const {t} = useI18n();
const cartStore = useCartStore();
@ -61,6 +62,7 @@ setPageTitle(t('breadcrumbs.cart'));
align-items: center;
justify-content: space-between;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
border-radius: $default_border_radius;
& p {
font-weight: 600;
@ -80,6 +82,7 @@ setPageTitle(t('breadcrumbs.cart'));
flex-direction: column;
gap: 20px;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
border-radius: $default_border_radius;
flex: 1;
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>
<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>
</template>
<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>
<style lang="scss" scoped>
.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>

View file

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

View file

@ -1,9 +1,18 @@
import { provideApolloClient } from '@vue/apollo-composable'
import type { ApolloClient } from '@apollo/client/core'
import { useAppConfig } from '~/composables/config';
export default defineNuxtPlugin(() => {
const apollo = useApollo()
const defaultClient = apollo.clients!.default as ApolloClient<unknown>
const { COOKIES_LOCALE_KEY } = useAppConfig();
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 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) => {
user.value = data;
@ -21,6 +25,7 @@ export const useUserStore = defineStore('user', () => {
return {
user,
setUser,
isAuthenticated
isAuthenticated,
finishedOrdersQuantity
};
})
});

View file

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

View file

@ -11,3 +11,15 @@ export interface IUserActivationResponse {
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 {
avatar: string,
uuid: string,
attributes: string | null
attributes: string | null,
language: string,
email: string,
firstName: string,
@ -10,5 +10,10 @@ export interface IUser {
dateJoined: string,
balance: {
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/category'
export * from './app/store'
export * from './app/promocodes'
@ -27,3 +28,4 @@ export * from './api/categories'
export * from './api/brands'
export * from './api/contact'
export * from './api/store'
export * from './api/promocodes'