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:
Alexandr SaVBaD Waltz 2026-03-09 20:32:36 +03:00
parent cb92b7ddaa
commit 048da0e251
25 changed files with 829 additions and 10 deletions

View file

@ -2,6 +2,26 @@
.el-select__wrapper { .el-select__wrapper {
height: 36px !important; height: 36px !important;
min-height: 36px !important; min-height: 50px !important;
background-color: transparent !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;
}

View 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>

View 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>

View file

@ -33,6 +33,14 @@
<icon name="fluent:ticket-20-filled" size="20" /> <icon name="fluent:ticket-20-filled" size="20" />
{{ t('profile.promocodes.title') }} {{ t('profile.promocodes.title') }}
</nuxt-link-locale> </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>
<div class="nav__logout" @click="logout"> <div class="nav__logout" @click="logout">
<icon name="material-symbols:power-settings-new-outline" size="20" /> <icon name="material-symbols:power-settings-new-outline" size="20" />
@ -90,6 +98,10 @@ const { logout } = useLogout();
color: $primary; color: $primary;
background-color: $main_hover; background-color: $main_hover;
} }
& span {
color: $secondary;
}
} }
&__logout { &__logout {

View file

@ -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>

View file

@ -10,6 +10,7 @@
@input="onInput" @input="onInput"
@keydown="numberOnly ? onlyNumbersKeydown($event) : null" @keydown="numberOnly ? onlyNumbersKeydown($event) : null"
class="block__input" class="block__input"
:disabled="isDisabled"
:inputmode="inputMode || 'text'" :inputmode="inputMode || 'text'"
> >
<button <button
@ -39,6 +40,7 @@ const props = defineProps<{
rules?: Rule[]; rules?: Rule[];
label?: string; label?: string;
numberOnly?: boolean; numberOnly?: boolean;
isDisabled?: boolean;
inputMode?: "text" | "email" | "search" | "tel" | "url" | "none" | "numeric" | "decimal"; inputMode?: "text" | "email" | "search" | "tel" | "url" | "none" | "numeric" | "decimal";
}>(); }>();
@ -132,6 +134,11 @@ function onInput(e: Event) {
&::placeholder { &::placeholder {
color: $disabled_secondary; color: $disabled_secondary;
} }
&:disabled {
cursor: not-allowed;
background-color: $main_hover;
}
} }
&__eyes { &__eyes {

View file

@ -29,7 +29,7 @@
</div> </div>
</form> </form>
<div class="search__results" :class="[{ active: (searchResults && isSearchActive) || loading }]"> <div class="search__results" :class="[{ active: (searchResults && isSearchActive) || loading }]">
<skeletons-header-search v-if="loading" /> <skeletons-ui-search v-if="loading"/>
<div <div
class="search__results-inner" class="search__results-inner"
v-for="(blocks, category) in filteredSearchResults" v-for="(blocks, category) in filteredSearchResults"

View file

@ -0,0 +1,4 @@
export * from './useAddressAutocomplete';
export * from './useAddressCreate';
export * from './useAddressDelete';
export * from './useAddresses';

View file

@ -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
};
}

View 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
};
}

View 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
};
}

View 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 {};
}

View file

@ -8,6 +8,7 @@ export function useUserBaseData() {
const wishlistStore = useWishlistStore(); const wishlistStore = useWishlistStore();
const cartStore = useCartStore(); const cartStore = useCartStore();
const promocodeStore = usePromocodeStore(); const promocodeStore = usePromocodeStore();
const addressesStore = useAddressesStore();
const { syncWishlist } = useWishlistSync(); const { syncWishlist } = useWishlistSync();
const { syncOrder } = useOrderSync(); const { syncOrder } = useOrderSync();
@ -40,6 +41,9 @@ export function useUserBaseData() {
if (data?.promocodes.edges) { if (data?.promocodes.edges) {
promocodeStore.setPromocodes(data.promocodes.edges); promocodeStore.setPromocodes(data.promocodes.edges);
} }
if (data?.addresses.edges) {
addressesStore.setAddresses(data.addresses.edges);
}
} }
return { return {

View file

@ -0,0 +1,12 @@
export const ADDRESS_FRAGMENT = gql`
fragment Address on AddressType {
uuid
city
addressLine
country
region
district
postalCode
street
}
`;

View 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
}
}
`;

View file

@ -1,13 +1,15 @@
import {GET_ADDRESSES} from "@graphql/queries/standalone/addresses";
import { GET_ORDERS } from '@graphql/queries/standalone/orders'; import { GET_ORDERS } from '@graphql/queries/standalone/orders';
import { GET_PROMOCODES } from '@graphql/queries/standalone/promocodes'; import { GET_PROMOCODES } from '@graphql/queries/standalone/promocodes';
import { GET_WISHLIST } from '@graphql/queries/standalone/wishlist'; import { GET_WISHLIST } from '@graphql/queries/standalone/wishlist';
import combineQuery from 'graphql-combine-query'; 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') const { document, variables } = combineQuery('getUserBaseData')
.add(GET_WISHLIST) .add(GET_WISHLIST)
.add(GET_PROMOCODES) .add(GET_PROMOCODES)
.add(GET_ORDERS, orderVariables || {}); .add(GET_ORDERS, orderVariables || {})
.add(GET_ADDRESSES);
return { return {
document, document,

View 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}
`;

View 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>

View 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,
};
});

View file

@ -22,7 +22,8 @@
"saveChanges": "Save Changes", "saveChanges": "Save Changes",
"clearAll": "Clear All", "clearAll": "Clear All",
"buyNow": "Buy Now", "buyNow": "Buy Now",
"backToHome": "Back To Home" "backToHome": "Back To Home",
"add": "Add"
}, },
"errors": { "errors": {
"required": "This field is required!", "required": "This field is required!",
@ -55,13 +56,24 @@
"confirmNewPassword": "Confirm new password", "confirmNewPassword": "Confirm new password",
"brandsSearch": "Search brands by name...", "brandsSearch": "Search brands by name...",
"promocode": "Enter promocode", "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": { "checkboxes": {
"remember": "Remember me", "remember": "Remember me",
"chooseAll": "Choose all", "chooseAll": "Choose all",
"agree": "I agree to the {terms} and {policy}", "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": { "popup": {
"errors": { "errors": {
@ -173,6 +185,9 @@
}, },
"newPassword": { "newPassword": {
"title": "New password" "title": "New password"
},
"address": {
"moscow": "Moscow"
} }
}, },
"cards": { "cards": {

View file

@ -22,7 +22,8 @@
"saveChanges": "Сохранить изменения", "saveChanges": "Сохранить изменения",
"clearAll": "Очистить всё", "clearAll": "Очистить всё",
"buyNow": "Купить Сейчас", "buyNow": "Купить Сейчас",
"backToHome": "Обратно на Главную" "backToHome": "Обратно на Главную",
"add": "Добавить"
}, },
"errors": { "errors": {
"required": "Это поле обязательно!", "required": "Это поле обязательно!",
@ -55,13 +56,24 @@
"confirmNewPassword": "Подтвердите новый пароль", "confirmNewPassword": "Подтвердите новый пароль",
"brandsSearch": "Поиск брендов по названию...", "brandsSearch": "Поиск брендов по названию...",
"promocode": "Введите промокод", "promocode": "Введите промокод",
"address": "Начните вводить адрес" "address": "Начните вводить адрес",
"addressName": "Название",
"country": "Страна",
"region": "Регион",
"district": "Регион",
"postalCode": "Индекс",
"street": "Улица",
"city": "Город",
"entrance": "Подъезд",
"floor": "Этаж",
"apartNumber": "Квартира"
}, },
"checkboxes": { "checkboxes": {
"remember": "Запомнить меня", "remember": "Запомнить меня",
"chooseAll": "Выбрать все", "chooseAll": "Выбрать все",
"agree": "Я согласен с {terms} и {policy}", "agree": "Я согласен с {terms} и {policy}",
"subscribe": "Подписаться на рассылку новостей об эксклюзивных предложениях и обновлениях" "subscribe": "Подписаться на рассылку новостей об эксклюзивных предложениях и обновлениях",
"isHouse": "Частный дом"
}, },
"popup": { "popup": {
"errors": { "errors": {
@ -173,6 +185,9 @@
}, },
"newPassword": { "newPassword": {
"title": "Новый пароль" "title": "Новый пароль"
},
"address": {
"moscow": "Москва"
} }
}, },
"cards": { "cards": {

View 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[];
}
}

View 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
}
}

View file

@ -1,5 +1,6 @@
// APP // APP
export * from './app/addresses';
export * from './app/brand'; export * from './app/brand';
export * from './app/category'; export * from './app/category';
export * from './app/company'; export * from './app/company';
@ -17,6 +18,7 @@ export * from './app/wishlist';
// API // API
export * from './api/addresses';
export * from './api/auth'; export * from './api/auth';
export * from './api/brands'; export * from './api/brands';
export * from './api/categories'; export * from './api/categories';