feat(addresses): add address management feature with UI and logic integration
Introduced address management functionality, including address creation, deletion, and display with full localization support. Integrated GraphQL queries, mutations, and reusable composables for backend communication. - Added `addresses.vue` to profile for managing user addresses. - Implemented `useAddressCreate`, `useAddressDelete`, and `useAddressAutocomplete` composables. - Created reusable components: `forms/address.vue` and `cards/address.vue`. - Updated `navigation.vue` to include addresses in profile navigation. - Enhanced localization files for address-related translations. This improves user experience by enabling comprehensive address management in the profile section. No breaking changes.
This commit is contained in:
parent
cb92b7ddaa
commit
048da0e251
25 changed files with 829 additions and 10 deletions
|
|
@ -2,6 +2,26 @@
|
|||
|
||||
.el-select__wrapper {
|
||||
height: 36px !important;
|
||||
min-height: 36px !important;
|
||||
min-height: 50px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.el-select--large .el-select__wrapper {
|
||||
border-radius: $default_border_radius !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid $border !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
.el-select__placeholder.is-transparent {
|
||||
color: $disabled_secondary !important;
|
||||
}
|
||||
|
||||
.el-select__popper.el-popper {
|
||||
background-color: $main_hover !important;
|
||||
}
|
||||
.el-select-dropdown__item {
|
||||
color: $primary_dark !important;
|
||||
}
|
||||
.el-select-dropdown__item.is-hovering {
|
||||
color: $primary !important;
|
||||
background-color: $main !important;
|
||||
}
|
||||
113
storefront/app/components/cards/address.vue
Normal file
113
storefront/app/components/cards/address.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<p class="card__title">{{ name }}</p>
|
||||
<div class="card__list">
|
||||
<ui-input
|
||||
v-model="address.country"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.country')"
|
||||
/>
|
||||
<ui-input
|
||||
v-model="address.city"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.city')"
|
||||
/>
|
||||
<ui-input
|
||||
v-if="address.region"
|
||||
v-model="address.region"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.region')"
|
||||
/>
|
||||
<ui-input
|
||||
v-else-if="address.district"
|
||||
v-model="address.district"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.district')"
|
||||
/>
|
||||
<ui-input
|
||||
v-model="address.postalCode"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.postalCode')"
|
||||
/>
|
||||
<ui-input
|
||||
v-model="address.street"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.street')"
|
||||
/>
|
||||
<ui-input
|
||||
v-model="entrance"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.entrance')"
|
||||
/>
|
||||
<ui-input
|
||||
v-model="floor"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.floor')"
|
||||
/>
|
||||
<ui-input
|
||||
v-model="apartmentNumber"
|
||||
:isDisabled="true"
|
||||
:label="t('fields.apartNumber')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {IAddress} from "@types";
|
||||
|
||||
const props = defineProps<{
|
||||
address: IAddress;
|
||||
}>();
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
const isHouse = computed<boolean>(() => {
|
||||
return props.address.addressLine.split(' ').find(el => el.includes('is_house'))?.split('=')[1] === 'true';
|
||||
});
|
||||
|
||||
const name = computed<string | undefined>(() => {
|
||||
return props.address.addressLine.split(' ').find(el => el.includes('name'))?.split('=')[1]?.split('_').join(' ');
|
||||
});
|
||||
|
||||
const apartmentNumber = computed<string | undefined>(() => {
|
||||
return props.address.addressLine.split(' ').find(el => el.includes('apartment_number'))?.split('=')[1];
|
||||
});
|
||||
|
||||
const floor = computed<string | undefined>(() => {
|
||||
return props.address.addressLine.split(' ').find(el => el.includes('floor'))?.split('=')[1];
|
||||
});
|
||||
|
||||
const entrance = computed<string | undefined>(() => {
|
||||
return props.address.addressLine.split(' ').find(el => el.includes('entrance'))?.split('=')[1];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
border: 1px solid $border;
|
||||
border-radius: $default_border_radius;
|
||||
padding: 15px 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
font-family: "Playfair Display", sans-serif;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.block__input) {
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
181
storefront/app/components/forms/address.vue
Normal file
181
storefront/app/components/forms/address.vue
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<template>
|
||||
<form class="form" @submit.prevent="handleCreate">
|
||||
<div class="form__block">
|
||||
<p>{{ t('fields.city') }}</p>
|
||||
<el-select
|
||||
v-model="city"
|
||||
size="large"
|
||||
:placeholder="t('fields.city')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="form__block">
|
||||
<p>{{ t('fields.address') }}</p>
|
||||
<el-select
|
||||
size="large"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="handleSearch"
|
||||
v-model="query"
|
||||
:placeholder="t('fields.address')"
|
||||
:loading="autocompleteLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, idx) in completeResults"
|
||||
:key="idx"
|
||||
:label="item.display_name"
|
||||
:value="item.display_name"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
v-model="name"
|
||||
:label="t('fields.addressName')"
|
||||
:placeholder="t('fields.addressName')"
|
||||
/>
|
||||
<ui-checkbox v-model="isHouse">
|
||||
{{ t('checkboxes.isHouse') }}
|
||||
</ui-checkbox>
|
||||
<div class="form__box">
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
v-model="entrance"
|
||||
:placeholder="t('fields.entrance')"
|
||||
:label="t('fields.entrance')"
|
||||
:numberOnly="true"
|
||||
:inputMode="'decimal'"
|
||||
:isDisabled="isHouse"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
v-model="floor"
|
||||
:placeholder="t('fields.floor')"
|
||||
:label="t('fields.floor')"
|
||||
:numberOnly="true"
|
||||
:inputMode="'decimal'"
|
||||
:isDisabled="isHouse"
|
||||
/>
|
||||
<ui-input
|
||||
:type="'text'"
|
||||
v-model="apartNumber"
|
||||
:placeholder="t('fields.apartNumber')"
|
||||
:label="t('fields.apartNumber')"
|
||||
:numberOnly="true"
|
||||
:inputMode="'decimal'"
|
||||
:isDisabled="isHouse"
|
||||
/>
|
||||
</div>
|
||||
<ui-button
|
||||
:type="'submit'"
|
||||
:isLoading="createLoading"
|
||||
:isDisabled="!isFormValid"
|
||||
class="form__button"
|
||||
>
|
||||
{{ t('buttons.add') }}
|
||||
</ui-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useAddressAutocomplete, useAddressCreate} from "@composables/adresses";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { createAddress, loading: createLoading } = useAddressCreate();
|
||||
|
||||
const {
|
||||
query,
|
||||
completeResults,
|
||||
loading: autocompleteLoading,
|
||||
handleSearch,
|
||||
} = useAddressAutocomplete();
|
||||
|
||||
const name = ref<string>('');
|
||||
const isHouse = ref<boolean>(false);
|
||||
const entrance = ref<string>('');
|
||||
const floor = ref<string>('');
|
||||
const apartNumber = ref<string>('');
|
||||
const city = ref<string>('');
|
||||
const options = [
|
||||
{
|
||||
value: 'Moscow',
|
||||
label: t('forms.address.moscow'),
|
||||
}
|
||||
];
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
const baseValid =
|
||||
city.value !== '' &&
|
||||
query.value !== '' &&
|
||||
name.value !== '';
|
||||
|
||||
if (isHouse.value) {
|
||||
return baseValid;
|
||||
}
|
||||
|
||||
return (
|
||||
baseValid &&
|
||||
entrance.value !== '' &&
|
||||
floor.value !== '' &&
|
||||
apartNumber.value !== ''
|
||||
);
|
||||
});
|
||||
|
||||
watch(isHouse, (val) => {
|
||||
if (val) {
|
||||
entrance.value = '';
|
||||
floor.value = '';
|
||||
apartNumber.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
async function handleCreate() {
|
||||
await createAddress({
|
||||
rawData: query.value,
|
||||
addressLine1: isHouse.value
|
||||
? `name=${name.value} is_house=true`
|
||||
: `entrance=${entrance.value} floor=${floor.value} apartment_number=${apartNumber.value} name=${name.value} is_house=false`
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
&__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
& p {
|
||||
color: $secondary;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin-top: 10px;
|
||||
width: fit-content;
|
||||
padding-inline: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -33,6 +33,14 @@
|
|||
<icon name="fluent:ticket-20-filled" size="20" />
|
||||
{{ t('profile.promocodes.title') }}
|
||||
</nuxt-link-locale>
|
||||
<nuxt-link-locale
|
||||
class="nav__item"
|
||||
:class="[{ active: route.path.includes('addresses') }]"
|
||||
to="/profile/addresses"
|
||||
>
|
||||
<icon name="material-symbols:location-on" size="20" />
|
||||
{{ t('profile.addresses.title') }}
|
||||
</nuxt-link-locale>
|
||||
</div>
|
||||
<div class="nav__logout" @click="logout">
|
||||
<icon name="material-symbols:power-settings-new-outline" size="20" />
|
||||
|
|
@ -90,6 +98,10 @@ const { logout } = useLogout();
|
|||
color: $primary;
|
||||
background-color: $main_hover;
|
||||
}
|
||||
|
||||
& span {
|
||||
color: $secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&__logout {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<el-skeleton class="sk" animated>
|
||||
<template #template>
|
||||
<div class="sk__item">
|
||||
<el-skeleton-item
|
||||
variant="p"
|
||||
class="sk__item-text"
|
||||
v-for="idx in 5"
|
||||
:key="idx"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sk {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
&__item {
|
||||
padding: 5px 15px;
|
||||
|
||||
&-text {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
@input="onInput"
|
||||
@keydown="numberOnly ? onlyNumbersKeydown($event) : null"
|
||||
class="block__input"
|
||||
:disabled="isDisabled"
|
||||
:inputmode="inputMode || 'text'"
|
||||
>
|
||||
<button
|
||||
|
|
@ -39,6 +40,7 @@ const props = defineProps<{
|
|||
rules?: Rule[];
|
||||
label?: string;
|
||||
numberOnly?: boolean;
|
||||
isDisabled?: boolean;
|
||||
inputMode?: "text" | "email" | "search" | "tel" | "url" | "none" | "numeric" | "decimal";
|
||||
}>();
|
||||
|
||||
|
|
@ -132,6 +134,11 @@ function onInput(e: Event) {
|
|||
&::placeholder {
|
||||
color: $disabled_secondary;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: $main_hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__eyes {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<div class="search__results" :class="[{ active: (searchResults && isSearchActive) || loading }]">
|
||||
<skeletons-header-search v-if="loading" />
|
||||
<skeletons-ui-search v-if="loading"/>
|
||||
<div
|
||||
class="search__results-inner"
|
||||
v-for="(blocks, category) in filteredSearchResults"
|
||||
|
|
|
|||
4
storefront/app/composables/adresses/index.ts
Normal file
4
storefront/app/composables/adresses/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './useAddressAutocomplete';
|
||||
export * from './useAddressCreate';
|
||||
export * from './useAddressDelete';
|
||||
export * from './useAddresses';
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import {AUTOCOMPLETE_ADDRESS} from "@graphql/mutations/addresses";
|
||||
import type {IAddressAutocompleteResponse, IAddressCompleteResult} from "@types";
|
||||
|
||||
export function useAddressAutocomplete() {
|
||||
const { t } = useI18n();
|
||||
|
||||
const query = ref('');
|
||||
const completeResults = ref<IAddressCompleteResult | null>(null);
|
||||
|
||||
const { mutate, loading, error } = useMutation<IAddressAutocompleteResponse>(AUTOCOMPLETE_ADDRESS);
|
||||
|
||||
async function complete(query: string) {
|
||||
completeResults.value = null;
|
||||
const result = await mutate({
|
||||
limit: 5,
|
||||
q: query,
|
||||
});
|
||||
|
||||
if (result?.data?.autocompleteAddress) {
|
||||
completeResults.value = result.data.autocompleteAddress.suggestions
|
||||
|
||||
return {
|
||||
results: result.data.autocompleteAddress.suggestions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedComplete = useDebounceFn(async (val: string) => {
|
||||
if (!val) {
|
||||
completeResults.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await complete(val);
|
||||
}, 400);
|
||||
|
||||
async function handleSearch(val: string) {
|
||||
await debouncedComplete(val);
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (!err) return;
|
||||
console.error('useAddressAutocomplete error:', err);
|
||||
let message = t('popup.errors.defaultError');
|
||||
if (isGraphQLError(err)) {
|
||||
message = err.graphQLErrors?.[0]?.message || message;
|
||||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
$notify({
|
||||
message,
|
||||
type: 'error',
|
||||
title: t('popup.errors.main'),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
query,
|
||||
completeResults,
|
||||
loading,
|
||||
handleSearch
|
||||
};
|
||||
}
|
||||
47
storefront/app/composables/adresses/useAddressCreate.ts
Normal file
47
storefront/app/composables/adresses/useAddressCreate.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {CREATE_ADDRESS} from "@graphql/mutations/addresses";
|
||||
import type {IAddressCreateResponse} from "@types";
|
||||
|
||||
interface ICreateAddressArgs {
|
||||
rawData: string;
|
||||
addressLine1: string;
|
||||
addressLine2?: string;
|
||||
}
|
||||
|
||||
export function useAddressCreate() {
|
||||
const { t } = useI18n();
|
||||
const { $notify } = useNuxtApp();
|
||||
|
||||
const { mutate, loading, error } = useMutation<IAddressCreateResponse>(CREATE_ADDRESS);
|
||||
|
||||
async function createAddress(args: ICreateAddressArgs) {
|
||||
const result = await mutate(args);
|
||||
|
||||
if (result?.data?.createAddress?.address?.uuid) {
|
||||
$notify({
|
||||
message: t('popup.success.createAddress'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (!err) return;
|
||||
console.error('useAddressCreate error:', err);
|
||||
let message = t('popup.errors.defaultError');
|
||||
if (isGraphQLError(err)) {
|
||||
message = err.graphQLErrors?.[0]?.message || message;
|
||||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
$notify({
|
||||
message,
|
||||
type: 'error',
|
||||
title: t('popup.errors.main'),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
createAddress,
|
||||
loading
|
||||
};
|
||||
}
|
||||
41
storefront/app/composables/adresses/useAddressDelete.ts
Normal file
41
storefront/app/composables/adresses/useAddressDelete.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import {DELETE_ADDRESS} from "@graphql/mutations/addresses";
|
||||
import type {IAddressDeleteResponse} from "@types";
|
||||
|
||||
export function useAddressDelete() {
|
||||
const { t } = useI18n();
|
||||
const { $notify } = useNuxtApp();
|
||||
|
||||
const { mutate, loading, error } = useMutation<IAddressDeleteResponse>(DELETE_ADDRESS);
|
||||
|
||||
async function deleteAddress(uuid: string) {
|
||||
const result = await mutate({ uuid });
|
||||
|
||||
if (result?.data?.deleteAddress?.success) {
|
||||
$notify({
|
||||
message: t('popup.success.deleteAddress'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (!err) return;
|
||||
console.error('useAddressDelete error:', err);
|
||||
let message = t('popup.errors.defaultError');
|
||||
if (isGraphQLError(err)) {
|
||||
message = err.graphQLErrors?.[0]?.message || message;
|
||||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
$notify({
|
||||
message,
|
||||
type: 'error',
|
||||
title: t('popup.errors.main'),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
deleteAddress,
|
||||
loading
|
||||
};
|
||||
}
|
||||
20
storefront/app/composables/adresses/useAddresses.ts
Normal file
20
storefront/app/composables/adresses/useAddresses.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import {GET_ADDRESSES} from "@graphql/queries/standalone/addresses";
|
||||
import type { IAddressesResponse } from '@types';
|
||||
|
||||
export async function useAddresses() {
|
||||
const addressesStore = useAddressesStore();
|
||||
|
||||
const { data, error } = await useAsyncQuery<IAddressesResponse>(GET_ADDRESSES);
|
||||
|
||||
if (!error.value && data.value?.addresses.edges[0]) {
|
||||
addressesStore.setAddresses(data.value.addresses.edges[0].node);
|
||||
}
|
||||
|
||||
watch(error, (err) => {
|
||||
if (err) {
|
||||
console.error('useAddresses error:', err);
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ export function useUserBaseData() {
|
|||
const wishlistStore = useWishlistStore();
|
||||
const cartStore = useCartStore();
|
||||
const promocodeStore = usePromocodeStore();
|
||||
const addressesStore = useAddressesStore();
|
||||
|
||||
const { syncWishlist } = useWishlistSync();
|
||||
const { syncOrder } = useOrderSync();
|
||||
|
|
@ -40,6 +41,9 @@ export function useUserBaseData() {
|
|||
if (data?.promocodes.edges) {
|
||||
promocodeStore.setPromocodes(data.promocodes.edges);
|
||||
}
|
||||
if (data?.addresses.edges) {
|
||||
addressesStore.setAddresses(data.addresses.edges);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
12
storefront/app/graphql/fragments/address.fragment.ts
Normal file
12
storefront/app/graphql/fragments/address.fragment.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export const ADDRESS_FRAGMENT = gql`
|
||||
fragment Address on AddressType {
|
||||
uuid
|
||||
city
|
||||
addressLine
|
||||
country
|
||||
region
|
||||
district
|
||||
postalCode
|
||||
street
|
||||
}
|
||||
`;
|
||||
41
storefront/app/graphql/mutations/addresses.ts
Normal file
41
storefront/app/graphql/mutations/addresses.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export const CREATE_ADDRESS = gql`
|
||||
mutation createAddress(
|
||||
$rawData: String!
|
||||
$addressLine1: String!,
|
||||
$addressLine2: String
|
||||
) {
|
||||
createAddress(
|
||||
rawData: $rawData
|
||||
addressLine1: $addressLine1,
|
||||
addressLine2: $addressLine2
|
||||
) {
|
||||
uuid
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_ADDRESS = gql`
|
||||
mutation deleteAddress(
|
||||
$uuid: UUID!
|
||||
) {
|
||||
deleteAddress(
|
||||
uuid: $uuid
|
||||
) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AUTOCOMPLETE_ADDRESS = gql`
|
||||
mutation autocompleteAddress(
|
||||
$limit: Int,
|
||||
$q: String
|
||||
) {
|
||||
autocompleteAddress(
|
||||
limit: $limit,
|
||||
q: $q
|
||||
) {
|
||||
suggestions
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import {GET_ADDRESSES} from "@graphql/queries/standalone/addresses";
|
||||
import { GET_ORDERS } from '@graphql/queries/standalone/orders';
|
||||
import { GET_PROMOCODES } from '@graphql/queries/standalone/promocodes';
|
||||
import { GET_WISHLIST } from '@graphql/queries/standalone/wishlist';
|
||||
import combineQuery from 'graphql-combine-query';
|
||||
|
||||
export const getUserBaseData = (orderVariables?: { userEmail?: string; status?: string }) => {
|
||||
export const getUserBaseData = (orderVariables?: { userEmail?: string; status?: string; }) => {
|
||||
const { document, variables } = combineQuery('getUserBaseData')
|
||||
.add(GET_WISHLIST)
|
||||
.add(GET_PROMOCODES)
|
||||
.add(GET_ORDERS, orderVariables || {});
|
||||
.add(GET_ORDERS, orderVariables || {})
|
||||
.add(GET_ADDRESSES);
|
||||
|
||||
return {
|
||||
document,
|
||||
|
|
|
|||
27
storefront/app/graphql/queries/standalone/addresses.ts
Normal file
27
storefront/app/graphql/queries/standalone/addresses.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { ADDRESS_FRAGMENT } from '@graphql/fragments/address.fragment';
|
||||
|
||||
export const GET_ADDRESSES = gql`
|
||||
query getAddresses(
|
||||
$addressesAfter: String,
|
||||
$addressesFirst: Int,
|
||||
$addressesOrderBy: String,
|
||||
|
||||
) {
|
||||
addresses(
|
||||
after: $addressesAfter,
|
||||
first: $addressesFirst,
|
||||
orderBy: $addressesOrderBy,
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...Address
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
${ADDRESS_FRAGMENT}
|
||||
`;
|
||||
85
storefront/app/pages/profile/addresses.vue
Normal file
85
storefront/app/pages/profile/addresses.vue
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<div class="addresses">
|
||||
<div class="addresses__top">
|
||||
<h1 class="addresses__top-title">{{ t('profile.addresses.title') }}</h1>
|
||||
</div>
|
||||
<div class="addresses__main">
|
||||
<div class="addresses__main-inner">
|
||||
<h6 class="addresses__main-title">{{ t('profile.addresses.title1') }}</h6>
|
||||
<forms-address />
|
||||
</div>
|
||||
<div class="addresses__main-inner">
|
||||
<h6 class="addresses__main-title">{{ t('profile.addresses.title2') }}</h6>
|
||||
<div class="list">
|
||||
<cards-address
|
||||
v-for="address in addresses"
|
||||
:key="address.node.uuid"
|
||||
:address="address.node"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {usePageTitle} from "@composables/utils";
|
||||
import {useAddressDelete} from "@composables/adresses";
|
||||
|
||||
const {t} = useI18n();
|
||||
const addressesStore = useAddressesStore();
|
||||
|
||||
const addresses = computed(() => addressesStore.addresses);
|
||||
|
||||
const { setPageTitle } = usePageTitle();
|
||||
const { deleteAddress, deleteLoading } = useAddressDelete();
|
||||
|
||||
setPageTitle(t('breadcrumbs.addresses'));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.addresses {
|
||||
background-color: $main;
|
||||
width: 100%;
|
||||
border: 1px solid $border;
|
||||
border-radius: $default_border_radius;
|
||||
height: fit-content;
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid $border;
|
||||
|
||||
&-title {
|
||||
color: $primary;
|
||||
font-family: "Playfair Display", sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 75px;
|
||||
padding: 24px 32px;
|
||||
|
||||
&-inner {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
storefront/app/stores/addresses.ts
Normal file
13
storefront/app/stores/addresses.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { IAddress } from '@types';
|
||||
|
||||
export const useAddressesStore = defineStore('addresses', () => {
|
||||
const addresses = ref<{ node: IAddress }[] | null>(null);
|
||||
const setAddresses = (payload: { node: IAddress }[] | null) => {
|
||||
addresses.value = payload;
|
||||
};
|
||||
|
||||
return {
|
||||
addresses,
|
||||
setAddresses,
|
||||
};
|
||||
});
|
||||
|
|
@ -22,7 +22,8 @@
|
|||
"saveChanges": "Save Changes",
|
||||
"clearAll": "Clear All",
|
||||
"buyNow": "Buy Now",
|
||||
"backToHome": "Back To Home"
|
||||
"backToHome": "Back To Home",
|
||||
"add": "Add"
|
||||
},
|
||||
"errors": {
|
||||
"required": "This field is required!",
|
||||
|
|
@ -55,13 +56,24 @@
|
|||
"confirmNewPassword": "Confirm new password",
|
||||
"brandsSearch": "Search brands by name...",
|
||||
"promocode": "Enter promocode",
|
||||
"address": "Start typing the address"
|
||||
"address": "Start typing the address",
|
||||
"addressName": "Name",
|
||||
"country": "Country",
|
||||
"region": "Region",
|
||||
"district": "District",
|
||||
"postalCode": "Postal code",
|
||||
"street": "Street",
|
||||
"city": "City",
|
||||
"entrance": "Entrance",
|
||||
"floor": "Floor",
|
||||
"apartNumber": "Apart. number"
|
||||
},
|
||||
"checkboxes": {
|
||||
"remember": "Remember me",
|
||||
"chooseAll": "Choose all",
|
||||
"agree": "I agree to the {terms} and {policy}",
|
||||
"subscribe": "Subscribe to newsletter for exclusive offers and updates"
|
||||
"subscribe": "Subscribe to newsletter for exclusive offers and updates",
|
||||
"isHouse": "Private house"
|
||||
},
|
||||
"popup": {
|
||||
"errors": {
|
||||
|
|
@ -173,6 +185,9 @@
|
|||
},
|
||||
"newPassword": {
|
||||
"title": "New password"
|
||||
},
|
||||
"address": {
|
||||
"moscow": "Moscow"
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
"saveChanges": "Сохранить изменения",
|
||||
"clearAll": "Очистить всё",
|
||||
"buyNow": "Купить Сейчас",
|
||||
"backToHome": "Обратно на Главную"
|
||||
"backToHome": "Обратно на Главную",
|
||||
"add": "Добавить"
|
||||
},
|
||||
"errors": {
|
||||
"required": "Это поле обязательно!",
|
||||
|
|
@ -55,13 +56,24 @@
|
|||
"confirmNewPassword": "Подтвердите новый пароль",
|
||||
"brandsSearch": "Поиск брендов по названию...",
|
||||
"promocode": "Введите промокод",
|
||||
"address": "Начните вводить адрес"
|
||||
"address": "Начните вводить адрес",
|
||||
"addressName": "Название",
|
||||
"country": "Страна",
|
||||
"region": "Регион",
|
||||
"district": "Регион",
|
||||
"postalCode": "Индекс",
|
||||
"street": "Улица",
|
||||
"city": "Город",
|
||||
"entrance": "Подъезд",
|
||||
"floor": "Этаж",
|
||||
"apartNumber": "Квартира"
|
||||
},
|
||||
"checkboxes": {
|
||||
"remember": "Запомнить меня",
|
||||
"chooseAll": "Выбрать все",
|
||||
"agree": "Я согласен с {terms} и {policy}",
|
||||
"subscribe": "Подписаться на рассылку новостей об эксклюзивных предложениях и обновлениях"
|
||||
"subscribe": "Подписаться на рассылку новостей об эксклюзивных предложениях и обновлениях",
|
||||
"isHouse": "Частный дом"
|
||||
},
|
||||
"popup": {
|
||||
"errors": {
|
||||
|
|
@ -173,6 +185,9 @@
|
|||
},
|
||||
"newPassword": {
|
||||
"title": "Новый пароль"
|
||||
},
|
||||
"address": {
|
||||
"moscow": "Москва"
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
|
|
|
|||
32
storefront/types/api/addresses.ts
Normal file
32
storefront/types/api/addresses.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type {IAddress, IAddressCompleteResult} from '@types';
|
||||
|
||||
export interface IAddressesResponse {
|
||||
addresses: {
|
||||
edges: {
|
||||
cursor: string;
|
||||
node: IAddress;
|
||||
}[];
|
||||
pageInfo: {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAddressCreateResponse {
|
||||
createAddress: {
|
||||
address: IAddress;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAddressDeleteResponse {
|
||||
deleteAddress: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAddressAutocompleteResponse {
|
||||
autocompleteAddress: {
|
||||
suggestions: IAddressCompleteResult[];
|
||||
}
|
||||
}
|
||||
27
storefront/types/app/addresses.ts
Normal file
27
storefront/types/app/addresses.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export interface IAddress {
|
||||
uuid: string,
|
||||
city: string,
|
||||
addressLine: string,
|
||||
country: string,
|
||||
region: string,
|
||||
district: string,
|
||||
postalCode: string,
|
||||
street: string
|
||||
}
|
||||
|
||||
export interface IAddressCompleteResult {
|
||||
display_name: string,
|
||||
lat: string,
|
||||
lon: string,
|
||||
address: {
|
||||
hamlet: string,
|
||||
village: string,
|
||||
municipality: string,
|
||||
county: string,
|
||||
state: string,
|
||||
region: string,
|
||||
postcode: string,
|
||||
country: string,
|
||||
country_code: string
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// APP
|
||||
|
||||
export * from './app/addresses';
|
||||
export * from './app/brand';
|
||||
export * from './app/category';
|
||||
export * from './app/company';
|
||||
|
|
@ -17,6 +18,7 @@ export * from './app/wishlist';
|
|||
|
||||
// API
|
||||
|
||||
export * from './api/addresses';
|
||||
export * from './api/auth';
|
||||
export * from './api/brands';
|
||||
export * from './api/categories';
|
||||
|
|
|
|||
Loading…
Reference in a new issue