Features: 1) Introduce useUserBaseData composable to fetch and manage user's wishlist, orders, and promocodes; 2) Add reusable useOrders and useOrderOverwrite composables with advanced filtering and pagination; 3) Implement order.vue component for detailed order displays with UI enhancements;
Fixes: 1) Replace deprecated context usage in `useAvatarUpload` mutation; 2) Resolve incorrect locale parsing in `useDate` utility and fix non-reactive cart state in `profile/cart.vue`; 3) Update stale imports and standardize type naming across composables; Extra: 1) Refactor i18n strings including order status and search-related texts; 2) Replace temporary workarounds with `apollo-upload-client` configuration and add `apollo-upload-link.ts` plugin; 3) Cleanup redundant files, comments, and improve SCSS structure with new variables and placeholders.
This commit is contained in:
parent
c60ac13e88
commit
52b32bd608
40 changed files with 984 additions and 148 deletions
11
storefront/assets/styles/modules/normalize.scss
vendored
11
storefront/assets/styles/modules/normalize.scss
vendored
|
|
@ -37,6 +37,17 @@ button:focus-visible {
|
|||
margin-inline: auto;
|
||||
}
|
||||
|
||||
//::-webkit-scrollbar {
|
||||
// width: 12px;
|
||||
//}
|
||||
//
|
||||
//::-webkit-scrollbar-thumb {
|
||||
// background-color: rgba($accent, 0.5);
|
||||
// border-radius: $default_border_radius;
|
||||
// -webkit-transition: all .2s ease;
|
||||
// transition: all .2s ease;
|
||||
//}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: $accent;
|
||||
}
|
||||
|
|
|
|||
246
storefront/components/cards/order.vue
Normal file
246
storefront/components/cards/order.vue
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
<template>
|
||||
<el-collapse-item
|
||||
class="order"
|
||||
>
|
||||
<template #title="{ isActive }">
|
||||
<div :class="['order__top', { 'is-active': isActive }]">
|
||||
<div>
|
||||
<p>{{ t('profile.orders.id') }}: {{ order.humanReadableId }}</p>
|
||||
<p v-if="order.buyTime">{{ useDate(order.buyTime, locale) }}</p>
|
||||
<el-tooltip
|
||||
:content="order.status"
|
||||
placement="top"
|
||||
>
|
||||
<p class="status" :style="[{ backgroundColor: statusColor(order.status) }]">
|
||||
{{ order.status }}
|
||||
<icon name="material-symbols:info-outline-rounded" size="14" />
|
||||
</p>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<icon
|
||||
name="material-symbols:keyboard-arrow-down"
|
||||
size="22"
|
||||
class="order__top-icon"
|
||||
:class="[{ active: isActive }]"
|
||||
/>
|
||||
</div>
|
||||
<div class="order__top-bottom" :class="{ active: !isActive }">
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<nuxt-img
|
||||
v-for="product in order.orderProducts.edges"
|
||||
:key="product.node.uuid"
|
||||
:src="product.node.product.images.edges[0].node.image"
|
||||
:alt="product.node.product.name"
|
||||
format="webp"
|
||||
densities="x1"
|
||||
/>
|
||||
</div>
|
||||
<p>{{ order.totalPrice }}{{ CURRENCY }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="order__main">
|
||||
<div
|
||||
class="order__product"
|
||||
v-for="product in order.orderProducts.edges"
|
||||
:key="product.node.uuid"
|
||||
>
|
||||
<div class="order__product-left">
|
||||
<nuxt-img
|
||||
:src="product.node.product.images.edges[0].node.image"
|
||||
:alt="product.node.product.name"
|
||||
/>
|
||||
<p>{{ product.node.product.name }}</p>
|
||||
</div>
|
||||
<div class="order__product-right">
|
||||
<h6>{{ t('profile.orders.price') }}: {{ product.node.product.price * product.node.quantity }}{{ CURRENCY }}</h6>
|
||||
<p>{{ product.node.quantity }} X {{ product.node.product.price }}{{ CURRENCY }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order__total">
|
||||
<p>{{ t('profile.orders.total') }}: {{ order.totalPrice }}{{ CURRENCY }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useDate} from "~/composables/date";
|
||||
import {CURRENCY, orderStatuses} from "~/config/constants";
|
||||
import type {IOrder} from "~/types";
|
||||
|
||||
const props = defineProps<{
|
||||
order: IOrder;
|
||||
}>();
|
||||
|
||||
const {t, locale} = useI18n();
|
||||
|
||||
const statusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case orderStatuses.FAILED:
|
||||
return '#FF0000';
|
||||
|
||||
case orderStatuses.PAYMENT:
|
||||
return '#FFC107';
|
||||
|
||||
case orderStatuses.CREATED:
|
||||
return '#007BFF';
|
||||
|
||||
case orderStatuses.DELIVERING:
|
||||
return '#00C853';
|
||||
|
||||
case orderStatuses.FINISHED:
|
||||
return '#00C853';
|
||||
|
||||
case orderStatuses.MOMENTAL:
|
||||
return '#00C853';
|
||||
|
||||
default:
|
||||
return '#000';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order {
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 0 10px 5px 10px;
|
||||
|
||||
& div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
|
||||
& p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
&.status {
|
||||
border-radius: $default_border_radius;
|
||||
padding: 3px 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
transition: 0.2s;
|
||||
|
||||
&.active {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.2s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&.active {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
& > * {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
& div {
|
||||
& div {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid $accent;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
|
||||
& div {
|
||||
padding-top: 0;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
& img {
|
||||
height: 65px;
|
||||
border-radius: $default_border_radius;
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&__product {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid $accent;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
& img {
|
||||
height: 150px;
|
||||
border-radius: $default_border_radius;
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
& h6 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__total {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
|
||||
& p {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
height: fit-content;
|
||||
padding-block: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -130,7 +130,7 @@ import 'swiper/css';
|
|||
import 'swiper/css/effect-fade';
|
||||
import 'swiper/css/pagination'
|
||||
import {useWishlistOverwrite} from "~/composables/wishlist";
|
||||
import {useOrderOverwrite} from "~/composables/orders/useOrderOverwrite";
|
||||
import {useOrderOverwrite} from "~/composables/orders";
|
||||
import {CURRENCY} from "~/config/constants";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
88
storefront/components/skeletons/cards/order.vue
Normal file
88
storefront/components/skeletons/cards/order.vue
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<el-skeleton
|
||||
class="sk"
|
||||
animated
|
||||
>
|
||||
<template #template>
|
||||
<div class="sk__main">
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
class="sk__text"
|
||||
/>
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
class="sk__text"
|
||||
/>
|
||||
</div>
|
||||
<div class="sk__bottom">
|
||||
<div class="sk__bottom-images">
|
||||
<el-skeleton-item
|
||||
variant="image"
|
||||
class="sk__image"
|
||||
v-for="idx in 3"
|
||||
:key="idx"
|
||||
/>
|
||||
</div>
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
class="sk__price"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sk {
|
||||
width: 100%;
|
||||
border-radius: $default_border_radius;
|
||||
background-color: rgba(255, 255, 255, 0.61);
|
||||
border: 1px solid $accent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 10px 8px;
|
||||
|
||||
&__main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
border-radius: $default_border_radius;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
border-top: 1px solid $accent;
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&-images {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 52px;
|
||||
height: 65px;
|
||||
border-radius: $default_border_radius;
|
||||
}
|
||||
|
||||
&__price {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -83,7 +83,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const attributesQuery = useRouteQuery<string>('attributes', '');
|
||||
|
||||
//TODO: price in filters
|
||||
const {
|
||||
selectedMap,
|
||||
selectedAllMap,
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ import type { ILoginResponse } from '~/types/api/auth';
|
|||
import { isGraphQLError } from '~/utils/error';
|
||||
import { useAppConfig } from '~/composables/config';
|
||||
import { useLocaleRedirect } from '~/composables/languages';
|
||||
import { useWishlist } from '~/composables/wishlist';
|
||||
import { usePendingOrder } from '~/composables/orders';
|
||||
import { useUserStore } from '~/stores/user';
|
||||
import { useAppStore } from '~/stores/app';
|
||||
import {DEFAULT_LOCALE} from "~/config/constants";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
import {usePromocodes} from "~/composables/promocodes";
|
||||
import {useUserBaseData} from "~/composables/user";
|
||||
|
||||
export function useLogin() {
|
||||
const { t } = useI18n();
|
||||
|
|
@ -70,10 +68,14 @@ export function useLogin() {
|
|||
await checkAndRedirect(authData.user.language);
|
||||
}
|
||||
|
||||
await useWishlist();
|
||||
await usePendingOrder(authData.user.email);
|
||||
await usePromocodes();
|
||||
//TODO: combine three requests
|
||||
// await useWishlist();
|
||||
// await useOrders({
|
||||
// userEmail: authData.user.email,
|
||||
// status: "PENDING"
|
||||
// });
|
||||
// await usePromocodes();
|
||||
|
||||
await useUserBaseData(authData.user.email);
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { REFRESH } from '@/graphql/mutations/auth';
|
||||
import { useAppConfig } from '~/composables/config';
|
||||
import { useLocaleRedirect } from '~/composables/languages';
|
||||
import { useWishlist } from '~/composables/wishlist';
|
||||
import { usePendingOrder } from '~/composables/orders';
|
||||
import { useUserStore } from '~/stores/user';
|
||||
import { isGraphQLError } from '~/utils/error';
|
||||
import {DEFAULT_LOCALE} from "~/config/constants";
|
||||
import {useNotification} from "~/composables/notification";
|
||||
import {usePromocodes} from "~/composables/promocodes";
|
||||
import {useUserBaseData} from "~/composables/user";
|
||||
|
||||
export function useRefresh() {
|
||||
const { t } = useI18n();
|
||||
|
|
@ -51,10 +49,13 @@ export function useRefresh() {
|
|||
|
||||
cookieRefresh.value = data.refreshToken
|
||||
|
||||
await useWishlist();
|
||||
await usePendingOrder(data.user.email);
|
||||
await usePromocodes();
|
||||
//TODO: combine three requests
|
||||
// await useWishlist();
|
||||
// await useOrders({
|
||||
// userEmail: data.user.email,
|
||||
// status: "PENDING"
|
||||
// });
|
||||
// await usePromocodes();
|
||||
await useUserBaseData(data.user.email);
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
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, {
|
||||
locale: string = 'en-gb',
|
||||
options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
): string {
|
||||
if (!iso) return ''
|
||||
const date = new Date(iso)
|
||||
const parsedLocale = locale.replace('_', '-').toLowerCase()
|
||||
|
||||
return new Intl.DateTimeFormat(parsedLocale, options).format(date)
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from './usePendingOrder';
|
||||
export * from './useOrderOverwrite';
|
||||
export * from './useOrders';
|
||||
79
storefront/composables/orders/useOrders.ts
Normal file
79
storefront/composables/orders/useOrders.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import {GET_ORDERS} from "~/graphql/queries/standalone/orders";
|
||||
import type {IOrdersResponse} from "~/types";
|
||||
import {orderStatuses} from "~/config/constants";
|
||||
|
||||
interface IOrdersArguments {
|
||||
userEmail: string,
|
||||
status?: string,
|
||||
after?: string,
|
||||
search: string
|
||||
}
|
||||
|
||||
interface IOrderVars {
|
||||
status: string,
|
||||
userEmail: string,
|
||||
first: number,
|
||||
after?: string,
|
||||
search: string
|
||||
}
|
||||
|
||||
export async function useOrders(args: IOrdersArguments) {
|
||||
const cartStore = useCartStore();
|
||||
|
||||
const variables = reactive<IOrderVars>({
|
||||
status: args.status || '',
|
||||
userEmail: args.userEmail,
|
||||
first: 10,
|
||||
after: args.after,
|
||||
search: args.search
|
||||
});
|
||||
|
||||
const { pending, data, error, refresh } = await useAsyncQuery<IOrdersResponse>(
|
||||
GET_ORDERS,
|
||||
variables
|
||||
);
|
||||
|
||||
const orders = ref(data.value?.orders.edges.filter((order) => order.node.status !== orderStatuses.PENDING) ?? []);
|
||||
const pageInfo = computed(() => data.value?.orders.pageInfo ?? null);
|
||||
|
||||
if (!error.value && data.value?.orders.edges) {
|
||||
if (args.status === orderStatuses.PENDING) {
|
||||
cartStore.setCurrentOrders(data.value?.orders.edges[0].node);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => variables.after,
|
||||
async (newCursor, oldCursor) => {
|
||||
if (!newCursor || newCursor === oldCursor) return;
|
||||
await refresh();
|
||||
const newEdges = data.value?.orders.edges ?? [];
|
||||
orders.value.push(...newEdges);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
[
|
||||
() => variables.status,
|
||||
() => variables.search
|
||||
],
|
||||
async () => {
|
||||
variables.after = '';
|
||||
await refresh();
|
||||
orders.value = data.value?.orders.edges ?? [];
|
||||
}
|
||||
);
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useOrders error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
pending,
|
||||
orders,
|
||||
pageInfo,
|
||||
variables
|
||||
};
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import {GET_ORDERS} from "~/graphql/queries/standalone/orders";
|
||||
import type {IOrderResponse} from "~/types";
|
||||
|
||||
export async function usePendingOrder(userEmail: string) {
|
||||
const cartStore = useCartStore();
|
||||
|
||||
const { data, error } = await useAsyncQuery<IOrderResponse>(
|
||||
GET_ORDERS,
|
||||
{
|
||||
status: "PENDING",
|
||||
userEmail
|
||||
}
|
||||
);
|
||||
|
||||
if (!error.value && data.value?.orders.edges[0].node) {
|
||||
cartStore.setCurrentOrders(data.value?.orders.edges[0].node);
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('usePendingOrder error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
};
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export async function usePromocodes () {
|
|||
const { data, error } = await useAsyncQuery<IPromocodesResponse>(
|
||||
GET_PROMOCODES
|
||||
);
|
||||
console.log(data.value)
|
||||
|
||||
if (!error.value && data.value?.promocodes.edges) {
|
||||
promocodesStore.setPromocodes(data.value.promocodes.edges);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { GET_PRODUCTS } from '~/graphql/queries/standalone/products';
|
||||
import type {IProductResponse} from '~/types';
|
||||
|
||||
interface ProdVars {
|
||||
interface IProdVars {
|
||||
first: number,
|
||||
categoriesSlugs: string,
|
||||
attributes?: string,
|
||||
|
|
@ -19,7 +19,7 @@ export async function useStore(
|
|||
maxPrice?: number,
|
||||
productAfter?: string
|
||||
) {
|
||||
const variables = reactive<ProdVars>({
|
||||
const variables = reactive<IProdVars>({
|
||||
first: 15,
|
||||
categoriesSlugs: slug,
|
||||
attributes,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './useUserActivation';
|
||||
export * from './useAvatarUpload';
|
||||
export * from './useUserUpdating';
|
||||
export * from './useDeposit';
|
||||
export * from './useDeposit';
|
||||
export * from './useUserBaseData';
|
||||
|
|
@ -6,17 +6,19 @@ 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
|
||||
}}
|
||||
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;
|
||||
console.log(file)
|
||||
|
||||
await mutate({ avatar: file });
|
||||
await mutate({ file });
|
||||
}
|
||||
|
||||
onDone(({ data }) => {
|
||||
|
|
|
|||
41
storefront/composables/user/useUserBaseData.ts
Normal file
41
storefront/composables/user/useUserBaseData.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type {IUserBaseDataResponse} from "~/types";
|
||||
import {getUserBaseData} from "~/graphql/queries/combined/userBaseData";
|
||||
import {orderStatuses} from "~/config/constants";
|
||||
|
||||
export async function useUserBaseData(userEmail: string) {
|
||||
const wishlistStore = useWishlistStore();
|
||||
const cartStore = useCartStore();
|
||||
const promocodesStore = usePromocodeStore();
|
||||
|
||||
const { document, variables } = getUserBaseData(
|
||||
{
|
||||
userEmail,
|
||||
status: orderStatuses.PENDING
|
||||
},
|
||||
);
|
||||
|
||||
const { data, error } = await useAsyncQuery<IUserBaseDataResponse>(
|
||||
document,
|
||||
variables
|
||||
);
|
||||
|
||||
if (data.value?.wishlists.edges) {
|
||||
wishlistStore.setWishlist(data.value.wishlists.edges[0].node);
|
||||
}
|
||||
if (data.value?.orders.edges) {
|
||||
cartStore.setCurrentOrders(data.value?.orders.edges[0].node);
|
||||
}
|
||||
if (data.value?.promocodes.edges) {
|
||||
promocodesStore.setPromocodes(data.value.promocodes.edges);
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useUserBaseData error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
};
|
||||
}
|
||||
|
|
@ -87,4 +87,14 @@ export const SUPPORTED_LOCALES: LocaleDefinition[] = [
|
|||
|
||||
export const DEFAULT_LOCALE = SUPPORTED_LOCALES.find(locale => locale.default)?.code || 'en-gb';
|
||||
|
||||
export const CURRENCY = '$'
|
||||
export const CURRENCY = '$';
|
||||
|
||||
export enum orderStatuses {
|
||||
PENDING = 'PENDING',
|
||||
FAILED = 'FAILED',
|
||||
PAYMENT = 'PAYMENT',
|
||||
CREATED = 'CREATED',
|
||||
DELIVERING = 'DELIVERING',
|
||||
FINISHED = 'FINISHED',
|
||||
MOMENTAL = 'MOMENTAL'
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export const ORDER_FRAGMENT = gql`
|
|||
status
|
||||
buyTime
|
||||
humanReadableId
|
||||
notifications
|
||||
orderProducts {
|
||||
edges {
|
||||
node {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ export const PROMOCODE_FRAGMENT = gql`
|
|||
discount
|
||||
discountType
|
||||
endTime
|
||||
id
|
||||
startTime
|
||||
usedOn
|
||||
uuid
|
||||
}
|
||||
`
|
||||
|
|
@ -43,10 +43,10 @@ export const UPDATE_USER = gql`
|
|||
|
||||
export const UPLOAD_AVATAR = gql`
|
||||
mutation uploadAvatar(
|
||||
$avatar: Upload!
|
||||
$file: Upload!
|
||||
) {
|
||||
uploadAvatar(
|
||||
avatar: $avatar
|
||||
file: $file
|
||||
) {
|
||||
user {
|
||||
...User
|
||||
|
|
|
|||
18
storefront/graphql/queries/combined/userBaseData.ts
Normal file
18
storefront/graphql/queries/combined/userBaseData.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import combineQuery from 'graphql-combine-query'
|
||||
import {GET_WISHLIST} from "~/graphql/queries/standalone/wishlist";
|
||||
import {GET_PROMOCODES} from "~/graphql/queries/standalone/promocodes";
|
||||
import {GET_ORDERS} from "~/graphql/queries/standalone/orders";
|
||||
|
||||
export const getUserBaseData = (
|
||||
orderVariables?: {
|
||||
userEmail?: string;
|
||||
status?: string;
|
||||
}
|
||||
) => {
|
||||
const { document, variables } = combineQuery('getUserBaseData')
|
||||
.add(GET_WISHLIST)
|
||||
.add(GET_PROMOCODES)
|
||||
.add(GET_ORDERS, orderVariables || {})
|
||||
|
||||
return { document, variables };
|
||||
};
|
||||
|
|
@ -3,18 +3,28 @@ import {ORDER_FRAGMENT} from "@/graphql/fragments/orders.fragment.js";
|
|||
export const GET_ORDERS = gql`
|
||||
query getOrders(
|
||||
$status: String!,
|
||||
$userEmail: String!
|
||||
$userEmail: String!,
|
||||
$first: Int,
|
||||
$after: String,
|
||||
$search: String,
|
||||
) {
|
||||
orders(
|
||||
status: $status,
|
||||
orderBy: "-buyTime",
|
||||
userEmail: $userEmail
|
||||
userEmail: $userEmail,
|
||||
first: $first,
|
||||
after: $after,
|
||||
search: $search
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...Order
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
${ORDER_FRAGMENT}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
},
|
||||
"fields": {
|
||||
"search": "Search",
|
||||
"searchOrder": "Search order",
|
||||
"name": "Name",
|
||||
"firstName": "First name",
|
||||
"lastName": "Last name",
|
||||
|
|
@ -126,6 +127,7 @@
|
|||
"home": "Home",
|
||||
"catalog": "Catalog",
|
||||
"contact": "Contact",
|
||||
"orders": "Orders",
|
||||
"wishlist": "Wishlist",
|
||||
"cart": "Cart",
|
||||
"settings": "Settings",
|
||||
|
|
@ -163,23 +165,42 @@
|
|||
"referralTooltip": "You will get a referral link after a successful purchase"
|
||||
},
|
||||
"orders": {
|
||||
"title": "Orders"
|
||||
"title": "Orders",
|
||||
"chooseStatus": "Choose status",
|
||||
"id": "ID",
|
||||
"price": "Price",
|
||||
"total": "Total",
|
||||
"empty": "There are not any orders by this parameters.",
|
||||
"statuses": {
|
||||
"all": "All",
|
||||
"failed": "Failed",
|
||||
"payment": "Payment",
|
||||
"created": "Created",
|
||||
"delivering": "Delivering",
|
||||
"finished": "Finished",
|
||||
"momental": "Momental"
|
||||
},
|
||||
"searchTooltip": "Enter order id or product name"
|
||||
},
|
||||
"wishlist": {
|
||||
"title": "Wishlist",
|
||||
"total": "{quantity} items worth {amount}",
|
||||
"deleteTooltip": "Delete all from wishlist"
|
||||
"deleteTooltip": "Delete all from wishlist",
|
||||
"empty": "Your wishlist is empty."
|
||||
},
|
||||
"cart": {
|
||||
"title": "Cart",
|
||||
"quantity": "Quantity: ",
|
||||
"total": "Total: "
|
||||
"total": "Total",
|
||||
"empty": "Your cart is empty."
|
||||
},
|
||||
"balance": {
|
||||
"title": "Balance"
|
||||
},
|
||||
"promocodes": {
|
||||
"title": "Promocodes"
|
||||
"title": "Promocodes",
|
||||
"until": "Until",
|
||||
"empty": "You don't have any promocodes."
|
||||
},
|
||||
"logout": "Logout"
|
||||
}
|
||||
|
|
|
|||
11
storefront/middleware/auth.ts
Normal file
11
storefront/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export default defineNuxtRouteMiddleware(() => {
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const localePath = useLocalePath();
|
||||
|
||||
if (!userStore.isAuthenticated) {
|
||||
appStore.setActiveState('login');
|
||||
|
||||
return navigateTo(localePath('/'));
|
||||
}
|
||||
})
|
||||
|
|
@ -2,6 +2,7 @@ import { defineNuxtConfig } from 'nuxt/config';
|
|||
import { i18nConfig } from './config/i18n';
|
||||
import {fileURLToPath, URL} from "node:url";
|
||||
import { resolve } from 'node:path';
|
||||
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||
|
||||
export default defineNuxtConfig({
|
||||
ssr: true,
|
||||
|
|
@ -92,6 +93,20 @@ export default defineNuxtConfig({
|
|||
file: resolve(__dirname, 'pages/index.vue')
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
// 'apollo:client:created'(apolloClient, { key }) {
|
||||
// console.log(key)
|
||||
// console.log('log')
|
||||
// if ( key !== 'default' ) return
|
||||
// const runtime = useRuntimeConfig()
|
||||
// const uploadLink = createUploadLink({
|
||||
// uri: `https://api.${runtime.public.evibesBaseDomain}/graphql/`,
|
||||
// credentials: 'include',
|
||||
// headers: {
|
||||
// 'X-EVIBES-AUTH': useCookie(`${runtime.public.evibesProjectName?.toLowerCase()}-access`).value || ''
|
||||
// }
|
||||
// })
|
||||
// apolloClient.setLink(uploadLink)
|
||||
// }
|
||||
}
|
||||
})
|
||||
37
storefront/package-lock.json
generated
37
storefront/package-lock.json
generated
|
|
@ -15,7 +15,7 @@
|
|||
"@vueuse/integrations": "^13.3.0",
|
||||
"@vueuse/nuxt": "^13.3.0",
|
||||
"@vueuse/router": "^13.3.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"apollo-upload-client": "^18.0.1",
|
||||
"axios": "^1.9.0",
|
||||
"graphql-combine-query": "^1.2.4",
|
||||
"graphql-tag": "^2.12.6",
|
||||
|
|
@ -3674,21 +3674,21 @@
|
|||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "18.0.1",
|
||||
"resolved": "https://registry.npmjs.org/apollo-upload-client/-/apollo-upload-client-18.0.1.tgz",
|
||||
"integrity": "sha512-OQvZg1rK05VNI79D658FUmMdoI2oB/KJKb6QGMa2Si25QXOaAvLMBFUEwJct7wf+19U8vk9ILhidBOU1ZWv6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extract-files": "^11.0.0"
|
||||
"extract-files": "^13.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >= 16.0.0"
|
||||
"node": "^18.15.0 || >=20.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jaydenseric"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apollo/client": "^3.0.0",
|
||||
"@apollo/client": "^3.8.0",
|
||||
"graphql": "14 - 16"
|
||||
}
|
||||
},
|
||||
|
|
@ -6223,17 +6223,32 @@
|
|||
"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==",
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-13.0.0.tgz",
|
||||
"integrity": "sha512-FXD+2Tsr8Iqtm3QZy1Zmwscca7Jx3mMC5Crr+sEP1I303Jy1CYMuYCm7hRTplFNg3XdUavErkxnTzpaqdSoi6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-obj": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jaydenseric"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-files/node_modules/is-plain-obj": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"@vueuse/integrations": "^13.3.0",
|
||||
"@vueuse/nuxt": "^13.3.0",
|
||||
"@vueuse/router": "^13.3.0",
|
||||
"apollo-upload-client": "17.0.0",
|
||||
"apollo-upload-client": "^18.0.1",
|
||||
"axios": "^1.9.0",
|
||||
"graphql-combine-query": "^1.2.4",
|
||||
"graphql-tag": "^2.12.6",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ useHead({
|
|||
|
||||
const token = useRouteQuery('token', '');
|
||||
const uid = useRouteQuery('uid', '');
|
||||
const referrer = useRouteQuery('referrer', '');
|
||||
|
||||
const { activateUser } = useUserActivation();
|
||||
|
||||
|
|
@ -32,6 +33,10 @@ onMounted( async () => {
|
|||
if (route.path.includes('reset-password') && token.value && uid.value) {
|
||||
appStore.setActiveState('new-password');
|
||||
}
|
||||
|
||||
if (referrer.value) {
|
||||
appStore.setActiveState('register');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ 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";
|
||||
import {useOrderOverwrite} from "~/composables/orders";
|
||||
|
||||
const route = useRoute();
|
||||
const {t} = useI18n();
|
||||
|
|
|
|||
|
|
@ -12,35 +12,24 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile {
|
||||
position: relative;
|
||||
padding-top: 50px;
|
||||
height: calc(100vh - 125px);
|
||||
display: flex;
|
||||
|
||||
& .container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 100px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,37 +3,41 @@
|
|||
<div class="cart__top">
|
||||
<div class="cart__top-left">
|
||||
<p><span>{{ t('profile.cart.quantity') }}</span> {{ productsInCartQuantity }}</p>
|
||||
<p><span>{{ t('profile.cart.total') }}</span> {{ totalPrice }}</p>
|
||||
<p><span>{{ t('profile.cart.total') }}: </span> {{ totalPrice }}{{ CURRENCY }}</p>
|
||||
</div>
|
||||
<ui-button class="cart__top-button">{{ t('buttons.checkout') }}</ui-button>
|
||||
</div>
|
||||
<div class="cart__list">
|
||||
<cards-product
|
||||
v-for="product in productsInCart"
|
||||
:key="product.node.uuid"
|
||||
:product="product.node.product"
|
||||
:isList="true"
|
||||
:isToolsVisible="true"
|
||||
/>
|
||||
<div class="cart__list-inner" v-if="productsInCart.length">
|
||||
<cards-product
|
||||
v-for="product in productsInCart"
|
||||
:key="product.node.uuid"
|
||||
:product="product.node.product"
|
||||
:isList="true"
|
||||
:isToolsVisible="true"
|
||||
/>
|
||||
</div>
|
||||
<p class="cart__empty">{{ t('profile.cart.empty') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {usePageTitle} from "~/composables/utils";
|
||||
import {CURRENCY} from "~/config/constants";
|
||||
|
||||
const {t} = useI18n();
|
||||
const cartStore = useCartStore();
|
||||
|
||||
const productsInCart = computed(() => {
|
||||
return cartStore.currentOrder.orderProducts ? cartStore.currentOrder.orderProducts.edges : [];
|
||||
return cartStore.currentOrder ? cartStore.currentOrder.orderProducts.edges : [];
|
||||
});
|
||||
const totalPrice = computed(() => {
|
||||
return cartStore.currentOrder.totalPrice ? cartStore.currentOrder.totalPrice : [];
|
||||
return cartStore.currentOrder ? cartStore.currentOrder.totalPrice : 0;
|
||||
});
|
||||
const productsInCartQuantity = computed(() => {
|
||||
let count = 0;
|
||||
cartStore.currentOrder.orderProducts?.edges.forEach((el) => {
|
||||
cartStore.currentOrder?.orderProducts?.edges.forEach((el) => {
|
||||
count = count + el.node.quantity;
|
||||
});
|
||||
|
||||
|
|
@ -51,8 +55,6 @@ setPageTitle(t('breadcrumbs.cart'));
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
&__top {
|
||||
width: 100%;
|
||||
|
|
@ -78,13 +80,18 @@ setPageTitle(t('breadcrumbs.cart'));
|
|||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: $white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||
border-radius: $default_border_radius;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +1,228 @@
|
|||
<template>
|
||||
<div class="settings">
|
||||
<p>orders</p>
|
||||
<div class="orders">
|
||||
<div class="orders__top">
|
||||
<h2>{{ t('profile.orders.title') }}</h2>
|
||||
<el-tooltip
|
||||
:visible="isSearchFocused"
|
||||
:content="t('profile.orders.searchTooltip')"
|
||||
placement="top-start"
|
||||
>
|
||||
<form class="orders__search" @submit.prevent="submitSearch">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="search"
|
||||
v-model="searchInput"
|
||||
:placeholder="t('fields.searchOrder')"
|
||||
@focus="isSearchFocused = true"
|
||||
@blur="isSearchFocused = false"
|
||||
>
|
||||
<button type="submit">
|
||||
<icon name="tabler:search" size="16" />
|
||||
</button>
|
||||
</form>
|
||||
</el-tooltip>
|
||||
<el-select
|
||||
v-model="status"
|
||||
size="large"
|
||||
style="width: 240px"
|
||||
:placeholder="t('profile.orders.chooseStatus')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="orders__inner" v-if="orders.length">
|
||||
<el-collapse
|
||||
v-model="collapse"
|
||||
class="orders__list"
|
||||
>
|
||||
<cards-order
|
||||
v-for="order in orders"
|
||||
:key="order.node.uuid"
|
||||
:order="order.node"
|
||||
/>
|
||||
</el-collapse>
|
||||
<div class="orders__list" v-if="pending">
|
||||
<skeletons-cards-order
|
||||
v-for="idx in 5"
|
||||
:key="idx"
|
||||
/>
|
||||
</div>
|
||||
<div class="orders__list-observer" ref="observer"></div>
|
||||
</div>
|
||||
<div class="orders__empty" v-if="!orders.length && !pending">
|
||||
<p>{{ t('profile.orders.empty') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {usePageTitle} from "~/composables/utils";
|
||||
import {useOrders} from "~/composables/orders";
|
||||
import {CURRENCY, orderStatuses} from "~/config/constants";
|
||||
import {useDate} from "~/composables/date";
|
||||
import {useRouteQuery} from "@vueuse/router";
|
||||
|
||||
const {t} = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const userEmail = computed(() => {
|
||||
return userStore.user ? userStore.user.email : '';
|
||||
});
|
||||
|
||||
const status = useRouteQuery<string>('status', '');
|
||||
const search = useRouteQuery<string>('search', '');
|
||||
const searchInput = ref<string>('');
|
||||
|
||||
const { pending, orders, pageInfo, variables } = await useOrders({
|
||||
userEmail: userEmail.value,
|
||||
status: status.value,
|
||||
search: search.value,
|
||||
after: ''
|
||||
});
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '',
|
||||
label: t('profile.orders.statuses.all')
|
||||
},
|
||||
{
|
||||
value: orderStatuses.FAILED,
|
||||
label: t('profile.orders.statuses.failed')
|
||||
},
|
||||
{
|
||||
value: orderStatuses.PAYMENT,
|
||||
label: t('profile.orders.statuses.payment')
|
||||
},
|
||||
{
|
||||
value: orderStatuses.CREATED,
|
||||
label: t('profile.orders.statuses.created')
|
||||
},
|
||||
{
|
||||
value: orderStatuses.DELIVERING,
|
||||
label: t('profile.orders.statuses.delivering')
|
||||
},
|
||||
{
|
||||
value: orderStatuses.FINISHED,
|
||||
label: t('profile.orders.statuses.finished')
|
||||
},
|
||||
{
|
||||
value: orderStatuses.MOMENTAL,
|
||||
label: t('profile.orders.statuses.momental')
|
||||
}
|
||||
];
|
||||
|
||||
const collapse = ref([]);
|
||||
const isSearchFocused = ref<boolean>(false);
|
||||
|
||||
const observer = ref(null);
|
||||
useIntersectionObserver(
|
||||
observer,
|
||||
async ([{ isIntersecting }]) => {
|
||||
if (isIntersecting && pageInfo.value?.hasNextPage && !pending.value) {
|
||||
variables.after = pageInfo.value.endCursor;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const submitSearch = () => {
|
||||
search.value = searchInput.value;
|
||||
variables.search = searchInput.value || '';
|
||||
};
|
||||
|
||||
watch(status, newVal => {
|
||||
variables.status = newVal || '';
|
||||
});
|
||||
|
||||
const { setPageTitle } = usePageTitle();
|
||||
|
||||
setPageTitle(t('breadcrumbs.orders'));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings {
|
||||
.orders {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
&__top {
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 50px;
|
||||
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||
border-radius: $default_border_radius;
|
||||
}
|
||||
|
||||
&__search {
|
||||
border-radius: $default_border_radius;
|
||||
border: 1px solid #dedede;
|
||||
overflow: hidden;
|
||||
background-color: $white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
& input {
|
||||
background-color: transparent;
|
||||
height: 100%;
|
||||
padding-inline: 15px;
|
||||
width: 100%;
|
||||
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
& button {
|
||||
height: 100%;
|
||||
padding-inline: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: $default_border_radius;
|
||||
background-color: rgba($accent, 0.2);
|
||||
border: 1px solid $accent;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
transition: 0.2s;
|
||||
|
||||
color: $accent;
|
||||
|
||||
@include hover {
|
||||
background-color: $accent;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
|
||||
&-observer {
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
background-color: $white;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||
border-radius: $default_border_radius;
|
||||
|
||||
& p {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,29 +1,38 @@
|
|||
<template>
|
||||
<div class="promocodes">
|
||||
<h2>{{ t('profile.promocodes.title') }}</h2>
|
||||
<div class="promocodes__list">
|
||||
<div class="promocodes__list" v-if="promocodes?.length">
|
||||
<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 class="promocodes__item-left">
|
||||
<icon
|
||||
name="material-symbols:content-copy"
|
||||
size="20"
|
||||
class="promocodes__item-button"
|
||||
@click="copyCode(promocode.node.code)"
|
||||
/>
|
||||
<p>{{ promocode.node.code }}</p>
|
||||
</div>
|
||||
<div class="promocodes__item-right">
|
||||
<p class="promocodes__item-text">{{ promocode.node.discount }} {{ promocode.node.discountType === 'percent' ? '%' : CURRENCY }}</p>
|
||||
<div class="promocodes__item-expire">{{ t('profile.promocodes.until') }} {{ useDate(promocode.node.endTime, locale) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="promocodes__empty">{{ t('profile.promocodes.empty') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {usePageTitle} from "~/composables/utils";
|
||||
import {useNotification} from "~/composables/notification/index.js";
|
||||
import {CURRENCY} from "~/config/constants";
|
||||
import {useDate} from "~/composables/date";
|
||||
|
||||
const {t} = useI18n();
|
||||
const {t, locale} = useI18n();
|
||||
const promocodesStore = usePromocodeStore();
|
||||
|
||||
const promocodes = computed(() => promocodesStore.promocodes);
|
||||
|
|
@ -40,7 +49,7 @@ const copyCode = (code: string) => {
|
|||
console.error(err);
|
||||
});
|
||||
};
|
||||
// TODO: display more info about promo
|
||||
// TODO: display time for expire date
|
||||
const { setPageTitle } = usePageTitle();
|
||||
|
||||
setPageTitle(t('breadcrumbs.promocodes'));
|
||||
|
|
@ -65,10 +74,20 @@ setPageTitle(t('breadcrumbs.promocodes'));
|
|||
&__item {
|
||||
border-radius: $default_border_radius;
|
||||
border: 1px solid $accent;
|
||||
padding: 7px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 7px 25px;
|
||||
|
||||
& p {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&-button {
|
||||
cursor: pointer;
|
||||
|
|
@ -80,9 +99,31 @@ setPageTitle(t('breadcrumbs.promocodes'));
|
|||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
font-weight: 600;
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&-expire {
|
||||
height: 100%;
|
||||
background-color: $accent;
|
||||
padding-inline: 15px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
margin-top: 50px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
</el-tooltip>
|
||||
</div>
|
||||
<div class="wishlist__list">
|
||||
<div class="wishlist__list-inner">
|
||||
<div class="wishlist__list-inner" v-if="productsInWishlist.length">
|
||||
<div
|
||||
class="wishlist__item"
|
||||
v-for="product in productsInWishlist"
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="wishlist__empty">{{ t('profile.wishlist.empty') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -112,8 +113,6 @@ setPageTitle(t('breadcrumbs.wishlist'));
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
&__top {
|
||||
width: 100%;
|
||||
|
|
@ -162,8 +161,6 @@ setPageTitle(t('breadcrumbs.wishlist'));
|
|||
padding: 20px;
|
||||
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.2);
|
||||
border-radius: $default_border_radius;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
|
|
@ -178,5 +175,9 @@ setPageTitle(t('breadcrumbs.wishlist'));
|
|||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
storefront/plugins/apollo-upload-link.ts
Normal file
21
storefront/plugins/apollo-upload-link.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
const { client } = useApolloClient()
|
||||
|
||||
if (client.name !== 'default') return
|
||||
|
||||
const runtime = useRuntimeConfig()
|
||||
const token = useCookie(
|
||||
`${runtime.public.evibesProjectName?.toLowerCase()}-access`
|
||||
).value || ''
|
||||
|
||||
const uploadLink = createUploadLink({
|
||||
uri: `https://api.${runtime.public.evibesBaseDomain}/graphql/`,
|
||||
credentials: 'include',
|
||||
headers: { 'X-EVIBES-AUTH': token }
|
||||
})
|
||||
|
||||
client.setLink(uploadLink)
|
||||
console.log('✅ apollo link replaced to uploadLink')
|
||||
})
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type {IUser} from "~/types";
|
||||
import {useAppConfig} from "~/composables/config";
|
||||
import {orderStatuses} from "~/config/constants";
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const { COOKIES_ACCESS_TOKEN_KEY } = useAppConfig();
|
||||
|
|
@ -15,7 +16,7 @@ export const useUserStore = defineStore('user', () => {
|
|||
|
||||
const isAuthenticated = computed(() => Boolean(cookieAccess.value && user.value));
|
||||
const finishedOrdersQuantity = computed(() => {
|
||||
return user.value?.orders.filter((order) => order.status === 'FINISHED').length || 0;
|
||||
return user.value?.orders.filter((order) => order.status === orderStatuses.FINISHED).length || 0;
|
||||
});
|
||||
|
||||
const setUser = (data: IUser | null) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import type {IOrder} from "~/types";
|
||||
|
||||
export interface IOrderResponse {
|
||||
export interface IOrdersResponse {
|
||||
orders: {
|
||||
edges: {
|
||||
node: IOrder
|
||||
}[]
|
||||
node: IOrder
|
||||
}[],
|
||||
pageInfo: {
|
||||
hasNextPage: boolean
|
||||
endCursor: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import type {IUser} from "~/types";
|
||||
import type {
|
||||
IOrdersResponse,
|
||||
IPromocodesResponse,
|
||||
IUser,
|
||||
IWishlistResponse
|
||||
} from "~/types";
|
||||
|
||||
export interface IUserResponse {
|
||||
updateUser: {
|
||||
|
|
@ -6,6 +11,11 @@ export interface IUserResponse {
|
|||
}
|
||||
}
|
||||
|
||||
export interface IUserBaseDataResponse
|
||||
extends IWishlistResponse,
|
||||
IOrdersResponse,
|
||||
IPromocodesResponse {}
|
||||
|
||||
export interface IUserActivationResponse {
|
||||
activateUser: {
|
||||
success: boolean
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface IOrder {
|
|||
status: string,
|
||||
buyTime: string | null,
|
||||
humanReadableId: string,
|
||||
notifications: string | null,
|
||||
orderProducts: {
|
||||
edges: {
|
||||
node: {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ export interface IPromocode {
|
|||
discount: string,
|
||||
discountType: string,
|
||||
endTime: string,
|
||||
id: string,
|
||||
startTime: string,
|
||||
usedOn: string,
|
||||
uuid: string
|
||||
}
|
||||
Loading…
Reference in a new issue