All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
520 lines
22 KiB
TypeScript
520 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useTranslations, useLocale } from 'next-intl';
|
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Search,
|
|
Package,
|
|
FileText,
|
|
ClipboardList,
|
|
Lightbulb,
|
|
History,
|
|
X,
|
|
Clock,
|
|
Ship,
|
|
ExternalLink,
|
|
Maximize2,
|
|
Minimize2,
|
|
Globe,
|
|
Anchor,
|
|
} from 'lucide-react';
|
|
|
|
interface SearchHistoryItem {
|
|
id: string;
|
|
trackingNumber: string;
|
|
carrierId: string;
|
|
carrierName: string;
|
|
timestamp: Date;
|
|
}
|
|
|
|
type CarrierDescKey = 'containerOrBl' | 'containerBlOrBooking' | 'containerOnly';
|
|
|
|
const carriers = [
|
|
{ id: 'maersk', name: 'Maersk', color: '#00243D', textColor: 'text-white', trackingUrl: 'https://www.maersk.com/tracking/', placeholder: 'Ex: MSKU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
{ id: 'msc', name: 'MSC', color: '#002B5C', textColor: 'text-white', trackingUrl: 'https://www.msc.com/track-a-shipment?query=', placeholder: 'Ex: MSCU1234567', descKey: 'containerBlOrBooking' as CarrierDescKey },
|
|
{ id: 'cma-cgm', name: 'CMA CGM', color: '#E30613', textColor: 'text-white', trackingUrl: 'https://www.cma-cgm.com/ebusiness/tracking/search?SearchBy=Container&Reference=', placeholder: 'Ex: CMAU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
{ id: 'hapag-lloyd', name: 'Hapag-Lloyd', color: '#FF6600', textColor: 'text-white', trackingUrl: 'https://www.hapag-lloyd.com/en/online-business/track/track-by-container-solution.html?container=', placeholder: 'Ex: HLCU1234567', descKey: 'containerOnly' as CarrierDescKey },
|
|
{ id: 'cosco', name: 'COSCO', color: '#003A70', textColor: 'text-white', trackingUrl: 'https://elines.coscoshipping.com/ebusiness/cargoTracking?trackingNumber=', placeholder: 'Ex: COSU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
{ id: 'one', name: 'ONE', color: '#FF00FF', textColor: 'text-white', trackingUrl: 'https://ecomm.one-line.com/one-ecom/manage-shipment/cargo-tracking?trkNoParam=', placeholder: 'Ex: ONEU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
{ id: 'evergreen', name: 'Evergreen', color: '#006633', textColor: 'text-white', trackingUrl: 'https://www.shipmentlink.com/servlet/TDB1_CargoTracking.do?BL=', placeholder: 'Ex: EGHU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
{ id: 'yangming', name: 'Yang Ming', color: '#FFD700', textColor: 'text-gray-900', trackingUrl: 'https://www.yangming.com/e-service/Track_Trace/track_trace_cargo_tracking.aspx?rdolType=CT&str=', placeholder: 'Ex: YMLU1234567', descKey: 'containerOnly' as CarrierDescKey },
|
|
{ id: 'zim', name: 'ZIM', color: '#1E3A8A', textColor: 'text-white', trackingUrl: 'https://www.zim.com/tools/track-a-shipment?consnumber=', placeholder: 'Ex: ZIMU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
{ id: 'hmm', name: 'HMM', color: '#E65100', textColor: 'text-white', trackingUrl: 'https://www.hmm21.com/cms/business/ebiz/trackTrace/trackTrace/index.jsp?type=1&number=', placeholder: 'Ex: HDMU1234567', descKey: 'containerOrBl' as CarrierDescKey },
|
|
];
|
|
|
|
const HISTORY_KEY = 'xpeditis_track_history';
|
|
|
|
export default function TrackTracePage() {
|
|
const t = useTranslations('dashboard.trackTrace');
|
|
const locale = useLocale();
|
|
const [trackingNumber, setTrackingNumber] = useState('');
|
|
const [selectedCarrier, setSelectedCarrier] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
|
|
const [showMap, setShowMap] = useState(false);
|
|
const [isMapFullscreen, setIsMapFullscreen] = useState(false);
|
|
const [isMapLoading, setIsMapLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const savedHistory = localStorage.getItem(HISTORY_KEY);
|
|
if (savedHistory) {
|
|
try {
|
|
const parsed = JSON.parse(savedHistory);
|
|
setSearchHistory(parsed.map((item: any) => ({
|
|
...item,
|
|
timestamp: new Date(item.timestamp)
|
|
})));
|
|
} catch (e) {
|
|
console.error('Failed to parse search history:', e);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const saveHistory = (history: SearchHistoryItem[]) => {
|
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
|
|
setSearchHistory(history);
|
|
};
|
|
|
|
const handleTrack = () => {
|
|
if (!trackingNumber.trim()) {
|
|
setError(t('errors.noTrackingNumber'));
|
|
return;
|
|
}
|
|
if (!selectedCarrier) {
|
|
setError(t('errors.noCarrier'));
|
|
return;
|
|
}
|
|
|
|
setError('');
|
|
|
|
const carrier = carriers.find(c => c.id === selectedCarrier);
|
|
if (carrier) {
|
|
const newHistoryItem: SearchHistoryItem = {
|
|
id: Date.now().toString(),
|
|
trackingNumber: trackingNumber.trim(),
|
|
carrierId: carrier.id,
|
|
carrierName: carrier.name,
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
const updatedHistory = [newHistoryItem, ...searchHistory.filter(
|
|
h => !(h.trackingNumber === newHistoryItem.trackingNumber && h.carrierId === newHistoryItem.carrierId)
|
|
)].slice(0, 10);
|
|
|
|
saveHistory(updatedHistory);
|
|
|
|
const trackingUrl = carrier.trackingUrl + encodeURIComponent(trackingNumber.trim());
|
|
window.open(trackingUrl, '_blank', 'noopener,noreferrer');
|
|
}
|
|
};
|
|
|
|
const handleHistoryClick = (item: SearchHistoryItem) => {
|
|
setTrackingNumber(item.trackingNumber);
|
|
setSelectedCarrier(item.carrierId);
|
|
};
|
|
|
|
const handleDeleteHistory = (id: string) => {
|
|
const updatedHistory = searchHistory.filter(h => h.id !== id);
|
|
saveHistory(updatedHistory);
|
|
};
|
|
|
|
const handleClearHistory = () => {
|
|
saveHistory([]);
|
|
};
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
handleTrack();
|
|
}
|
|
};
|
|
|
|
const selectedCarrierData = carriers.find(c => c.id === selectedCarrier);
|
|
|
|
const formatTimeAgo = (date: Date) => {
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return t('timeAgo.justNow');
|
|
if (diffMins < 60) return t('timeAgo.minutesAgo', { count: diffMins });
|
|
if (diffHours < 24) return t('timeAgo.hoursAgo', { count: diffHours });
|
|
if (diffDays < 7) return t('timeAgo.daysAgo', { count: diffDays });
|
|
return date.toLocaleDateString(locale);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900">{t('title')}</h1>
|
|
<p className="mt-2 text-gray-600">{t('description')}</p>
|
|
</div>
|
|
|
|
{/* Search Form */}
|
|
<Card className="bg-white shadow-lg border-blue-100">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl flex items-center gap-2">
|
|
<Search className="h-5 w-5 text-blue-600" />
|
|
{t('searchCard.title')}
|
|
</CardTitle>
|
|
<CardDescription>{t('searchCard.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Carrier Selection */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
{t('searchCard.selectCarrier')}
|
|
</label>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
|
|
{carriers.map(carrier => (
|
|
<button
|
|
key={carrier.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setSelectedCarrier(carrier.id);
|
|
setError('');
|
|
}}
|
|
className={`flex flex-col items-center justify-center p-4 rounded-xl border-2 transition-all hover:scale-105 ${
|
|
selectedCarrier === carrier.id
|
|
? 'border-blue-500 shadow-lg ring-2 ring-blue-200'
|
|
: 'border-gray-200 hover:border-gray-300 hover:shadow-md'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-12 h-12 rounded-lg flex items-center justify-center text-sm font-bold mb-2 shadow-sm ${carrier.textColor}`}
|
|
style={{ backgroundColor: carrier.color }}
|
|
>
|
|
{carrier.name.length <= 3 ? carrier.name : carrier.name.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
<span className="text-xs font-semibold text-gray-800 text-center leading-tight">{carrier.name}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tracking Number Input */}
|
|
<div>
|
|
<label htmlFor="tracking-number" className="block text-sm font-medium text-gray-700 mb-2">
|
|
{t('searchCard.trackingNumber')}
|
|
</label>
|
|
<div className="flex gap-3">
|
|
<div className="flex-1">
|
|
<Input
|
|
id="tracking-number"
|
|
type="text"
|
|
value={trackingNumber}
|
|
onChange={e => {
|
|
setTrackingNumber(e.target.value.toUpperCase());
|
|
setError('');
|
|
}}
|
|
onKeyPress={handleKeyPress}
|
|
placeholder={selectedCarrierData?.placeholder || 'Ex: MSKU1234567'}
|
|
className="text-lg font-mono border-gray-300 focus:border-blue-500 h-12"
|
|
/>
|
|
{selectedCarrierData && (
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
{t(`carriers.${selectedCarrierData.descKey}` as any)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Button
|
|
onClick={handleTrack}
|
|
size="lg"
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-8 h-12 font-semibold shadow-md"
|
|
>
|
|
<Search className="mr-2 h-5 w-5" />
|
|
{t('searchCard.searchButton')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Map Toggle */}
|
|
<div className="flex flex-wrap gap-3 pt-2">
|
|
<Button
|
|
variant={showMap ? "default" : "outline"}
|
|
onClick={() => {
|
|
setShowMap(!showMap);
|
|
if (!showMap) setIsMapLoading(true);
|
|
}}
|
|
className={showMap
|
|
? "bg-blue-600 hover:bg-blue-700 text-white"
|
|
: "text-gray-700 border-gray-300 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700"
|
|
}
|
|
>
|
|
<Globe className="mr-2 h-4 w-4" />
|
|
{showMap ? t('searchCard.hideMap') : t('searchCard.showMap')}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
<p className="text-sm text-red-600">{error}</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Vessel Position Map */}
|
|
{showMap && (
|
|
<div className={`${isMapFullscreen ? 'fixed inset-0 z-50 bg-gray-900' : ''}`}>
|
|
<Card className={`bg-white shadow-xl overflow-hidden ${isMapFullscreen ? 'h-full rounded-none' : ''}`}>
|
|
{/* Map Header */}
|
|
<div className={`flex items-center justify-between px-6 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white ${isMapFullscreen ? '' : 'rounded-t-lg'}`}>
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-white/20 rounded-lg">
|
|
<Globe className="h-6 w-6" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold">{t('map.title')}</h3>
|
|
<p className="text-blue-100 text-sm">{t('map.subtitle')}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setIsMapFullscreen(!isMapFullscreen)}
|
|
className="text-white hover:bg-white/20"
|
|
>
|
|
{isMapFullscreen ? (
|
|
<>
|
|
<Minimize2 className="h-4 w-4 mr-2" />
|
|
{t('map.minimize')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Maximize2 className="h-4 w-4 mr-2" />
|
|
{t('map.fullscreen')}
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setShowMap(false);
|
|
setIsMapFullscreen(false);
|
|
}}
|
|
className="text-white hover:bg-white/20"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Map Container */}
|
|
<div className={`relative w-full ${isMapFullscreen ? 'h-[calc(100vh-80px)]' : 'h-[70vh] min-h-[500px] max-h-[800px]'}`}>
|
|
{isMapLoading && (
|
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center z-10">
|
|
<div className="text-center">
|
|
<div className="relative">
|
|
<Ship className="h-16 w-16 text-blue-600 animate-pulse" />
|
|
<div className="absolute -bottom-1 left-1/2 transform -translate-x-1/2">
|
|
<div className="flex gap-1">
|
|
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="mt-4 text-blue-700 font-medium">{t('map.loading')}</p>
|
|
<p className="text-blue-500 text-sm">{t('map.connecting')}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<iframe
|
|
src="https://www.marinetraffic.com/en/ais/embed/zoom:3/centery:25/centerx:0/maptype:4/shownames:true/mmsi:0/shipid:0/fleet:/fleet_id:/vtypes:/showmenu:true/remember:false"
|
|
className="w-full h-full border-0"
|
|
title={t('map.iframeTitle')}
|
|
loading="lazy"
|
|
onLoad={() => setIsMapLoading(false)}
|
|
/>
|
|
|
|
{/* Map Legend Overlay */}
|
|
<div className={`absolute bottom-4 left-4 bg-white/95 backdrop-blur-sm rounded-xl shadow-lg p-4 ${isMapFullscreen ? 'max-w-xs' : 'max-w-[280px]'}`}>
|
|
<h4 className="font-semibold text-gray-800 text-sm mb-3 flex items-center gap-2">
|
|
<Anchor className="h-4 w-4 text-blue-600" />
|
|
{t('map.legend')}
|
|
</h4>
|
|
<div className="space-y-2 text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-green-500" />
|
|
<span className="text-gray-600">{t('map.cargo')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-red-500" />
|
|
<span className="text-gray-600">{t('map.tankers')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
|
<span className="text-gray-600">{t('map.passengers')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
|
<span className="text-gray-600">High Speed</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Stats Overlay */}
|
|
<div className={`absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-xl shadow-lg p-4 ${isMapFullscreen ? '' : 'hidden lg:block'}`}>
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-blue-600">90K+</p>
|
|
<p className="text-gray-500 text-xs">{t('map.activeVessels')}</p>
|
|
</div>
|
|
<div className="w-px h-10 bg-gray-200" />
|
|
<div className="text-center">
|
|
<p className="text-2xl font-bold text-green-600">3,500+</p>
|
|
<p className="text-gray-500 text-xs">{t('map.worldPorts')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Map Footer */}
|
|
<div className="px-6 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
|
|
<p className="text-xs text-gray-500 flex items-center gap-1">
|
|
<ExternalLink className="h-3 w-3" />
|
|
{t('map.dataSource')}
|
|
</p>
|
|
<a
|
|
href="https://www.marinetraffic.com"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1"
|
|
>
|
|
{t('map.openOnSite')}
|
|
<ExternalLink className="h-3 w-3" />
|
|
</a>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Search History */}
|
|
<Card className="bg-white shadow">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<History className="h-5 w-5 text-gray-600" />
|
|
{t('history.title')}
|
|
</CardTitle>
|
|
{searchHistory.length > 0 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClearHistory}
|
|
className="text-gray-500 hover:text-red-600 text-xs"
|
|
>
|
|
{t('history.clearAll')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{searchHistory.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<Clock className="h-10 w-10 mx-auto mb-3 text-gray-300" />
|
|
<p className="text-sm">{t('history.empty')}</p>
|
|
<p className="text-xs text-gray-400 mt-1">{t('history.emptyHint')}</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{searchHistory.map(item => {
|
|
const carrier = carriers.find(c => c.id === item.carrierId);
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:bg-gray-50 group cursor-pointer transition-colors"
|
|
onClick={() => handleHistoryClick(item)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={`w-8 h-8 rounded flex items-center justify-center text-xs font-bold ${carrier?.textColor || 'text-white'}`}
|
|
style={{ backgroundColor: carrier?.color || '#666' }}
|
|
>
|
|
{item.carrierName.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<p className="font-mono text-sm font-medium text-gray-900">{item.trackingNumber}</p>
|
|
<p className="text-xs text-gray-500">{item.carrierName} • {formatTimeAgo(item.timestamp)}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDeleteHistory(item.id);
|
|
}}
|
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-50 rounded transition-opacity"
|
|
>
|
|
<X className="h-4 w-4 text-gray-400 hover:text-red-500" />
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Help Section */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<Card className="bg-white">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<Package className="h-5 w-5 text-blue-600" />
|
|
{t('help.containerNumber.title')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-600">{t('help.containerNumber.description')}</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-white">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<FileText className="h-5 w-5 text-blue-600" />
|
|
{t('help.billOfLading.title')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-600">{t('help.billOfLading.description')}</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="bg-white">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
<ClipboardList className="h-5 w-5 text-blue-600" />
|
|
{t('help.bookingRef.title')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-600">{t('help.bookingRef.description')}</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Info Box */}
|
|
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
|
|
<div className="flex items-start gap-3">
|
|
<Lightbulb className="h-5 w-5 text-blue-600 flex-shrink-0" />
|
|
<div>
|
|
<p className="text-sm font-medium text-blue-800">{t('infoBox.title')}</p>
|
|
<p className="text-sm text-blue-700 mt-1">{t('infoBox.description')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|