diff --git a/storefront/app/assets/styles/ui/select.scss b/storefront/app/assets/styles/ui/select.scss index 6fb32a07..526c9851 100644 --- a/storefront/app/assets/styles/ui/select.scss +++ b/storefront/app/assets/styles/ui/select.scss @@ -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; } \ No newline at end of file diff --git a/storefront/app/components/cards/address.vue b/storefront/app/components/cards/address.vue new file mode 100644 index 00000000..1386711d --- /dev/null +++ b/storefront/app/components/cards/address.vue @@ -0,0 +1,113 @@ + + + + + \ No newline at end of file diff --git a/storefront/app/components/forms/address.vue b/storefront/app/components/forms/address.vue new file mode 100644 index 00000000..2e21e8f2 --- /dev/null +++ b/storefront/app/components/forms/address.vue @@ -0,0 +1,181 @@ + + + + + \ No newline at end of file diff --git a/storefront/app/components/profile/navigation.vue b/storefront/app/components/profile/navigation.vue index 4e9b65fb..59b39d1e 100644 --- a/storefront/app/components/profile/navigation.vue +++ b/storefront/app/components/profile/navigation.vue @@ -33,6 +33,14 @@ {{ t('profile.promocodes.title') }} + + + {{ t('profile.addresses.title') }} +
- +
(null); + + const { mutate, loading, error } = useMutation(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 + }; +} \ No newline at end of file diff --git a/storefront/app/composables/adresses/useAddressCreate.ts b/storefront/app/composables/adresses/useAddressCreate.ts new file mode 100644 index 00000000..8d685895 --- /dev/null +++ b/storefront/app/composables/adresses/useAddressCreate.ts @@ -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(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 + }; +} \ No newline at end of file diff --git a/storefront/app/composables/adresses/useAddressDelete.ts b/storefront/app/composables/adresses/useAddressDelete.ts new file mode 100644 index 00000000..7bfba4ef --- /dev/null +++ b/storefront/app/composables/adresses/useAddressDelete.ts @@ -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(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 + }; +} \ No newline at end of file diff --git a/storefront/app/composables/adresses/useAddresses.ts b/storefront/app/composables/adresses/useAddresses.ts new file mode 100644 index 00000000..5d822da2 --- /dev/null +++ b/storefront/app/composables/adresses/useAddresses.ts @@ -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(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 {}; +} diff --git a/storefront/app/composables/user/useUserBaseData.ts b/storefront/app/composables/user/useUserBaseData.ts index d1ca7d94..a02b43e6 100644 --- a/storefront/app/composables/user/useUserBaseData.ts +++ b/storefront/app/composables/user/useUserBaseData.ts @@ -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 { diff --git a/storefront/app/graphql/fragments/address.fragment.ts b/storefront/app/graphql/fragments/address.fragment.ts new file mode 100644 index 00000000..20fb38e2 --- /dev/null +++ b/storefront/app/graphql/fragments/address.fragment.ts @@ -0,0 +1,12 @@ +export const ADDRESS_FRAGMENT = gql` + fragment Address on AddressType { + uuid + city + addressLine + country + region + district + postalCode + street + } +`; diff --git a/storefront/app/graphql/mutations/addresses.ts b/storefront/app/graphql/mutations/addresses.ts new file mode 100644 index 00000000..76069c7b --- /dev/null +++ b/storefront/app/graphql/mutations/addresses.ts @@ -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 + } + } +`; diff --git a/storefront/app/graphql/queries/combined/userBaseData.ts b/storefront/app/graphql/queries/combined/userBaseData.ts index 15a0fbf1..12772d7b 100644 --- a/storefront/app/graphql/queries/combined/userBaseData.ts +++ b/storefront/app/graphql/queries/combined/userBaseData.ts @@ -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, diff --git a/storefront/app/graphql/queries/standalone/addresses.ts b/storefront/app/graphql/queries/standalone/addresses.ts new file mode 100644 index 00000000..6f60103f --- /dev/null +++ b/storefront/app/graphql/queries/standalone/addresses.ts @@ -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} +`; diff --git a/storefront/app/pages/profile/addresses.vue b/storefront/app/pages/profile/addresses.vue new file mode 100644 index 00000000..be5782f1 --- /dev/null +++ b/storefront/app/pages/profile/addresses.vue @@ -0,0 +1,85 @@ + + + + + \ No newline at end of file diff --git a/storefront/app/stores/addresses.ts b/storefront/app/stores/addresses.ts new file mode 100644 index 00000000..3e618399 --- /dev/null +++ b/storefront/app/stores/addresses.ts @@ -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, + }; +}); diff --git a/storefront/i18n/locales/en-gb.json b/storefront/i18n/locales/en-gb.json index e99dc933..a7227804 100644 --- a/storefront/i18n/locales/en-gb.json +++ b/storefront/i18n/locales/en-gb.json @@ -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": { diff --git a/storefront/i18n/locales/ru-ru.json b/storefront/i18n/locales/ru-ru.json index ee0d402e..fce0b4a2 100644 --- a/storefront/i18n/locales/ru-ru.json +++ b/storefront/i18n/locales/ru-ru.json @@ -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": { diff --git a/storefront/types/api/addresses.ts b/storefront/types/api/addresses.ts new file mode 100644 index 00000000..5b92853d --- /dev/null +++ b/storefront/types/api/addresses.ts @@ -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[]; + } +} diff --git a/storefront/types/app/addresses.ts b/storefront/types/app/addresses.ts new file mode 100644 index 00000000..ed22c4dc --- /dev/null +++ b/storefront/types/app/addresses.ts @@ -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 + } +} diff --git a/storefront/types/index.ts b/storefront/types/index.ts index 08d2b905..48d18849 100644 --- a/storefront/types/index.ts +++ b/storefront/types/index.ts @@ -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';