279 lines
10 KiB
TypeScript
279 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { Card } from '../ui/card';
|
|
import { Badge } from '../ui/badge';
|
|
import { GearItem } from '../../types';
|
|
import {
|
|
ArrowLeft,
|
|
Edit3,
|
|
Trash2,
|
|
Tag,
|
|
ShieldCheck,
|
|
FileText,
|
|
Wrench,
|
|
Download,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Loader2,
|
|
} from 'lucide-react';
|
|
import { useToast } from '../../context/ToastContext';
|
|
import { gearService } from '../../services/gearService';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
interface EquipmentDetailViewProps {
|
|
itemId: string;
|
|
onBack: () => void;
|
|
}
|
|
|
|
export const EquipmentDetailView: React.FC<EquipmentDetailViewProps> = ({
|
|
itemId,
|
|
onBack,
|
|
}) => {
|
|
const { addToast } = useToast();
|
|
const [item, setItem] = useState<GearItem | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeImgIndex, setActiveImgIndex] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const fetchItem = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await gearService.get(itemId);
|
|
setItem(data);
|
|
} catch (e) {
|
|
logger.error('Failed to load equipment details', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
itemId,
|
|
});
|
|
addToast('Failed to load equipment details', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchItem();
|
|
}, [itemId]);
|
|
|
|
if (loading)
|
|
return (
|
|
<div className="flex h-[50vh] items-center justify-center">
|
|
<Loader2 className="w-8 h-8 text-kodo-cyan animate-spin" />
|
|
</div>
|
|
);
|
|
if (!item)
|
|
return (
|
|
<div className="text-center py-20 text-gray-500">Item not found</div>
|
|
);
|
|
|
|
const images =
|
|
item.images && item.images.length > 0 ? item.images : [item.image || ''];
|
|
|
|
const nextImage = () =>
|
|
setActiveImgIndex((prev) => (prev + 1) % images.length);
|
|
const prevImage = () =>
|
|
setActiveImgIndex((prev) => (prev - 1 + images.length) % images.length);
|
|
|
|
return (
|
|
<div className="animate-fadeIn max-w-6xl mx-auto pb-20">
|
|
{/* Nav */}
|
|
<div className="flex justify-between items-center mb-6">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={onBack}
|
|
className="pl-0 text-gray-400 hover:text-white"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Inventory
|
|
</Button>
|
|
<div className="flex gap-2">
|
|
<Button variant="secondary" icon={<Edit3 className="w-4 h-4" />}>
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
className="text-kodo-red hover:bg-kodo-red/10"
|
|
icon={<Trash2 className="w-4 h-4" />}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Left: Photos & Key Info */}
|
|
<div className="space-y-6">
|
|
<div className="relative aspect-video bg-black rounded-xl overflow-hidden border border-kodo-steel group">
|
|
<img
|
|
src={images[activeImgIndex]}
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
{images.length > 1 && (
|
|
<>
|
|
<button
|
|
onClick={prevImage}
|
|
className="absolute left-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<ChevronLeft className="w-6 h-6" />
|
|
</button>
|
|
<button
|
|
onClick={nextImage}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/80 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<ChevronRight className="w-6 h-6" />
|
|
</button>
|
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
|
{images.map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`w-2 h-2 rounded-full ${i === activeImgIndex ? 'bg-kodo-cyan' : 'bg-gray-600'}`}
|
|
></div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<Card variant="default">
|
|
<h3 className="font-bold text-white mb-4 border-b border-kodo-steel pb-2 flex items-center gap-2">
|
|
<Tag className="w-4 h-4 text-kodo-cyan" /> Core Specifications
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
{item.specs ? (
|
|
Object.entries(item.specs).map(([key, val]) => (
|
|
<div key={key}>
|
|
<span className="text-gray-500 block text-xs uppercase">
|
|
{key}
|
|
</span>
|
|
<span className="text-white font-medium">{val}</span>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="text-gray-500">No specs defined.</p>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Right: Details & History */}
|
|
<div className="space-y-6">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<Badge label={item.category} variant="terminal" />
|
|
<span
|
|
className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase ${item.status === 'Active' ? 'bg-kodo-lime/10 text-kodo-lime' : 'bg-gray-700 text-gray-300'}`}
|
|
>
|
|
{item.status}
|
|
</span>
|
|
</div>
|
|
<h1 className="text-4xl font-display font-bold text-white mb-1">
|
|
{item.name}
|
|
</h1>
|
|
<p className="text-xl text-kodo-gold font-mono mb-4">
|
|
{item.brand} {item.model}
|
|
</p>
|
|
|
|
<div className="flex gap-6 text-sm text-gray-400 mb-6 font-mono bg-kodo-ink p-4 rounded-lg border border-kodo-steel/50">
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] uppercase font-bold text-gray-500">
|
|
Serial
|
|
</span>
|
|
<span className="text-white">{item.serialNumber}</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] uppercase font-bold text-gray-500">
|
|
Purchased
|
|
</span>
|
|
<span className="text-white">{item.purchaseDate}</span>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] uppercase font-bold text-gray-500">
|
|
Value
|
|
</span>
|
|
<span className="text-kodo-cyan font-bold">
|
|
${item.purchasePrice}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Card variant="gaming">
|
|
<h3 className="font-bold text-white mb-4 border-b border-gray-700 pb-2 flex items-center gap-2">
|
|
<ShieldCheck className="w-4 h-4 text-kodo-lime" /> Warranty &
|
|
Support
|
|
</h3>
|
|
<div className="flex justify-between items-center mb-4 p-3 bg-kodo-ink rounded border border-kodo-steel/30">
|
|
<div>
|
|
<span className="block text-xs text-gray-500 uppercase">
|
|
Expires
|
|
</span>
|
|
<span className="font-bold text-white">
|
|
{item.warrantyExpire || 'N/A'}
|
|
</span>
|
|
</div>
|
|
<Badge label={item.warrantyType || 'Standard'} variant="cyan" />
|
|
</div>
|
|
{item.supportContact && (
|
|
<div className="text-sm">
|
|
<span className="text-gray-500">Support: </span>
|
|
<a
|
|
href={`mailto:${item.supportContact}`}
|
|
className="text-kodo-cyan hover:underline"
|
|
>
|
|
{item.supportContact}
|
|
</a>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
<Card variant="default">
|
|
<h3 className="font-bold text-white mb-4 border-b border-kodo-steel pb-2 flex items-center gap-2">
|
|
<FileText className="w-4 h-4 text-gray-400" /> Documentation
|
|
</h3>
|
|
<div className="space-y-2">
|
|
{item.documents?.map((doc, i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center justify-between p-2 hover:bg-white/5 rounded cursor-pointer group transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="w-4 h-4 text-kodo-cyan" />
|
|
<span className="text-sm text-gray-300">{doc.name}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-gray-500 hover:text-white"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card variant="default">
|
|
<h3 className="font-bold text-white mb-4 border-b border-kodo-steel pb-2 flex items-center gap-2">
|
|
<Wrench className="w-4 h-4 text-kodo-orange" /> Service History
|
|
</h3>
|
|
<div className="space-y-4 relative">
|
|
<div className="absolute left-2 top-2 bottom-2 w-px bg-kodo-steel"></div>
|
|
{item.maintenanceHistory?.map((log) => (
|
|
<div key={log.id} className="relative pl-6">
|
|
<div className="absolute left-0 top-1.5 w-4 h-4 bg-kodo-graphite border border-kodo-orange rounded-full flex items-center justify-center">
|
|
<div className="w-1.5 h-1.5 bg-kodo-orange rounded-full"></div>
|
|
</div>
|
|
<div className="flex justify-between items-start">
|
|
<span className="font-bold text-white text-sm">
|
|
{log.type}
|
|
</span>
|
|
<span className="text-xs text-gray-500">{log.date}</span>
|
|
</div>
|
|
<p className="text-xs text-gray-400 mt-1">{log.notes}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|