Simplify profile settings screens
This commit is contained in:
188
app/pages/profile/addresses/new.vue
Normal file
188
app/pages/profile/addresses/new.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@vue/apollo-composable';
|
||||
import { CreateMyDeliveryAddressDocument } from '~/composables/graphql/generated';
|
||||
|
||||
type AddressSuggestion = {
|
||||
value: string;
|
||||
unrestricted_value?: string;
|
||||
data?: {
|
||||
fias_id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const addressFeedback = ref('');
|
||||
const addressLoading = ref(false);
|
||||
const addressOpen = ref(false);
|
||||
const addressSearch = ref('');
|
||||
const addressSuggestions = ref<AddressSuggestion[]>([]);
|
||||
const addressSearchTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const addressDropdownRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const createAddressMutation = useMutation(CreateMyDeliveryAddressDocument, { throws: 'never' });
|
||||
|
||||
const addressForm = reactive({
|
||||
label: '',
|
||||
address: '',
|
||||
unrestrictedValue: '',
|
||||
fiasId: '',
|
||||
});
|
||||
|
||||
function clearAddressTimer() {
|
||||
if (!addressSearchTimer.value) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(addressSearchTimer.value);
|
||||
addressSearchTimer.value = null;
|
||||
}
|
||||
|
||||
async function fetchAddressSuggestions() {
|
||||
const query = addressSearch.value.trim();
|
||||
if (query.length < 2) {
|
||||
addressSuggestions.value = [];
|
||||
addressOpen.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
addressLoading.value = true;
|
||||
await $fetch<{ suggestions: AddressSuggestion[] }>('/api/dadata/address', {
|
||||
method: 'POST',
|
||||
body: { query },
|
||||
})
|
||||
.then((response) => {
|
||||
addressSuggestions.value = response.suggestions || [];
|
||||
addressOpen.value = addressSuggestions.value.length > 0;
|
||||
})
|
||||
.finally(() => {
|
||||
addressLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleAddressSuggest() {
|
||||
clearAddressTimer();
|
||||
addressSearchTimer.value = setTimeout(() => {
|
||||
void fetchAddressSuggestions();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function applyAddressSuggestion(item: AddressSuggestion) {
|
||||
addressOpen.value = false;
|
||||
addressSearch.value = item.value;
|
||||
addressForm.address = item.value;
|
||||
addressForm.unrestrictedValue = item.unrestricted_value || item.value;
|
||||
addressForm.fiasId = item.data?.fias_id || '';
|
||||
}
|
||||
|
||||
function closeDropdownsFromOutside(event: MouseEvent) {
|
||||
const target = event.target as Node | null;
|
||||
if (addressDropdownRef.value && target && !addressDropdownRef.value.contains(target)) {
|
||||
addressOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function addDeliveryAddress() {
|
||||
addressFeedback.value = '';
|
||||
|
||||
const normalizedAddress = addressForm.address.trim() || addressSearch.value.trim();
|
||||
if (normalizedAddress.length < 5) {
|
||||
addressFeedback.value = 'Введите адрес через подсказки DaData.';
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await createAddressMutation.mutate({
|
||||
input: {
|
||||
label: addressForm.label.trim() ? addressForm.label.trim() : null,
|
||||
address: normalizedAddress,
|
||||
unrestrictedValue: addressForm.unrestrictedValue.trim() ? addressForm.unrestrictedValue.trim() : null,
|
||||
fiasId: addressForm.fiasId.trim() ? addressForm.fiasId.trim() : null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result?.data?.createMyDeliveryAddress) {
|
||||
addressFeedback.value = createAddressMutation.error.value?.message || 'Не удалось добавить адрес.';
|
||||
return;
|
||||
}
|
||||
|
||||
await navigateTo('/profile/addresses?created=1');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeDropdownsFromOutside);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', closeDropdownsFromOutside);
|
||||
clearAddressTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<NuxtLink to="/profile/addresses" class="link link-hover text-sm">← Назад к адресам</NuxtLink>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Новый адрес</h1>
|
||||
<p class="text-sm leading-6 text-[#466653]">
|
||||
Найдите адрес через DaData и сохраните его в профиль.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[28px] bg-white px-5 py-5 shadow-[0_18px_38px_rgba(18,56,36,0.08)] md:px-6 md:py-6">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Название адреса</legend>
|
||||
<input v-model="addressForm.label" type="text" class="input w-full" placeholder="Склад МСК">
|
||||
</fieldset>
|
||||
|
||||
<div ref="addressDropdownRef" class="relative mt-4">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Адрес</legend>
|
||||
<input
|
||||
v-model="addressSearch"
|
||||
type="text"
|
||||
class="input w-full"
|
||||
placeholder="Начните вводить адрес"
|
||||
@input="scheduleAddressSuggest"
|
||||
@focus="addressOpen = addressSuggestions.length > 0"
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<span v-if="addressLoading" class="loading loading-spinner loading-sm absolute right-3 top-1/2 -translate-y-1/2" />
|
||||
|
||||
<div
|
||||
v-if="addressOpen && addressSuggestions.length > 0"
|
||||
class="absolute z-30 mt-2 max-h-72 w-full overflow-auto rounded-[24px] bg-white p-2 shadow-[0_18px_38px_rgba(18,56,36,0.08)]"
|
||||
>
|
||||
<button
|
||||
v-for="item in addressSuggestions"
|
||||
:key="`${item.value}-${item.data?.fias_id || ''}`"
|
||||
type="button"
|
||||
class="w-full rounded-[18px] px-3 py-3 text-left transition hover:bg-[#f6fbf8]"
|
||||
@click="applyAddressSuggestion(item)"
|
||||
>
|
||||
<span class="block text-sm font-semibold text-[#123824]">{{ item.value }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="addressFeedback"
|
||||
class="mt-4 rounded-[20px] border border-[#f1d1c7] bg-[#fff3ef] px-4 py-3 text-sm font-medium text-[#9d4426]"
|
||||
>
|
||||
{{ addressFeedback }}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-col gap-3 md:flex-row">
|
||||
<button
|
||||
class="btn rounded-full border-0 bg-[#123824] px-6 text-white hover:bg-[#0f2f20]"
|
||||
:disabled="createAddressMutation.loading.value"
|
||||
@click="addDeliveryAddress"
|
||||
>
|
||||
{{ createAddressMutation.loading.value ? 'Сохраняем...' : 'Сохранить адрес' }}
|
||||
</button>
|
||||
|
||||
<NuxtLink to="/profile/addresses" class="btn rounded-full border border-[#d7e6dc] bg-white px-6 text-[#123824] hover:border-[#bed6c7] hover:bg-[#f6fbf8]">
|
||||
Отмена
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user