feat: tính năng chia sẻ khẩn cấp
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, MapPin } from 'lucide-react';
|
||||
import { X, MapPin, Search, Loader2 } from 'lucide-react';
|
||||
import { MapContainer, TileLayer, Marker, useMap, useMapEvents } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { useTranslation } from '../hooks/useTranslation';
|
||||
|
||||
// Fix Leaflet default marker icon bug
|
||||
const DefaultIcon = L.icon({
|
||||
@@ -55,10 +56,16 @@ export const CoordinateSelectModal: React.FC<CoordinateSelectModalProps> = ({
|
||||
initialLng,
|
||||
onSelect
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultCenter: [number, number] = [10.7769, 106.7009]; // TP.HCM default
|
||||
const [position, setPosition] = useState<[number, number]>(defaultCenter);
|
||||
const [hasSelected, setHasSelected] = useState(false);
|
||||
|
||||
// Search States
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (typeof initialLat === 'number' && typeof initialLng === 'number' && !isNaN(initialLat) && !isNaN(initialLng)) {
|
||||
@@ -68,6 +75,8 @@ export const CoordinateSelectModal: React.FC<CoordinateSelectModalProps> = ({
|
||||
setPosition(defaultCenter);
|
||||
setHasSelected(false);
|
||||
}
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
}
|
||||
}, [isOpen, initialLat, initialLng]);
|
||||
|
||||
@@ -83,6 +92,31 @@ export const CoordinateSelectModal: React.FC<CoordinateSelectModalProps> = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) return;
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&accept-language=vi&limit=5`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSearchResults(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error during Nominatim search:', e);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectResult = (place: any) => {
|
||||
const lat = parseFloat(place.lat);
|
||||
const lng = parseFloat(place.lon);
|
||||
setPosition([lat, lng]);
|
||||
setHasSelected(true);
|
||||
setSearchResults([]);
|
||||
setSearchQuery(place.display_name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[6000] flex items-center justify-center p-4 animate-in fade-in duration-200">
|
||||
{/* Backdrop */}
|
||||
@@ -92,33 +126,75 @@ export const CoordinateSelectModal: React.FC<CoordinateSelectModalProps> = ({
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative w-full max-w-2xl h-[550px] bg-white rounded-3xl shadow-2xl overflow-hidden flex flex-col border border-gray-100 animate-in zoom-in-95 duration-200">
|
||||
<div className="relative w-full max-w-2xl h-[550px] bg-white dark:bg-slate-900 rounded-3xl shadow-2xl overflow-hidden flex flex-col border border-gray-100 dark:border-slate-800 animate-in zoom-in-95 duration-200">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-gray-100 flex items-center justify-between bg-white shrink-0">
|
||||
<div className="p-4 border-b border-gray-100 dark:border-slate-800/80 flex items-center justify-between bg-white dark:bg-slate-900 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5 text-blue-500" />
|
||||
<div className="text-left">
|
||||
<h3 className="font-extrabold text-sm text-gray-900">Chọn vị trí trên bản đồ</h3>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Click lên bản đồ để chọn tọa độ</p>
|
||||
<h3 className="font-extrabold text-sm text-gray-900 dark:text-white">{t('chooseLocationMap')}</h3>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">{t('clickMapSelectCoords')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-full transition-all text-gray-400 hover:text-gray-600"
|
||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-full transition-all text-gray-400 hover:text-gray-650"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Map Body */}
|
||||
<div className="flex-1 bg-gray-50 relative min-h-[300px]" style={{ zIndex: 10 }}>
|
||||
<div className="flex-1 bg-gray-50 dark:bg-slate-950 relative min-h-[300px]" style={{ zIndex: 10 }}>
|
||||
|
||||
{/* Floating Search Panel */}
|
||||
<div className="absolute top-4 left-4 right-4 sm:right-auto z-[1000] sm:w-80 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md rounded-2xl border border-slate-150 dark:border-slate-800 shadow-xl p-2 flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder={t('searchPlaceholder')}
|
||||
className="w-full bg-slate-50 dark:bg-slate-800 border-0 outline-none rounded-xl pl-8 pr-3 py-2 text-xs text-slate-800 dark:text-slate-100"
|
||||
/>
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 absolute left-2.5" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-bold px-3 py-2 rounded-xl text-xs transition-all active:scale-95 shrink-0 flex items-center gap-1"
|
||||
>
|
||||
{isSearching ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : t('confirm')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<div className="max-h-48 overflow-y-auto divide-y divide-gray-100 dark:divide-slate-800/50 bg-white dark:bg-slate-900 rounded-xl border border-slate-150 dark:border-slate-800 shadow-inner">
|
||||
{searchResults.map((r, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => handleSelectResult(r)}
|
||||
className="w-full text-left px-3 py-2.5 text-[10px] text-gray-700 dark:text-slate-350 hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors truncate block"
|
||||
title={r.display_name}
|
||||
>
|
||||
{r.display_name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MapContainer
|
||||
center={position}
|
||||
zoom={13}
|
||||
attributionControl={false}
|
||||
style={{ width: '100%', height: '100%', zIndex: 1 }}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<MapClickEvents onClick={handleMapClick} />
|
||||
@@ -131,29 +207,29 @@ export const CoordinateSelectModal: React.FC<CoordinateSelectModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-gray-100 flex items-center justify-between bg-white shrink-0">
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="p-4 border-t border-gray-100 dark:border-slate-800/80 flex items-center justify-between bg-white dark:bg-slate-900 shrink-0">
|
||||
<div className="text-xs text-gray-500 dark:text-slate-400">
|
||||
{hasSelected ? (
|
||||
<span className="font-semibold text-gray-700">
|
||||
Tọa độ: {position[0].toFixed(6)}, {position[1].toFixed(6)}
|
||||
<span className="font-semibold text-gray-700 dark:text-slate-200">
|
||||
{t('coordsLabel')}: {position[0].toFixed(6)}, {position[1].toFixed(6)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="italic text-gray-400">Chưa chọn vị trí</span>
|
||||
<span className="italic text-gray-400 dark:text-slate-500">{t('noCoordsSelected')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-200 hover:bg-gray-50 text-gray-700 rounded-xl text-xs font-bold transition-all"
|
||||
className="px-4 py-2 border border-gray-200 dark:border-slate-800 hover:bg-gray-50 dark:hover:bg-slate-800 text-gray-700 dark:text-slate-300 rounded-xl text-xs font-bold transition-all animate-fade-in"
|
||||
>
|
||||
Hủy
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!hasSelected}
|
||||
className="px-5 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-blue-500/10"
|
||||
className="px-5 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-200 disabled:text-gray-400 dark:disabled:bg-slate-800 dark:disabled:text-slate-650 disabled:cursor-not-allowed text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-blue-500/10"
|
||||
>
|
||||
Xác nhận
|
||||
{t('confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user