fix: lỗi hiển thị ở frontend
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation, Search } from 'lucide-react';
|
||||
import { X, MapPin, Loader2, Map as MapIcon, Navigation, Search } from 'lucide-react';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { X, Upload, Image as ImageIcon, Loader2 } from 'lucide-react';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
import { processImageModeration } from '@/hooks/useImageModeration';
|
||||
import { compressImage } from '../utils/image';
|
||||
|
||||
interface AddPhotoModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -39,8 +40,11 @@ export const AddPhotoModal: React.FC<AddPhotoModalProps> = ({ isOpen, onClose, t
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nén ảnh trước
|
||||
const compressedFile = await compressImage(file);
|
||||
|
||||
// 2. Chạy kiểm duyệt hình ảnh
|
||||
const moderationResult = await processImageModeration(file);
|
||||
const moderationResult = await processImageModeration(compressedFile);
|
||||
if (moderationResult.blocked) {
|
||||
notify({ title: 'Ảnh bị từ chối', message: `Ảnh ${file.name} chứa nội dung không phù hợp và bị chặn.`, type: 'error' });
|
||||
continue;
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Comment {
|
||||
userName: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
userId: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface CommentModalProps {
|
||||
@@ -81,7 +81,8 @@ export const CommentModal: React.FC<CommentModalProps> = ({ isOpen, onClose, loc
|
||||
id: newCommentData.id,
|
||||
userName: newCommentData.user?.name || 'Ẩn danh',
|
||||
content: newCommentData.content,
|
||||
createdAt: newCommentData.createdAt
|
||||
createdAt: newCommentData.createdAt,
|
||||
userId: newCommentData.userId || newCommentData.user?.id
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { format, differenceInMinutes, parseISO } from 'date-fns';
|
||||
import { CheckCircle2, Circle, Clock, MapPin, AlertCircle, Zap, Navigation, Edit2, Trash2, Plus, List, X, Calendar as CalendarIcon, AlignLeft, MessageSquare, FileText, Flag } from 'lucide-react';
|
||||
import { useTourStore } from '@/store/useTourStore';
|
||||
@@ -63,7 +63,6 @@ export const ItineraryTimeline = ({
|
||||
// Removed: const { isPublicView } = useTourStore(state => state); // isPublicView is passed as a prop
|
||||
const deleteLeg = useTourStore(state => state.deleteLeg);
|
||||
const initializeLegs = useTourStore(state => state.initializeLegs);
|
||||
const fetchTour = useTourStore(state => state.fetchTour);
|
||||
const deleteLocation = useTourStore(state => state.deleteLocation);
|
||||
|
||||
// Khai báo logic canEdit để sử dụng trong toàn bộ component
|
||||
@@ -80,9 +79,9 @@ export const ItineraryTimeline = ({
|
||||
// Tối ưu hóa: Cập nhật UI ngay lập tức bằng cách can thiệp vào State của Store
|
||||
const handleCommentIncrement = (locationId: string) => {
|
||||
const currentLegs = useTourStore.getState().legs;
|
||||
const updatedLegs = currentLegs.map(leg => ({
|
||||
const updatedLegs = currentLegs.map((leg: any) => ({
|
||||
...leg,
|
||||
locations: leg.locations.map(loc =>
|
||||
locations: leg.locations.map((loc: any) =>
|
||||
loc.id === locationId
|
||||
? { ...loc, _count: { ...loc._count, comments: (loc._count?.comments || 0) + 1 } }
|
||||
: loc
|
||||
@@ -94,9 +93,9 @@ export const ItineraryTimeline = ({
|
||||
|
||||
const handleCommentDecrement = (locationId: string) => {
|
||||
const currentLegs = useTourStore.getState().legs;
|
||||
const updatedLegs = currentLegs.map(leg => ({
|
||||
const updatedLegs = currentLegs.map((leg: any) => ({
|
||||
...leg,
|
||||
locations: leg.locations.map(loc =>
|
||||
locations: leg.locations.map((loc: any) =>
|
||||
loc.id === locationId
|
||||
? { ...loc, _count: { ...loc._count, comments: Math.max(0, (loc._count?.comments || 1) - 1) } }
|
||||
: loc
|
||||
@@ -305,7 +304,7 @@ export const ItineraryTimeline = ({
|
||||
|
||||
<div className="ml-2">
|
||||
{/* Nút thêm nhanh "Điểm xuất phát" cho Chặng 1 nếu chưa có */}
|
||||
{legIdx === 0 && !leg.locations.some(loc => loc.plannedStart && new Date(loc.plannedStart).getTime() === 0) && (
|
||||
{legIdx === 0 && !leg.locations.some((loc: any) => loc.plannedStart && new Date(loc.plannedStart).getTime() === 0) && (
|
||||
<div className="relative flex group mb-6 opacity-80 hover:opacity-100 transition-opacity">
|
||||
<div className="z-10 mt-1.5 mr-4">
|
||||
<div className="w-8 h-8 bg-white rounded-full border-2 border-dashed border-blue-200 flex items-center justify-center text-blue-400">
|
||||
@@ -326,7 +325,7 @@ export const ItineraryTimeline = ({
|
||||
)}
|
||||
|
||||
{/* Nút thêm nhanh "Điểm kết thúc" cho Chặng cuối nếu chưa có */}
|
||||
{legIdx === legs.length - 1 && !legs.some(l => l.locations.some(loc => loc.plannedEnd && new Date(loc.plannedEnd).getTime() === 0)) && (
|
||||
{legIdx === legs.length - 1 && !legs.some((l: any) => l.locations.some((loc: any) => loc.plannedEnd && new Date(loc.plannedEnd).getTime() === 0)) && (
|
||||
<div className="relative flex group mb-6 opacity-80 hover:opacity-100 transition-opacity">
|
||||
<div className="z-10 mt-1.5 mr-4">
|
||||
<div className="w-8 h-8 bg-white rounded-full border-2 border-dashed border-red-200 flex items-center justify-center text-red-400">
|
||||
@@ -346,9 +345,9 @@ export const ItineraryTimeline = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{leg.locations.map((location, idx) => {
|
||||
{leg.locations.map((location: any) => {
|
||||
// Tìm vị trí của điểm này trong toàn bộ hành trình
|
||||
const globalIdx = allLocations.findIndex(loc => loc.id === location.id);
|
||||
const globalIdx = allLocations.findIndex((loc: any) => loc.id === location.id);
|
||||
const prevLocation = globalIdx > 0 ? allLocations[globalIdx - 1] : null;
|
||||
|
||||
const distanceFromPrev = prevLocation
|
||||
@@ -386,9 +385,12 @@ export const ItineraryTimeline = ({
|
||||
</div>
|
||||
|
||||
{/* Card Content */}
|
||||
<div className={`flex-1 bg-white p-4 rounded-xl border transition-all duration-200 ${
|
||||
location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'
|
||||
}`}>
|
||||
<div
|
||||
onClick={() => onNavigate?.(location)}
|
||||
className={`flex-1 bg-white p-4 rounded-xl border transition-all duration-200 cursor-pointer ${
|
||||
location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
{isStartPoint && (
|
||||
@@ -398,7 +400,10 @@ export const ItineraryTimeline = ({
|
||||
<span className="inline-block px-2 py-0.5 bg-red-100 text-red-700 text-[10px] font-bold rounded-md mb-1 uppercase tracking-wider">Điểm kết thúc</span>
|
||||
)}
|
||||
<h3
|
||||
onClick={() => onNavigate?.(location)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNavigate?.(location);
|
||||
}}
|
||||
className={`font-semibold text-lg cursor-pointer hover:text-blue-600 transition-colors select-none ${location.status === 'COMPLETED' ? 'text-gray-500 line-through' : 'text-gray-800'}`}
|
||||
>
|
||||
{location.name}
|
||||
@@ -437,11 +442,14 @@ export const ItineraryTimeline = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-right flex flex-col items-end">
|
||||
<div className="text-right flex flex-col items-end" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex gap-1 mb-2">
|
||||
{onQuickNote && !isPublicView && (
|
||||
<button
|
||||
onClick={() => onQuickNote(location.name)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onQuickNote(location.name);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-amber-50 hover:bg-amber-100 text-amber-600 rounded-lg text-[10px] font-bold transition-all border border-amber-100"
|
||||
title="Ghi chú nhanh"
|
||||
>
|
||||
@@ -449,7 +457,8 @@ export const ItineraryTimeline = ({
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCommentLocationId(location.id);
|
||||
setCommentLocationName(location.name);
|
||||
setIsCommentModalOpen(true);
|
||||
@@ -471,10 +480,22 @@ export const ItineraryTimeline = ({
|
||||
)}
|
||||
{canEdit && ( // Allow editing and deleting of all locations if user has edit permissions
|
||||
<div className="flex gap-1 mt-2">
|
||||
<button onClick={() => onEditLocation?.(location)} className="p-1 text-gray-400 hover:text-blue-600 transition-colors">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditLocation?.(location);
|
||||
}}
|
||||
className="p-1 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
<Edit2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button onClick={() => handleDeleteLocation(location.id)} className="p-1 text-gray-400 hover:text-red-600 transition-colors">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteLocation(location.id);
|
||||
}}
|
||||
className="p-1 text-gray-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -150,7 +150,7 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<div className="relative w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300">
|
||||
<div className="relative w-full max-w-md bg-white rounded-3xl shadow-2xl overflow-hidden max-h-[90vh] overflow-y-auto animate-in zoom-in-95 duration-300">
|
||||
<div className="p-8 sm:p-10">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
@@ -173,12 +173,12 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
|
||||
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold text-gray-700 ml-1">Email</label>
|
||||
<label className="text-sm font-semibold text-gray-700 ml-1">Tài khoản hoặc Email</label>
|
||||
<div className="relative group">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
|
||||
<input
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
type="text"
|
||||
placeholder="admin hoặc email..."
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, CheckCircle, AlertCircle, Info } from 'lucide-react';
|
||||
import { CheckCircle, AlertCircle, Info } from 'lucide-react';
|
||||
|
||||
interface NotificationModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -64,6 +64,7 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||
const [resolvedAddress, setResolvedAddress] = useState<string>('');
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
|
||||
|
||||
useEffect(() => {
|
||||
const lat = photo?.metadata?.lat;
|
||||
@@ -441,11 +442,15 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
<img
|
||||
src={photo.imageUrl}
|
||||
alt="Public Map Upload"
|
||||
className="w-full h-auto max-h-[85vh] object-contain md:h-full md:max-h-none select-none"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className={`w-full h-auto max-h-[85vh] object-contain md:h-full md:max-h-none select-none ${
|
||||
!isLoggedIn ? 'pointer-events-none' : ''
|
||||
}`}
|
||||
draggable={false}
|
||||
/>
|
||||
{/* Shield overlay to prevent Save Image As on right-click / long press for non-owners */}
|
||||
{!isAuthorized && (
|
||||
{(!isAuthorized || !isLoggedIn) && (
|
||||
<div className="absolute inset-0 bg-transparent select-none z-10" />
|
||||
)}
|
||||
</a>
|
||||
@@ -472,7 +477,15 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
isActive ? 'border-emerald-500 scale-110 shadow-lg' : 'border-slate-700 hover:border-slate-500'
|
||||
}`}
|
||||
>
|
||||
<img src={p.imageUrl} alt="Timeline thumbnail" className="w-full h-full object-cover" />
|
||||
<img
|
||||
src={p.imageUrl}
|
||||
alt="Timeline thumbnail"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className={`w-full h-full object-cover ${
|
||||
!isLoggedIn ? 'pointer-events-none' : ''
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute bottom-0 inset-x-0 bg-slate-950/70 text-[8px] text-center font-bold text-slate-300 py-0.5">
|
||||
{new Date(p.capturedAt).toLocaleDateString('vi-VN', { month: '2-digit', day: '2-digit' })}
|
||||
</div>
|
||||
@@ -775,7 +788,11 @@ export const PublicPhotoModal: React.FC<PublicPhotoModalProps> = ({
|
||||
<img
|
||||
src={photo.imageUrl}
|
||||
alt="Fullscreen photo"
|
||||
className="max-w-full max-h-full object-contain select-none animate-in zoom-in-95 duration-200"
|
||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||
className={`max-w-full max-h-full object-contain select-none animate-in zoom-in-95 duration-200 ${
|
||||
!isLoggedIn ? 'pointer-events-none' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, ShieldAlert, MapPin, Loader2, Phone, Mail, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from '../hooks/useTranslation';
|
||||
|
||||
interface ReportBusinessModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
initialLatitude?: number;
|
||||
initialLongitude?: number;
|
||||
}
|
||||
|
||||
export const ReportBusinessModal: React.FC<ReportBusinessModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
initialLatitude,
|
||||
initialLongitude
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [type, setType] = useState('RESTAURANT');
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [address, setAddress] = useState('');
|
||||
const [latitude, setLatitude] = useState(initialLatitude ? String(initialLatitude) : '');
|
||||
const [longitude, setLongitude] = useState(initialLongitude ? String(initialLongitude) : '');
|
||||
const [reason, setReason] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLatitude(initialLatitude ? String(initialLatitude) : '');
|
||||
setLongitude(initialLongitude ? String(initialLongitude) : '');
|
||||
setSuccess(false);
|
||||
setError('');
|
||||
}
|
||||
}, [isOpen, initialLatitude, initialLongitude]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleGetCurrentLocation = () => {
|
||||
if (!navigator.geolocation) {
|
||||
setError('Trình duyệt không hỗ trợ định vị GPS.');
|
||||
return;
|
||||
}
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
setLatitude(String(position.coords.latitude.toFixed(6)));
|
||||
setLongitude(String(position.coords.longitude.toFixed(6)));
|
||||
},
|
||||
() => {
|
||||
setError('Không thể lấy vị trí hiện tại. Vui lòng bật định vị GPS.');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/reports`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
name,
|
||||
phone: phone || null,
|
||||
email: email || null,
|
||||
address: address || null,
|
||||
latitude: latitude ? parseFloat(latitude) : null,
|
||||
longitude: longitude ? parseFloat(longitude) : null,
|
||||
reason,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Gửi báo cáo thất bại.');
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
// Reset form
|
||||
setName('');
|
||||
setPhone('');
|
||||
setEmail('');
|
||||
setAddress('');
|
||||
setLatitude('');
|
||||
setLongitude('');
|
||||
setReason('');
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[3000] flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-300"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="relative w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300 flex flex-col max-h-[90vh]">
|
||||
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-red-50 text-red-500 rounded-xl">
|
||||
<ShieldAlert className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">{t('reportModalTitle')}</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Báo cáo các hành vi không lành mạnh hoặc lừa đảo kinh doanh.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{success ? (
|
||||
<div className="p-10 flex flex-col items-center justify-center text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center shadow-lg animate-bounce">
|
||||
<ShieldAlert className="w-8 h-8" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900">{t('success')}!</h3>
|
||||
<p className="text-sm text-gray-500 max-w-sm">{t('reportSuccess')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4 overflow-y-auto flex-1 text-left">
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-2xl text-xs font-bold flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loại hình */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider">{t('businessType')} *</label>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all text-sm font-bold text-gray-800"
|
||||
>
|
||||
<option value="USER">{t('typeUser')}</option>
|
||||
<option value="RESTAURANT">{t('typeRestaurant')}</option>
|
||||
<option value="HOTEL">{t('typeHotel')}</option>
|
||||
<option value="HOMESTAY">{t('typeHomestay')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Tên */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider">{t('businessName')} *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="VD: Nhà hàng ABC, Homestay X..."
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all text-sm text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/* Số điện thoại */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider flex items-center gap-1">
|
||||
<Phone className="w-3.5 h-3.5 text-gray-400" /> {t('businessPhone')}
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="0987xxxxxx"
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all text-sm text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider flex items-center gap-1">
|
||||
<Mail className="w-3.5 h-3.5 text-gray-400" /> {t('businessEmail')}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="contact@business.com"
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all text-sm text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Địa chỉ */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider flex items-center gap-1">
|
||||
<MapPin className="w-3.5 h-3.5 text-gray-400" /> {t('businessAddress')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
placeholder="VD: 123 Đường Trần Phú, Đà Lạt..."
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all text-sm text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tọa độ địa lý */}
|
||||
<div className="space-y-1.5 bg-blue-50/50 p-4 rounded-2xl border border-blue-100">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-xs font-bold text-blue-700 uppercase tracking-wider flex items-center gap-1.5">
|
||||
📍 Vị trí địa lý (Tùy chọn)
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetCurrentLocation}
|
||||
className="text-xs font-bold text-blue-600 hover:text-blue-700 hover:underline flex items-center gap-1"
|
||||
>
|
||||
Lấy vị trí GPS hiện tại
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase mb-1">Vĩ độ (Latitude)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={latitude}
|
||||
onChange={(e) => setLatitude(e.target.value)}
|
||||
placeholder="11.9404"
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-xs text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-bold text-gray-400 uppercase mb-1">Kinh độ (Longitude)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={longitude}
|
||||
onChange={(e) => setLongitude(e.target.value)}
|
||||
placeholder="108.4382"
|
||||
className="w-full px-3 py-2 bg-white border border-gray-200 rounded-xl outline-none text-xs text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lý do */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider">{t('reportReason')} *</label>
|
||||
<textarea
|
||||
required
|
||||
rows={3}
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
placeholder="Hãy mô tả hành vi không đàng hoàng, lừa đảo hoặc gian dối của cơ sở/người dùng này..."
|
||||
className="w-full px-4 py-3 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all resize-none text-sm text-gray-800"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-4 bg-red-600 hover:bg-red-700 text-white font-bold rounded-2xl shadow-lg shadow-red-100 transition-all flex items-center justify-center gap-2 active:scale-[0.98] disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Đang gửi...' : t('submitReport')}
|
||||
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : <ShieldAlert className="w-5 h-5" />}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user