fix: gửi vị trí khẩn cấp trong Dashboard
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
@@ -3,10 +3,30 @@ server {
|
|||||||
server_name yotrip.labz.io.vn localhost;
|
server_name yotrip.labz.io.vn localhost;
|
||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
# JavaScript and CSS files - immutable caching
|
||||||
|
location ~* \.(?:js|css)$ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
add_header Vary "Accept-Encoding" always;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Images and fonts - long caching
|
||||||
|
location ~* \.(?:jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main app route - SPA fallback (only for HTML)
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
|
# Only redirect actual routes to index.html, not assets
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Proxy API requests to backend
|
# Proxy API requests to backend
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { format, differenceInMinutes, parseISO } from 'date-fns';
|
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 { CheckCircle2, Circle, Clock, MapPin, AlertCircle, Zap, Navigation, Edit2, Trash2, Plus, List, X, Calendar as CalendarIcon, AlignLeft, MessageSquare, FileText, Flag, ChevronDown } from 'lucide-react';
|
||||||
import { useTourStore } from '@/store/useTourStore';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
import { useConfirm } from '@/hooks/useConfirm';
|
import { useConfirm } from '@/hooks/useConfirm';
|
||||||
import { useNotification } from '@/hooks/useNotification';
|
import { useNotification } from '@/hooks/useNotification';
|
||||||
@@ -57,7 +57,6 @@ export const ItineraryTimeline = ({
|
|||||||
const currentTour = useTourStore(state => state.currentTour);
|
const currentTour = useTourStore(state => state.currentTour);
|
||||||
const legs = useTourStore(state => state.legs);
|
const legs = useTourStore(state => state.legs);
|
||||||
const userRole = useTourStore(state => state.userRole);
|
const userRole = useTourStore(state => state.userRole);
|
||||||
const optimizeRouting = useTourStore(state => state.optimizeRouting);
|
|
||||||
const addLeg = useTourStore(state => state.addLeg);
|
const addLeg = useTourStore(state => state.addLeg);
|
||||||
const updateLeg = useTourStore(state => state.updateLeg);
|
const updateLeg = useTourStore(state => state.updateLeg);
|
||||||
// Removed: const { isPublicView } = useTourStore(state => state); // isPublicView is passed as a prop
|
// Removed: const { isPublicView } = useTourStore(state => state); // isPublicView is passed as a prop
|
||||||
@@ -259,32 +258,47 @@ export const ItineraryTimeline = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 ml-4" onClick={(e) => e.stopPropagation()}>
|
{/* Expand/Collapse indicator button */}
|
||||||
{canEdit && (
|
<button
|
||||||
<>
|
onClick={(e) => { e.stopPropagation(); toggleStageExpanded(leg.id); }}
|
||||||
<button
|
className="ml-2 p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all shrink-0"
|
||||||
onClick={(e) => { e.stopPropagation(); onAddLocation?.(leg.id, legIdx === 0 && leg.locations.length === 0); }}
|
title={expandedStageId === leg.id ? 'Collapse chặng này' : 'Expand chặng này'}
|
||||||
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
|
>
|
||||||
title="Thêm địa điểm vào chặng này"
|
<ChevronDown
|
||||||
>
|
className={`w-5 h-5 transition-transform duration-300 ${
|
||||||
<Plus className="w-4 h-4" />
|
expandedStageId === leg.id ? 'rotate-180' : ''
|
||||||
</button>
|
}`}
|
||||||
<button
|
/>
|
||||||
onClick={(e) => { e.stopPropagation(); handleEditLeg(leg); }}
|
</button>
|
||||||
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
|
</div>
|
||||||
>
|
|
||||||
<Edit2 className="w-4 h-4" />
|
{/* Action Buttons Row - Below stage title */}
|
||||||
</button>
|
<div className="px-4 py-2 bg-white border-b border-gray-100 flex items-center gap-2 flex-wrap">
|
||||||
<button
|
{canEdit && (
|
||||||
onClick={(e) => { e.stopPropagation(); handleDeleteLeg(leg.id); }}
|
<>
|
||||||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all"
|
<button
|
||||||
>
|
onClick={(e) => { e.stopPropagation(); onAddLocation?.(leg.id, legIdx === 0 && leg.locations.length === 0); }}
|
||||||
<Trash2 className="w-4 h-4" />
|
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
|
||||||
</button>
|
title="Thêm địa điểm vào chặng này"
|
||||||
</>
|
>
|
||||||
)}
|
<Plus className="w-4 h-4" />
|
||||||
{leg.totalDistance !== undefined && (
|
</button>
|
||||||
<div className="hidden sm:flex items-center gap-2">
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleEditLeg(leg); }}
|
||||||
|
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleDeleteLeg(leg.id); }}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{leg.totalDistance !== undefined && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-xs font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-lg border border-blue-100">
|
<div className="text-xs font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-lg border border-blue-100">
|
||||||
{leg.totalDistance} km
|
{leg.totalDistance} km
|
||||||
</div>
|
</div>
|
||||||
@@ -294,22 +308,12 @@ export const ItineraryTimeline = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{totalDwellMinutes > 0 && ( // Always show dwell time
|
{totalDwellMinutes > 0 && (
|
||||||
<div className="hidden md:flex text-xs font-bold text-amber-600 bg-amber-50 px-2 py-1 rounded-lg border border-amber-100 items-center gap-1">
|
<div className="text-xs font-bold text-amber-600 bg-amber-50 px-2 py-1 rounded-lg border border-amber-100 items-center gap-1 flex">
|
||||||
<Clock className="w-3 h-3" />
|
<Clock className="w-3 h-3" />
|
||||||
Dừng: {formatTravelTime(totalDwellMinutes)}
|
Dừng: {formatTravelTime(totalDwellMinutes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
{['OWNER', 'MANAGER'].includes(userRole || '') && legs.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => optimizeRouting(leg.id)}
|
|
||||||
className="ml-auto flex items-center gap-1.5 text-xs font-bold text-indigo-600 hover:text-indigo-700 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-xl transition-all"
|
|
||||||
>
|
|
||||||
<Zap className="w-3 h-3" />
|
|
||||||
Tối ưu
|
|
||||||
</button>
|
|
||||||
)} {/* Only show optimize button if canEdit */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content body with proper z-index layering */}
|
{/* Scrollable content body with proper z-index layering */}
|
||||||
|
|||||||
@@ -135,9 +135,10 @@
|
|||||||
.folder-node-wrapper {
|
.folder-node-wrapper {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
margin-bottom: 1px !important;
|
margin-bottom: 0px !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual stage card block - backwards compatibility */
|
/* Individual stage card block - backwards compatibility */
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Bell,
|
Bell,
|
||||||
BellOff,
|
BellOff,
|
||||||
ShieldAlert
|
ShieldAlert,
|
||||||
|
Camera
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTourStore } from '@/store/useTourStore';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
import { useNotification } from '@/hooks/useNotification';
|
import { useNotification } from '@/hooks/useNotification';
|
||||||
@@ -128,6 +129,56 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEmergencyShare = async (tour: any) => {
|
||||||
|
try {
|
||||||
|
setLoadingShare(true);
|
||||||
|
// Get current location
|
||||||
|
const position = await new Promise<GeolocationCoordinates>((resolve, reject) => {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => resolve(pos.coords),
|
||||||
|
(err) => reject(err),
|
||||||
|
{ enableHighAccuracy: true, timeout: 5000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send location message to tour chat
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const message = `📍 Vị trí hiện tại: ${position.latitude.toFixed(6)}, ${position.longitude.toFixed(6)}\n🔗 Google Maps: https://maps.google.com/?q=${position.latitude},${position.longitude}`;
|
||||||
|
|
||||||
|
const res = await fetch(`/api/v1/tours/${tour.id}/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content: message })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
notify({
|
||||||
|
title: 'Thành công',
|
||||||
|
message: 'Đã gửi vị trí hiện tại cho nhóm',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: 'Lỗi',
|
||||||
|
message: 'Không thể gửi vị trí',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error sharing location:', e);
|
||||||
|
notify({
|
||||||
|
title: 'Lỗi',
|
||||||
|
message: 'Không thể lấy vị trí hiện tại',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingShare(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleShare = async (isEnabled: boolean) => {
|
const handleToggleShare = async (isEnabled: boolean) => {
|
||||||
if (!sharingTour) return;
|
if (!sharingTour) return;
|
||||||
try {
|
try {
|
||||||
@@ -154,7 +205,81 @@ export const MemberDashboard: React.FC<MemberDashboardProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTakeCameraPhoto = async (tour: any) => {
|
||||||
|
if (cameraInputRef.current) {
|
||||||
|
cameraInputRef.current.click();
|
||||||
|
// Store tour ID for later processing
|
||||||
|
(cameraInputRef.current as any).dataset.tourId = tour.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCameraFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const tourId = (event.target as any).dataset.tourId;
|
||||||
|
// Get current location if available
|
||||||
|
try {
|
||||||
|
setIsLocating(true);
|
||||||
|
const position = await new Promise<GeolocationCoordinates>((resolve, reject) => {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => resolve(pos.coords),
|
||||||
|
(err) => reject(err),
|
||||||
|
{ enableHighAccuracy: true, timeout: 5000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setAttachedLocation({ latitude: position.latitude, longitude: position.longitude });
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not get location:', e);
|
||||||
|
}
|
||||||
|
setIsLocating(false);
|
||||||
|
|
||||||
|
// Upload photo to tour
|
||||||
|
setIsUploading(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('images', file);
|
||||||
|
if (attachedLocation) {
|
||||||
|
formData.append('latitude', attachedLocation.latitude.toString());
|
||||||
|
formData.append('longitude', attachedLocation.longitude.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`/api/v1/tours/${tourId}/photos`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
notify({
|
||||||
|
title: 'Thành công',
|
||||||
|
message: 'Đã upload ảnh vào thư viện tour',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
// Clear input
|
||||||
|
if (cameraInputRef.current) cameraInputRef.current.value = '';
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: 'Lỗi',
|
||||||
|
message: 'Không thể upload ảnh',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Upload error:', e);
|
||||||
|
notify({
|
||||||
|
title: 'Lỗi',
|
||||||
|
message: 'Lỗi upload ảnh',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const cameraInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||||
|
|
||||||
@@ -1300,42 +1425,73 @@ const renderInitialsAvatar = (name: string, sizeClass = 'w-10 h-10 text-sm') =>
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 mt-2">
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
|
{/* Chat button - moved to top */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.setItem('tour_detail_default_tab', 'chat');
|
||||||
|
setUnreadTourChats(prev => prev.filter(id => id !== tour.id));
|
||||||
|
onViewTour(tour.id, 'dashboard');
|
||||||
|
}}
|
||||||
|
className="w-full py-2 px-2 bg-indigo-600/20 hover:bg-indigo-600 text-indigo-300 hover:text-white rounded-xl text-xs font-bold transition-all border border-indigo-500/30 hover:border-indigo-500 flex items-center justify-center gap-1 relative whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
|
<span>Trò chuyện</span>
|
||||||
|
{unreadTourChats.includes(tour.id) && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full animate-ping border border-slate-800" />
|
||||||
|
)}
|
||||||
|
{unreadTourChats.includes(tour.id) && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full border border-slate-800" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Camera + Detail buttons row */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleTakeCameraPhoto(tour)}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="flex-1 py-2 px-2 bg-green-600/20 hover:bg-green-600 text-green-300 hover:text-white rounded-xl text-xs font-bold transition-all border border-green-500/30 hover:border-green-500 flex items-center justify-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Camera className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
<span>Chụp ảnh</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onViewTour(tour.id, 'dashboard')}
|
onClick={() => onViewTour(tour.id, 'dashboard')}
|
||||||
className="flex-1 py-2 px-2 bg-slate-900 hover:bg-slate-800 text-slate-350 hover:text-white rounded-xl text-xs font-bold transition-all border border-slate-800 hover:border-slate-700 flex items-center justify-center gap-1"
|
className="flex-1 py-2 px-2 bg-slate-900 hover:bg-slate-800 text-slate-350 hover:text-white rounded-xl text-xs font-bold transition-all border border-slate-800 hover:border-slate-700 flex items-center justify-center gap-1"
|
||||||
>
|
>
|
||||||
<span>Xem chi tiết</span>
|
<span>Chi tiết hành trình</span>
|
||||||
<ChevronRight className="w-3.5 h-3.5" />
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
localStorage.setItem('tour_detail_default_tab', 'chat');
|
|
||||||
setUnreadTourChats(prev => prev.filter(id => id !== tour.id));
|
|
||||||
onViewTour(tour.id, 'dashboard');
|
|
||||||
}}
|
|
||||||
className="flex-1 py-2 px-2 bg-indigo-600/20 hover:bg-indigo-600 text-indigo-300 hover:text-white rounded-xl text-xs font-bold transition-all border border-indigo-500/30 hover:border-indigo-500 flex items-center justify-center gap-1 relative whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-3.5 h-3.5" />
|
|
||||||
<span>Trò chuyện</span>
|
|
||||||
{unreadTourChats.includes(tour.id) && (
|
|
||||||
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full animate-ping border border-slate-800" />
|
|
||||||
)}
|
|
||||||
{unreadTourChats.includes(tour.id) && (
|
|
||||||
<span className="absolute -top-1 -right-1 w-2.5 h-2.5 bg-rose-600 rounded-full border border-slate-800" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Emergency Share Button - moved to bottom */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleOpenShareModal(tour)}
|
onClick={() => handleEmergencyShare(tour)}
|
||||||
className="w-full py-2 px-3 bg-rose-600/10 hover:bg-rose-600 text-rose-400 hover:text-white rounded-xl text-xs font-bold transition-all border border-rose-500/20 hover:border-rose-500 flex items-center justify-center gap-1.5"
|
disabled={loadingShare}
|
||||||
|
className="w-full py-2 px-3 bg-rose-600/10 hover:bg-rose-600 text-rose-400 hover:text-white rounded-xl text-xs font-bold transition-all border border-rose-500/20 hover:border-rose-500 flex items-center justify-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<ShieldAlert className="w-3.5 h-3.5 text-rose-500" />
|
{loadingShare ? (
|
||||||
<span>Chia sẻ khẩn cấp</span>
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ShieldAlert className="w-3.5 h-3.5 text-rose-500" />
|
||||||
|
)}
|
||||||
|
<span>Chia sẻ vị trí khẩn cấp</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden camera input */}
|
||||||
|
<input
|
||||||
|
ref={cameraInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
capture="environment"
|
||||||
|
onChange={handleCameraFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user