first commit
This commit is contained in:
154
apps/frontend/components/addresses/address-quick-section.tsx
Normal file
154
apps/frontend/components/addresses/address-quick-section.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ChevronRight, Home, MapPin } from 'lucide-react';
|
||||
import { AddressFormDialog } from '@/components/addresses/address-form-dialog';
|
||||
import { ActionTile, SectionTitle } from '@/components/id/action-tile';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { useToast } from '@/components/id/toast-provider';
|
||||
import {
|
||||
ADDRESS_LABELS,
|
||||
formatAddressLine,
|
||||
getOtherAddresses,
|
||||
getPrimaryAddress,
|
||||
type AddressLabel
|
||||
} from '@/lib/address-catalog';
|
||||
import { fetchUserAddresses, getApiErrorMessage, UserAddress } from '@/lib/api';
|
||||
|
||||
function AddressRow({ title, text, onClick }: { title: string; text: string; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex w-full items-center gap-4 border-b border-[#eceef4] px-4 py-4 text-left transition last:border-b-0 hover:bg-[#fafbfd]"
|
||||
>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-white">
|
||||
<MapPin className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium">{title}</div>
|
||||
<div className="truncate text-sm text-[#667085]">{text}</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 shrink-0 text-[#a8adbc]" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function AddressQuickSection({
|
||||
layout,
|
||||
isPinLocked = false,
|
||||
isReady = true
|
||||
}: {
|
||||
layout: 'home' | 'data';
|
||||
isPinLocked?: boolean;
|
||||
isReady?: boolean;
|
||||
}) {
|
||||
const { user, token } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const [addresses, setAddresses] = useState<UserAddress[]>([]);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [activeLabel, setActiveLabel] = useState<AddressLabel>('HOME');
|
||||
const [editingAddress, setEditingAddress] = useState<UserAddress | null>(null);
|
||||
|
||||
const loadAddresses = useCallback(async () => {
|
||||
if (!user || !token || isPinLocked) return;
|
||||
try {
|
||||
const response = await fetchUserAddresses(user.id, token);
|
||||
setAddresses(response.addresses ?? []);
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, 'Не удалось загрузить адреса');
|
||||
if (message) showToast(message);
|
||||
}
|
||||
}, [isPinLocked, showToast, token, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && user && !isPinLocked) void loadAddresses();
|
||||
}, [isPinLocked, isReady, loadAddresses, user]);
|
||||
|
||||
const homeAddress = useMemo(() => getPrimaryAddress(addresses, 'HOME') as UserAddress | null, [addresses]);
|
||||
const workAddress = useMemo(() => getPrimaryAddress(addresses, 'WORK') as UserAddress | null, [addresses]);
|
||||
const otherAddresses = useMemo(() => getOtherAddresses(addresses), [addresses]);
|
||||
|
||||
function openAddress(label: AddressLabel, existing: UserAddress | null = null) {
|
||||
setActiveLabel(label);
|
||||
if (label === 'HOME') setEditingAddress(existing ?? homeAddress);
|
||||
else if (label === 'WORK') setEditingAddress(existing ?? workAddress);
|
||||
else setEditingAddress(existing);
|
||||
setFormOpen(true);
|
||||
}
|
||||
|
||||
const tiles = (
|
||||
<div className={layout === 'home' ? 'grid grid-cols-3 gap-4' : 'mt-4 grid grid-cols-3 gap-3'}>
|
||||
<ActionTile
|
||||
icon={Home}
|
||||
label={ADDRESS_LABELS.HOME.title}
|
||||
description={homeAddress ? formatAddressLine(homeAddress) : layout === 'data' ? 'Не указан' : undefined}
|
||||
className={layout === 'home' ? 'min-h-[76px]' : 'min-h-[76px]'}
|
||||
onClick={() => openAddress('HOME')}
|
||||
/>
|
||||
<ActionTile
|
||||
icon={MapPin}
|
||||
label={ADDRESS_LABELS.WORK.title}
|
||||
description={workAddress ? formatAddressLine(workAddress) : layout === 'data' ? 'Не указан' : undefined}
|
||||
className="min-h-[76px]"
|
||||
onClick={() => openAddress('WORK')}
|
||||
/>
|
||||
<ActionTile
|
||||
icon={MapPin}
|
||||
label={ADDRESS_LABELS.OTHER.shortTitle}
|
||||
description={
|
||||
otherAddresses.length > 0
|
||||
? `${otherAddresses.length} ${otherAddresses.length === 1 ? 'адрес' : otherAddresses.length < 5 ? 'адреса' : 'адресов'}`
|
||||
: layout === 'data'
|
||||
? 'Добавить'
|
||||
: undefined
|
||||
}
|
||||
className="min-h-[76px]"
|
||||
onClick={() => openAddress('OTHER')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{layout === 'home' ? (
|
||||
<>
|
||||
<SectionTitle>Адреса</SectionTitle>
|
||||
{tiles}
|
||||
</>
|
||||
) : (
|
||||
<section className="mt-10">
|
||||
<h2 className="text-2xl font-medium">Адреса</h2>
|
||||
<p className="mt-1 text-sm text-[#667085]">Укажите адреса на карте или введите их вручную.</p>
|
||||
{tiles}
|
||||
<div className="mt-4 overflow-hidden rounded-[24px] bg-[#f4f5f8]">
|
||||
{addresses.length === 0 ? (
|
||||
<p className="px-4 py-6 text-center text-sm text-[#667085]">Адреса пока не добавлены</p>
|
||||
) : (
|
||||
addresses.map((address) => (
|
||||
<AddressRow
|
||||
key={address.id}
|
||||
title={ADDRESS_LABELS[address.label as AddressLabel]?.title ?? address.label}
|
||||
text={formatAddressLine(address)}
|
||||
onClick={() => openAddress(address.label as AddressLabel, address)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{user ? (
|
||||
<AddressFormDialog
|
||||
open={formOpen}
|
||||
onOpenChange={setFormOpen}
|
||||
label={activeLabel}
|
||||
userId={user.id}
|
||||
token={token}
|
||||
existing={editingAddress}
|
||||
onSaved={loadAddresses}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user