Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions frontend/src/pages/Shipment/ShipmentDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import MilestoneTimeline from './sections/MilestoneTimeline/MilestoneTimeline';
import { Milestone } from './sections/MilestoneTimeline/types';

const ShipmentDetail: React.FC = () => {
const mockMilestones: Milestone[] = [
{
id: '1',
status: 'Created',
label: 'Shipment Created',
timestamp: 'Oct 20, 2026, 09:00 AM',
location: 'Warehouse A, San Francisco',
isCompleted: true,
},
{
id: '2',
status: 'In Transit',
label: 'Picked Up by Carrier',
timestamp: 'Oct 21, 2026, 02:30 PM',
location: 'San Francisco Distribution Center',
isCompleted: true,
},
{
id: '3',
status: 'At Checkpoint',
label: 'Customs Clearance',
timestamp: 'Oct 22, 2026, 11:15 AM',
location: 'Port of Los Angeles',
isCompleted: false,
isCurrent: true,
},
{
id: '4',
status: 'Delivered',
label: 'Delivered to Destination',
timestamp: 'Estimated: Oct 24, 2026',
location: 'Retail Store, San Diego',
isCompleted: false,
},
];

return (
<div className="min-h-screen bg-background text-text-primary p-4 md:p-8">
<div className="max-w-4xl mx-auto">
<header className="mb-12">
<div className="flex items-center gap-4 mb-4">
<span className="px-3 py-1 bg-primary/10 text-primary border border-primary/20 rounded-full text-xs font-bold uppercase tracking-wider">
In Transit
</span>
<h1 className="text-3xl md:text-4xl font-display font-bold">
Shipment <span className="text-primary">#NVN-2026-X81</span>
</h1>
</div>
<p className="text-text-secondary">
Blockchain-verified tracking for secure global logistics.
</p>
</header>

<main>
<section className="bg-background-card border border-border rounded-3xl p-6 md:p-10 shadow-xl overflow-hidden relative">
<div className="absolute top-0 right-0 p-8 opacity-5">
<Package className="w-32 h-32" />
</div>

<h2 className="text-2xl font-display font-semibold mb-8 border-b border-border pb-4">
Tracking <span className="text-primary">Timeline</span>
</h2>

<MilestoneTimeline milestones={mockMilestones} />
</section>
</main>

<footer className="mt-12 text-center text-text-secondary text-sm">
<p>© 2026 Navin Logistics Platform. All milestones are recorded on the Stellar blockchain.</p>
</footer>
</div>
</div>
);
};

// Simple Package icon for the background decoration
const Package = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m7.5 4.27 9 5.15" /><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" /><path d="m3.3 7 8.7 5 8.7-5" /><path d="M12 22V12" />
</svg>
);

export default ShipmentDetail;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import MilestoneTimeline from './MilestoneTimeline';
import { Milestone } from './types';

describe('MilestoneTimeline', () => {
const mockMilestones: Milestone[] = [
{
id: '1',
status: 'Created',
label: 'Order Created',
timestamp: '2026-02-20 09:15 AM',
location: 'New York, NY',
isCompleted: true,
},
{
id: '2',
status: 'In Transit',
label: 'In Transit',
timestamp: '2026-02-21 10:00 AM',
location: 'Philadelphia, PA',
isCompleted: false,
isCurrent: true,
},
{
id: '3',
status: 'Delivered',
label: 'Delivered',
timestamp: 'Expected: 2026-02-23 05:00 PM',
location: 'Boston, MA',
isCompleted: false,
},
];

it('renders all milestones in desktop view', () => {
render(<MilestoneTimeline milestones={mockMilestones} />);

// Check for labels
expect(screen.getAllByText('Order Created').length).toBeGreaterThan(0);
expect(screen.getAllByText('In Transit').length).toBeGreaterThan(0);
expect(screen.getAllByText('Delivered').length).toBeGreaterThan(0);
});

it('shows timestamps and locations', () => {
render(<MilestoneTimeline milestones={mockMilestones} />);

expect(screen.getAllByText('2026-02-20 09:15 AM').length).toBeGreaterThan(0);
expect(screen.getAllByText('New York, NY').length).toBeGreaterThan(0);
});

it('highlights the current milestone', () => {
const { container } = render(<MilestoneTimeline milestones={mockMilestones} />);

// The current milestone should have a primary color border (part of classes)
const currentMilestone = container.querySelector('.border-primary\\/30');
expect(currentMilestone).toBeInTheDocument();
expect(currentMilestone).toHaveTextContent('In Transit');
});

it('marks completed milestones', () => {
const { container } = render(<MilestoneTimeline milestones={mockMilestones} />);

// Completed milestones use bg-accent-blue for the node
const completedNode = container.querySelector('.bg-accent-blue');
expect(completedNode).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import { CheckCircle2, Circle, Package, Truck, MapPin, Flag } from 'lucide-react';
import { MilestoneTimelineProps } from './types';

const statusIcons: Record<string, React.ReactNode> = {
'Created': <Package className="w-5 h-5" />,
'In Transit': <Truck className="w-5 h-5" />,
'At Checkpoint': <MapPin className="w-5 h-5" />,
'Delivered': <Flag className="w-5 h-5" />,
};

const MilestoneTimeline: React.FC<MilestoneTimelineProps> = ({ milestones }) => {
return (
<div className="w-full py-8">
{/* Desktop View: Vertical Timeline */}
<div className="hidden md:flex flex-col space-y-0 relative before:absolute before:left-[19px] before:top-2 before:bottom-2 before:w-0.5 before:bg-border">
{milestones.map((milestone) => (
<div key={milestone.id} className="relative flex items-start gap-6 pb-10 last:pb-0">
{/* Timeline Node */}
<div className="relative z-10 flex items-center justify-center">
{milestone.isCompleted ? (
<div className="w-10 h-10 rounded-full bg-accent-blue flex items-center justify-center text-white shadow-[0_0_15px_rgba(59,130,246,0.5)]">
<CheckCircle2 className="w-6 h-6" />
</div>
) : milestone.isCurrent ? (
<div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-background shadow-[0_0_15px_rgba(0,217,255,0.5)] animate-pulse">
{statusIcons[milestone.status] || <Circle className="w-6 h-6" />}
</div>
) : (
<div className="w-10 h-10 rounded-full bg-background-card border-2 border-border flex items-center justify-center text-text-secondary">
<Circle className="w-6 h-6" />
</div>
)}
</div>

{/* Content */}
<div className={`flex-1 p-5 rounded-2xl border transition-all duration-300 ${
milestone.isCurrent
? 'bg-background-card border-primary/30 shadow-[0_0_20px_rgba(0,217,255,0.1)]'
: milestone.isCompleted
? 'bg-background-card border-border hover:border-accent-blue/30'
: 'bg-background-secondary/50 border-border/50 opacity-60'
}`}>
<div className="flex justify-between items-start mb-2">
<h3 className={`text-lg font-semibold ${milestone.isCompleted ? 'text-text-primary' : milestone.isCurrent ? 'text-primary' : 'text-text-secondary'}`}>
{milestone.label}
</h3>
<span className="text-sm font-medium text-text-secondary">
{milestone.timestamp}
</span>
</div>

<div className="flex items-center gap-2 text-text-secondary text-sm">
<MapPin className="w-4 h-4" />
<span>{milestone.location || 'Location pending'}</span>
</div>

{milestone.isCurrent && (
<div className="mt-4 flex items-center gap-2">
<span className="flex h-2 w-2 rounded-full bg-primary animate-ping"></span>
<span className="text-xs font-bold uppercase tracking-wider text-primary">Live Updates Enabled</span>
</div>
)}
</div>
</div>
))}
</div>

{/* Mobile View: Horizontal Card List */}
<div className="md:hidden flex flex-col gap-4 overflow-x-auto pb-4">
{milestones.map((milestone) => (
<div
key={milestone.id}
className={`flex items-center gap-4 p-4 rounded-xl border ${
milestone.isCurrent
? 'bg-background-card border-primary/40 shadow-glow-blue'
: 'bg-background-card border-border'
} ${!milestone.isCompleted && !milestone.isCurrent ? 'opacity-60' : ''}`}
>
<div className={`shrink-0 w-12 h-12 rounded-full flex items-center justify-center ${
milestone.isCompleted ? 'bg-accent-blue text-white' : milestone.isCurrent ? 'bg-primary text-background' : 'bg-background-secondary text-text-secondary'
}`}>
{milestone.isCompleted ? <CheckCircle2 className="w-6 h-6" /> : (statusIcons[milestone.status] || <Circle className="w-6 h-6" />)}
</div>

<div className="flex-1 min-w-0">
<h4 className={`font-bold truncate ${milestone.isCurrent ? 'text-primary' : 'text-text-primary'}`}>
{milestone.label}
</h4>
<p className="text-xs text-text-secondary truncate">{milestone.location}</p>
<p className="text-[10px] text-text-secondary mt-1">{milestone.timestamp}</p>
</div>

{milestone.isCurrent && (
<div className="w-2 h-2 rounded-full bg-primary shadow-[0_0_8px_#00D9FF]"></div>
)}
</div>
))}
</div>
</div>
);
};

export default MilestoneTimeline;
15 changes: 15 additions & 0 deletions frontend/src/pages/Shipment/sections/MilestoneTimeline/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type MilestoneStatus = 'Created' | 'In Transit' | 'At Checkpoint' | 'Delivered';

export interface Milestone {
id: string;
status: MilestoneStatus;
label: string;
timestamp: string;
location?: string;
isCompleted: boolean;
isCurrent?: boolean;
}

export interface MilestoneTimelineProps {
milestones: Milestone[];
}
2 changes: 1 addition & 1 deletion frontend/tsconfig.node.tsbuildinfo

Large diffs are not rendered by default.

Loading
Loading