fix
This commit is contained in:
174
apps/frontend/components/chat/chat-location-share-dialog.tsx
Normal file
174
apps/frontend/components/chat/chat-location-share-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user