This commit is contained in:
lendry
2026-07-10 10:37:22 +03:00
parent caf12e64f7
commit a4b4577c55
13 changed files with 669 additions and 48 deletions

View File

@@ -0,0 +1,174 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { Loader2, LocateFixed, MapPin, Send } from 'lucide-react';
import { AddressMapPicker } from '@/components/addresses/address-map-picker';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { reverseGeocode } from '@/lib/geocoding';
export interface ChatLocationSharePayload {
latitude: number;
longitude: number;
address?: string;
label?: string;
}
export function ChatLocationShareDialog({
open,
sending,
onOpenChange,
onSend,
onLocateError
}: {
open: boolean;
sending?: boolean;
onOpenChange: (open: boolean) => void;
onSend: (payload: ChatLocationSharePayload) => Promise<void>;
onLocateError?: (message: string) => void;
}) {
const [latitude, setLatitude] = useState<number | undefined>();
const [longitude, setLongitude] = useState<number | undefined>();
const [address, setAddress] = useState('');
const [label, setLabel] = useState('');
const [locating, setLocating] = useState(false);
const [resolvingAddress, setResolvingAddress] = useState(false);
const resetState = useCallback(() => {
setLatitude(undefined);
setLongitude(undefined);
setAddress('');
setLabel('');
setLocating(false);
setResolvingAddress(false);
}, []);
useEffect(() => {
if (!open) {
resetState();
}
}, [open, resetState]);
const handlePick = useCallback(async (lat: number, lng: number) => {
setLatitude(lat);
setLongitude(lng);
setResolvingAddress(true);
try {
const parsed = await reverseGeocode(lat, lng);
if (parsed?.fullAddress) {
setAddress(parsed.fullAddress);
}
} finally {
setResolvingAddress(false);
}
}, []);
async function handleUseCurrentLocation() {
if (!navigator.geolocation) return;
setLocating(true);
try {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 12_000,
maximumAge: 30_000
});
});
await handlePick(position.coords.latitude, position.coords.longitude);
} catch {
// ошибка геолокации обрабатывается на уровне вызывающего через toast
throw new Error('Не удалось определить текущее местоположение');
} finally {
setLocating(false);
}
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (latitude == null || longitude == null) return;
await onSend({
latitude,
longitude,
address: address.trim() || undefined,
label: label.trim() || undefined
});
}
const canSend = latitude != null && longitude != null && !sending;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[min(92vw,480px)]">
<DialogHeader>
<div className="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-[#f4f5f8]">
<MapPin className="h-6 w-6 text-[#3390ec]" />
</div>
<DialogTitle className="text-center">Отправить геопозицию</DialogTitle>
<p className="text-center text-sm text-[#6b6f7b]">
Выберите точку на карте или используйте текущее местоположение
</p>
</DialogHeader>
<form onSubmit={(event) => void handleSubmit(event)} className="space-y-3">
<div className="overflow-hidden rounded-2xl border border-[#dce3ec]">
<AddressMapPicker latitude={latitude} longitude={longitude} onPick={(lat, lng) => void handlePick(lat, lng)} />
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
className="rounded-xl"
disabled={locating || sending}
onClick={() => {
void handleUseCurrentLocation().catch((error) => {
onLocateError?.(error instanceof Error ? error.message : 'Не удалось определить текущее местоположение');
});
}}
>
{locating ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <LocateFixed className="mr-2 h-4 w-4" />}
Моё местоположение
</Button>
{resolvingAddress ? (
<span className="flex items-center text-xs text-[#667085]">
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
Определяем адрес...
</span>
) : null}
</div>
<Input
value={label}
onChange={(event) => setLabel(event.target.value)}
placeholder="Подпись (необязательно)"
className="h-11 rounded-xl"
disabled={sending}
/>
<Input
value={address}
onChange={(event) => setAddress(event.target.value)}
placeholder="Адрес"
className="h-11 rounded-xl"
disabled={sending || resolvingAddress}
/>
<Button type="submit" size="lg" className="w-full rounded-[18px]" disabled={!canSend}>
{sending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Отправляем...
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
Отправить
</>
)}
</Button>
</form>
</DialogContent>
</Dialog>
);
}