175 lines
5.9 KiB
TypeScript
175 lines
5.9 KiB
TypeScript
'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>
|
||
);
|
||
}
|