+
+ {/* Additional shipment detail sections would go here */}
+
+ Additional shipment details and tracking information will be displayed here.
+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 (
+
+
+
+
+
+ In Transit
+
+
+ Shipment #NVN-2026-X81
+
+
+
+ Blockchain-verified tracking for secure global logistics.
+
+
+
+
+
+
+
+
+ Tracking Timeline
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ShipmentDetail;
+// Simple Package icon for the background decoration
+const Package = ({ className }: { className?: string }) => (
+
+);
+
+export default ShipmentDetail;
diff --git a/frontend/src/pages/Shipment/sections/MilestoneTimeline/MilestoneTimeline.test.tsx b/frontend/src/pages/Shipment/sections/MilestoneTimeline/MilestoneTimeline.test.tsx
new file mode 100644
index 0000000..fd7e192
--- /dev/null
+++ b/frontend/src/pages/Shipment/sections/MilestoneTimeline/MilestoneTimeline.test.tsx
@@ -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(
);
+
+ // 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(
);
+
+ 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(
);
+
+ // 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(
);
+
+ // Completed milestones use bg-accent-blue for the node
+ const completedNode = container.querySelector('.bg-accent-blue');
+ expect(completedNode).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/pages/Shipment/sections/MilestoneTimeline/MilestoneTimeline.tsx b/frontend/src/pages/Shipment/sections/MilestoneTimeline/MilestoneTimeline.tsx
new file mode 100644
index 0000000..65c2a13
--- /dev/null
+++ b/frontend/src/pages/Shipment/sections/MilestoneTimeline/MilestoneTimeline.tsx
@@ -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
= {
+ 'Created': ,
+ 'In Transit': ,
+ 'At Checkpoint': ,
+ 'Delivered': ,
+};
+
+const MilestoneTimeline: React.FC = ({ milestones }) => {
+ return (
+
+ {/* Desktop View: Vertical Timeline */}
+
+ {milestones.map((milestone) => (
+
+ {/* Timeline Node */}
+
+ {milestone.isCompleted ? (
+
+
+
+ ) : milestone.isCurrent ? (
+
+ {statusIcons[milestone.status] || }
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Content */}
+
+
+
+ {milestone.label}
+
+
+ {milestone.timestamp}
+
+
+
+
+
+ {milestone.location || 'Location pending'}
+
+
+ {milestone.isCurrent && (
+
+
+ Live Updates Enabled
+
+ )}
+
+
+ ))}
+
+
+ {/* Mobile View: Horizontal Card List */}
+
+ {milestones.map((milestone) => (
+
+
+ {milestone.isCompleted ? : (statusIcons[milestone.status] || )}
+
+
+
+
+ {milestone.label}
+
+
{milestone.location}
+
{milestone.timestamp}
+
+
+ {milestone.isCurrent && (
+
+ )}
+
+ ))}
+
+
+ );
+};
+
+export default MilestoneTimeline;
diff --git a/frontend/src/pages/Shipment/sections/MilestoneTimeline/types.ts b/frontend/src/pages/Shipment/sections/MilestoneTimeline/types.ts
new file mode 100644
index 0000000..f5214aa
--- /dev/null
+++ b/frontend/src/pages/Shipment/sections/MilestoneTimeline/types.ts
@@ -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[];
+}
diff --git a/frontend/src/pages/Shipment/sections/ShipmentHeader/ShipmentHeader.css b/frontend/src/pages/Shipment/sections/ShipmentHeader/ShipmentHeader.css
new file mode 100644
index 0000000..23f7747
--- /dev/null
+++ b/frontend/src/pages/Shipment/sections/ShipmentHeader/ShipmentHeader.css
@@ -0,0 +1,242 @@
+.shipment-header {
+ background-color: var(--bg-card);
+ border: 1px solid var(--border-color);
+ border-radius: 16px;
+ padding: 24px;
+ margin-bottom: 24px;
+}
+
+.shipment-header-content {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 32px;
+ align-items: start;
+}
+
+/* Left Section - Shipment ID and Status */
+.shipment-header-left {
+ min-width: 200px;
+}
+
+.shipment-id-section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.shipment-id {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 0;
+ letter-spacing: -0.025em;
+}
+
+/* Center Section - Parties and Dates */
+.shipment-header-center {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ min-width: 0; /* Allow shrinking */
+}
+
+.shipment-parties {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+}
+
+.party-info {
+ flex: 1;
+ min-width: 0; /* Allow text truncation */
+}
+
+.party-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0 0 8px 0;
+}
+
+.party-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0 0 4px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.party-address {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.route-arrow {
+ color: var(--accent-blue);
+ flex-shrink: 0;
+}
+
+.shipment-dates {
+ display: flex;
+ gap: 32px;
+}
+
+.date-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.date-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.date-value {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+/* Right Section - Action Buttons */
+.shipment-header-right {
+ min-width: 200px;
+}
+
+.action-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.action-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+}
+
+.action-btn:hover {
+ transform: translateY(-1px);
+}
+
+.action-btn:active {
+ transform: translateY(0);
+}
+
+.action-btn.primary {
+ background-color: var(--accent-blue);
+ color: white;
+}
+
+.action-btn.primary:hover {
+ background-color: #2563eb;
+}
+
+.action-btn.secondary {
+ background-color: transparent;
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+}
+
+.action-btn.secondary:hover {
+ background-color: var(--bg-card-header);
+ color: var(--text-primary);
+ border-color: var(--accent-blue);
+}
+
+/* Mobile Responsive */
+@media (max-width: 1024px) {
+ .shipment-header-content {
+ grid-template-columns: 1fr;
+ gap: 24px;
+ }
+
+ .shipment-header-left {
+ min-width: auto;
+ }
+
+ .shipment-id-section {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .shipment-parties {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .route-arrow {
+ align-self: center;
+ transform: rotate(90deg);
+ }
+
+ .party-info {
+ text-align: center;
+ }
+
+ .party-name,
+ .party-address {
+ white-space: normal;
+ overflow: visible;
+ text-overflow: unset;
+ }
+
+ .shipment-dates {
+ justify-content: space-between;
+ }
+
+ .shipment-header-right {
+ min-width: auto;
+ }
+
+ .action-buttons {
+ flex-direction: row;
+ gap: 12px;
+ }
+
+ .action-btn {
+ flex: 1;
+ justify-content: center;
+ }
+}
+
+@media (max-width: 640px) {
+ .shipment-header {
+ padding: 16px;
+ }
+
+ .shipment-id {
+ font-size: 24px;
+ }
+
+ .shipment-dates {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ gap: 8px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Shipment/sections/ShipmentHeader/ShipmentHeader.tsx b/frontend/src/pages/Shipment/sections/ShipmentHeader/ShipmentHeader.tsx
new file mode 100644
index 0000000..c8e6bbf
--- /dev/null
+++ b/frontend/src/pages/Shipment/sections/ShipmentHeader/ShipmentHeader.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { Download, Share2, MapPin } from 'lucide-react';
+import { StatusBadge, ShipmentStatus } from '../../../../components/ui/StatusBadge/StatusBadge';
+import './ShipmentHeader.css';
+
+export interface ShipmentHeaderProps {
+ shipmentId: string;
+ status: ShipmentStatus;
+ sender: {
+ name: string;
+ address: string;
+ };
+ receiver: {
+ name: string;
+ address: string;
+ };
+ createdAt: string;
+ expectedDelivery: string;
+ onTrack?: () => void;
+ onDownloadProof?: () => void;
+ onShare?: () => void;
+}
+
+export const ShipmentHeader: React.FC = ({
+ shipmentId,
+ status,
+ sender,
+ receiver,
+ createdAt,
+ expectedDelivery,
+ onTrack = () => console.log('Track clicked'),
+ onDownloadProof = () => console.log('Download Proof clicked'),
+ onShare = () => console.log('Share clicked'),
+}) => {
+ const formatDate = (dateString: string): string => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default ShipmentHeader;
\ No newline at end of file
diff --git a/frontend/src/pages/ShipmentDetail/DeliveryProofUpload/DeliveryProofUpload.tsx b/frontend/src/pages/ShipmentDetail/DeliveryProofUpload/DeliveryProofUpload.tsx
new file mode 100644
index 0000000..9bad768
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/DeliveryProofUpload/DeliveryProofUpload.tsx
@@ -0,0 +1,238 @@
+import React, { useState, useRef } from "react";
+
+const DeliveryProofUpload: React.FC = () => {
+ const [file, setFile] = useState(null);
+ const [previewUrl, setPreviewUrl] = useState(null);
+ const [recipientName, setRecipientName] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+ const [isDragging, setIsDragging] = useState(false);
+ const fileInputRef = useRef(null);
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ };
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ if (e.dataTransfer.files?.[0]) handleFileSelection(e.dataTransfer.files[0]);
+ };
+
+ const handleFileInputChange = (e: React.ChangeEvent) => {
+ if (e.target.files?.[0]) handleFileSelection(e.target.files[0]);
+ };
+
+ const handleFileSelection = (selectedFile: File) => {
+ if (!selectedFile.type.startsWith("image/")) {
+ alert("Please upload an image file.");
+ return;
+ }
+
+ // ADD THIS: Cleanup the previous URL if it exists
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
+ }
+
+ setFile(selectedFile);
+ setPreviewUrl(URL.createObjectURL(selectedFile));
+ };
+
+ const triggerFileInput = () => fileInputRef.current?.click();
+
+ const removeFile = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setFile(null);
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
+ setPreviewUrl(null);
+ }
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!file || !recipientName.trim()) return;
+ setIsSubmitting(true);
+ setTimeout(() => {
+ setIsSubmitting(false);
+ setSubmitted(true);
+ }, 1500);
+ };
+
+ return (
+
+
+ DELIVERY PROOF
+
+
+ {!submitted ? (
+
+ ) : (
+
+ {/* Success banner */}
+
+
+
Delivery proof submitted successfully
+
+
+ {/* Proof details */}
+
+
+ {previewUrl && (
+

+ )}
+
+
+
+ Signed By
+
+
+ {recipientName}
+
+
+ Timestamp
+
+
+ {new Date().toLocaleString()}
+
+
+
+
+ )}
+
+ );
+};
+
+export default DeliveryProofUpload;
diff --git a/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.example.tsx b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.example.tsx
new file mode 100644
index 0000000..c92f4d3
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.example.tsx
@@ -0,0 +1,245 @@
+/**
+ * Example usage of the MilestoneTimeline component
+ * This file demonstrates how to use the expanded milestone timeline in your application
+ */
+
+import MilestoneTimeline, { MilestoneDetail } from './MilestoneTimeline';
+
+// Example 1: Complete shipment journey with blockchain verification
+export const CompleteShipmentExample = () => {
+ const milestones: MilestoneDetail[] = [
+ {
+ id: '1',
+ name: 'Order Confirmed',
+ timestamp: '2026-02-20 09:15 AM EST',
+ location: 'New York Distribution Center, NY',
+ blockchainAddress: 'GABCD1234567890WXYZ1234567890ABCDEF',
+ status: 'completed',
+ notes: 'Order successfully confirmed and payment verified on blockchain. Shipment prepared for pickup.',
+ sensorReadings: {
+ temperature: '22°C',
+ humidity: '45%',
+ pressure: '1013 hPa',
+ },
+ },
+ {
+ id: '2',
+ name: 'Picked Up by Carrier',
+ timestamp: '2026-02-20 02:30 PM EST',
+ location: 'New York Distribution Center, NY',
+ blockchainAddress: 'GEFGH2345678901YZAB2345678901BCDEFG',
+ status: 'completed',
+ notes: 'Package picked up by carrier. Driver ID verified and logged on-chain.',
+ sensorReadings: {
+ temperature: '21°C',
+ humidity: '48%',
+ pressure: '1012 hPa',
+ },
+ },
+ {
+ id: '3',
+ name: 'In Transit - Philadelphia Hub',
+ timestamp: '2026-02-21 08:45 AM EST',
+ location: 'Philadelphia Logistics Hub, PA',
+ blockchainAddress: 'GIJKL3456789012ZABC3456789012CDEFGH',
+ status: 'completed',
+ notes: 'Shipment arrived at Philadelphia hub. Passed quality inspection.',
+ sensorReadings: {
+ temperature: '20°C',
+ humidity: '50%',
+ pressure: '1014 hPa',
+ },
+ },
+ {
+ id: '4',
+ name: 'Out for Delivery',
+ timestamp: '2026-02-23 09:00 AM EST',
+ location: 'Boston, MA',
+ blockchainAddress: 'GMNOP4567890123ABCD4567890123DEFGHI',
+ status: 'current',
+ notes: 'Package is currently out for delivery. Driver en route to destination.',
+ sensorReadings: {
+ temperature: '18°C',
+ humidity: '55%',
+ pressure: '1016 hPa',
+ },
+ },
+ {
+ id: '5',
+ name: 'Delivered',
+ timestamp: 'Expected: 2026-02-23 05:00 PM EST',
+ location: 'Boston, MA',
+ blockchainAddress: 'GQRST5678901234BCDE5678901234EFGHIJ',
+ status: 'upcoming',
+ notes: 'Estimated delivery time. Signature will be required upon delivery.',
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
+
+// Example 2: International shipment with customs clearance
+export const InternationalShipmentExample = () => {
+ const milestones: MilestoneDetail[] = [
+ {
+ id: '1',
+ name: 'Order Placed',
+ timestamp: '2026-02-15 10:00 AM CST',
+ location: 'Shanghai, China',
+ blockchainAddress: 'GUVWX6789012345CDEF6789012345FGHIJK',
+ status: 'completed',
+ notes: 'International order confirmed. Export documentation prepared.',
+ sensorReadings: {
+ temperature: '24°C',
+ humidity: '60%',
+ },
+ },
+ {
+ id: '2',
+ name: 'Departed Origin Port',
+ timestamp: '2026-02-16 02:00 PM CST',
+ location: 'Shanghai Port, China',
+ blockchainAddress: 'GYZAB7890123456DEFG7890123456GHIJKL',
+ status: 'completed',
+ notes: 'Container loaded onto vessel. Sea freight journey initiated.',
+ sensorReadings: {
+ temperature: '23°C',
+ humidity: '65%',
+ },
+ },
+ {
+ id: '3',
+ name: 'In Transit - Pacific Ocean',
+ timestamp: '2026-02-18 08:00 AM PST',
+ location: 'Pacific Ocean',
+ blockchainAddress: 'GCDEF8901234567EFGH8901234567HIJKLM',
+ status: 'completed',
+ notes: 'Vessel on schedule. All cargo secure and monitored.',
+ sensorReadings: {
+ temperature: '22°C',
+ humidity: '70%',
+ },
+ },
+ {
+ id: '4',
+ name: 'Arrived at Destination Port',
+ timestamp: '2026-02-22 01:00 PM PST',
+ location: 'Los Angeles Port, CA',
+ blockchainAddress: 'GHIJK9012345678FGHI9012345678IJKLMN',
+ status: 'completed',
+ notes: 'Container offloaded. Awaiting customs clearance.',
+ },
+ {
+ id: '5',
+ name: 'Customs Clearance',
+ timestamp: '2026-02-23 10:30 AM PST',
+ location: 'Los Angeles Customs, CA',
+ blockchainAddress: 'GLMNO0123456789GHIJ0123456789JKLMNO',
+ status: 'current',
+ notes: 'Customs inspection in progress. Documentation under review.',
+ },
+ {
+ id: '6',
+ name: 'Out for Delivery',
+ timestamp: 'Expected: 2026-02-24 09:00 AM PST',
+ location: 'Los Angeles, CA',
+ blockchainAddress: 'GOPQR1234567890HIJK1234567890KLMNOP',
+ status: 'upcoming',
+ },
+ {
+ id: '7',
+ name: 'Delivered',
+ timestamp: 'Expected: 2026-02-24 05:00 PM PST',
+ location: 'Los Angeles, CA',
+ blockchainAddress: 'GRSTU2345678901IJKL2345678901LMNOPQ',
+ status: 'upcoming',
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
+
+// Example 3: Cold chain shipment with detailed sensor monitoring
+export const ColdChainExample = () => {
+ const milestones: MilestoneDetail[] = [
+ {
+ id: '1',
+ name: 'Pharmaceutical Order Confirmed',
+ timestamp: '2026-02-22 08:00 AM EST',
+ location: 'Boston Medical Supply, MA',
+ blockchainAddress: 'GVWXY3456789012JKLM3456789012MNOPQR',
+ status: 'completed',
+ notes: 'Temperature-controlled shipment initiated. Cold chain protocol activated.',
+ sensorReadings: {
+ temperature: '4°C',
+ humidity: '35%',
+ pressure: '1015 hPa',
+ },
+ },
+ {
+ id: '2',
+ name: 'Loaded into Refrigerated Truck',
+ timestamp: '2026-02-22 10:30 AM EST',
+ location: 'Boston Medical Supply, MA',
+ blockchainAddress: 'GYZAB4567890123KLMN4567890123NOPQRS',
+ status: 'completed',
+ notes: 'Package secured in temperature-controlled compartment. Continuous monitoring active.',
+ sensorReadings: {
+ temperature: '3°C',
+ humidity: '38%',
+ pressure: '1014 hPa',
+ },
+ },
+ {
+ id: '3',
+ name: 'Temperature Alert - Resolved',
+ timestamp: '2026-02-22 02:15 PM EST',
+ location: 'En route to Hartford, CT',
+ blockchainAddress: 'GBCDE5678901234LMNO5678901234OPQRST',
+ status: 'completed',
+ notes: 'Brief temperature fluctuation detected (5.2°C). Cooling system adjusted. Temperature restored to safe range within 3 minutes.',
+ sensorReadings: {
+ temperature: '3.5°C',
+ humidity: '40%',
+ pressure: '1013 hPa',
+ },
+ },
+ {
+ id: '4',
+ name: 'Arrived at Distribution Center',
+ timestamp: '2026-02-22 06:00 PM EST',
+ location: 'Hartford Distribution Center, CT',
+ blockchainAddress: 'GEFGH6789012345MNOP6789012345PQRSTU',
+ status: 'current',
+ notes: 'Package transferred to facility cold storage. Quality check in progress.',
+ sensorReadings: {
+ temperature: '4°C',
+ humidity: '36%',
+ pressure: '1015 hPa',
+ },
+ },
+ {
+ id: '5',
+ name: 'Final Delivery',
+ timestamp: 'Expected: 2026-02-23 09:00 AM EST',
+ location: 'Hartford Hospital, CT',
+ blockchainAddress: 'GHIJK7890123456NOPQ7890123456QRSTUV',
+ status: 'upcoming',
+ notes: 'Scheduled delivery to hospital pharmacy. Cold chain integrity maintained throughout journey.',
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.test.tsx b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.test.tsx
new file mode 100644
index 0000000..c5f7587
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.test.tsx
@@ -0,0 +1,136 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import MilestoneTimeline, { MilestoneDetail } from './MilestoneTimeline';
+
+describe('MilestoneTimeline', () => {
+ const mockMilestones: MilestoneDetail[] = [
+ {
+ id: '1',
+ name: 'Order Confirmed',
+ timestamp: '2026-02-20 09:15 AM',
+ location: 'New York, NY',
+ blockchainAddress: 'GABCD1234567890WXYZ',
+ status: 'completed',
+ notes: 'Order confirmed',
+ sensorReadings: {
+ temperature: '22°C',
+ humidity: '45%',
+ },
+ },
+ {
+ id: '2',
+ name: 'In Transit',
+ timestamp: '2026-02-21 10:00 AM',
+ location: 'Philadelphia, PA',
+ blockchainAddress: 'GEFGH2345678901YZAB',
+ status: 'current',
+ },
+ {
+ id: '3',
+ name: 'Delivered',
+ timestamp: 'Expected: 2026-02-23 05:00 PM',
+ location: 'Boston, MA',
+ blockchainAddress: 'GIJKL3456789012ZABC',
+ status: 'upcoming',
+ },
+ ];
+
+ it('renders all milestones', () => {
+ render();
+
+ expect(screen.getByText('Order Confirmed')).toBeInTheDocument();
+ expect(screen.getByText('In Transit')).toBeInTheDocument();
+ expect(screen.getByText('Delivered')).toBeInTheDocument();
+ });
+
+ it('displays truncated blockchain addresses', () => {
+ render();
+
+ expect(screen.getByText('GABCD...WXYZ')).toBeInTheDocument();
+ expect(screen.getByText('GEFGH...YZAB')).toBeInTheDocument();
+ });
+
+ it('shows timestamps and locations', () => {
+ render();
+
+ expect(screen.getByText('2026-02-20 09:15 AM')).toBeInTheDocument();
+ expect(screen.getByText('New York, NY')).toBeInTheDocument();
+ });
+
+ it('expands milestone details when expand button is clicked', () => {
+ render();
+
+ // Notes should not be visible initially
+ expect(screen.queryByText('Order confirmed')).not.toBeInTheDocument();
+
+ // Find and click the expand button for the first milestone
+ const expandButtons = screen.getAllByLabelText('Expand details');
+ fireEvent.click(expandButtons[0]);
+
+ // Notes should now be visible
+ expect(screen.getByText('Order confirmed')).toBeInTheDocument();
+ });
+
+ it('displays sensor readings when expanded', () => {
+ render();
+
+ // Expand the first milestone
+ const expandButtons = screen.getAllByLabelText('Expand details');
+ fireEvent.click(expandButtons[0]);
+
+ // Check for sensor readings
+ expect(screen.getByText('22°C')).toBeInTheDocument();
+ expect(screen.getByText('45%')).toBeInTheDocument();
+ });
+
+ it('collapses milestone details when expand button is clicked again', () => {
+ render();
+
+ const expandButtons = screen.getAllByLabelText('Expand details');
+
+ // Expand
+ fireEvent.click(expandButtons[0]);
+ expect(screen.getByText('Order confirmed')).toBeInTheDocument();
+
+ // Collapse
+ const collapseButton = screen.getByLabelText('Collapse details');
+ fireEvent.click(collapseButton);
+ expect(screen.queryByText('Order confirmed')).not.toBeInTheDocument();
+ });
+
+ it('does not show expand button for milestones without additional details', () => {
+ render();
+
+ // The second milestone has no notes or sensor readings
+ const expandButtons = screen.getAllByLabelText(/Expand details|Collapse details/);
+
+ // Only the first milestone should have an expand button
+ expect(expandButtons.length).toBe(1);
+ });
+
+ it('renders status icons with correct aria-labels', () => {
+ render();
+
+ expect(screen.getByLabelText('Completed')).toBeInTheDocument();
+ expect(screen.getByLabelText('Current')).toBeInTheDocument();
+ expect(screen.getByLabelText('Upcoming')).toBeInTheDocument();
+ });
+
+ it('renders connectors between milestones', () => {
+ const { container } = render();
+
+ // Connectors are aria-hidden divs (not SVGs) between milestone items
+ const connectors = container.querySelectorAll('div[aria-hidden="true"]');
+
+ // Should have n-1 connectors for n milestones
+ expect(connectors.length).toBe(mockMilestones.length - 1);
+ });
+
+ it('handles empty milestones array', () => {
+ render();
+
+ const timeline = screen.getByRole('list');
+ expect(timeline).toBeInTheDocument();
+ expect(timeline.children.length).toBe(0);
+ });
+});
diff --git a/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.tsx b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.tsx
new file mode 100644
index 0000000..759beb0
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/MilestoneTimeline.tsx
@@ -0,0 +1,211 @@
+import React, { useState } from 'react';
+
+export interface MilestoneDetail {
+ id: string;
+ name: string;
+ timestamp: string;
+ location: string;
+ blockchainAddress: string;
+ status: 'completed' | 'current' | 'upcoming';
+ notes?: string;
+ sensorReadings?: {
+ temperature?: string;
+ humidity?: string;
+ pressure?: string;
+ [key: string]: string | undefined;
+ };
+}
+
+export interface MilestoneTimelineProps {
+ milestones: MilestoneDetail[];
+}
+
+const MilestoneTimeline: React.FC = ({ milestones }) => {
+ const [expandedMilestones, setExpandedMilestones] = useState>(new Set());
+
+ const toggleExpanded = (id: string) => {
+ setExpandedMilestones(prev => {
+ const next = new Set(prev);
+ next.has(id) ? next.delete(id) : next.add(id);
+ return next;
+ });
+ };
+
+ const truncateAddress = (address: string) =>
+ address.length <= 12 ? address : `${address.slice(0, 5)}...${address.slice(-4)}`;
+
+ const getStatusIcon = (status: MilestoneDetail['status']) => {
+ const base = 'w-8 h-8 shrink-0 z-10';
+ switch (status) {
+ case 'completed':
+ return (
+
+ );
+ case 'current':
+ return (
+
+ );
+ case 'upcoming':
+ return (
+
+ );
+ }
+ };
+
+ const hasExpandableContent = (m: MilestoneDetail) => !!(m.notes || m.sensorReadings);
+ //build
+ return (
+
+ {milestones.map((milestone, index) => {
+ const isExpanded = expandedMilestones.has(milestone.id);
+ const canExpand = hasExpandableContent(milestone);
+ const isUpcoming = milestone.status === 'upcoming';
+ const isCurrent = milestone.status === 'current';
+
+ return (
+
+ {/* Marker */}
+
+ {getStatusIcon(milestone.status)}
+ {index < milestones.length - 1 && (
+
+ )}
+
+
+ {/* Content wrapper */}
+
+
+ {/* Header row */}
+
+
+ {milestone.name}
+
+ {canExpand && (
+
+ )}
+
+
+ {/* Info rows */}
+
+ {[
+ {
+ icon: (
+
+ ),
+ text: milestone.timestamp,
+ },
+ {
+ icon: (
+
+ ),
+ text: milestone.location,
+ },
+ ].map(({ icon, text }, i) => (
+
+ {icon}
+ {text}
+
+ ))}
+
+ {/* Blockchain address */}
+
+
+
+
+ Blockchain:
+
+ {truncateAddress(milestone.blockchainAddress)}
+
+
+
+
+ {/* Expandable details */}
+ {canExpand && isExpanded && (
+
+ {milestone.notes && (
+
+
Notes
+
{milestone.notes}
+
+ )}
+ {milestone.sensorReadings && (
+
+
Sensor Readings
+
+ {Object.entries(milestone.sensorReadings).map(([key, value]) =>
+ value ? (
+
+
+ {key.charAt(0).toUpperCase() + key.slice(1)}:
+
+ {value}
+
+ ) : null
+ )}
+
+
+ )}
+
+ )}
+
+
+
+ );
+ })}
+
+ );
+};
+
+export default MilestoneTimeline;
diff --git a/frontend/src/pages/ShipmentDetail/MilestoneTimeline/README.md b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/README.md
new file mode 100644
index 0000000..0d771b0
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/MilestoneTimeline/README.md
@@ -0,0 +1,151 @@
+# MilestoneTimeline Component
+
+An expanded, detailed timeline component for displaying shipment milestones with blockchain verification, sensor readings, and expandable details.
+
+## Features
+
+- **Blockchain Verification**: Each milestone displays a truncated blockchain address (e.g., GABCD...WXYZ)
+- **Visual Status States**: Completed, current, and upcoming milestones with distinct styling
+- **Expandable Details**: Click to reveal additional notes and sensor readings
+- **Sensor Monitoring**: Display temperature, humidity, pressure, and custom sensor data
+- **Responsive Design**: Optimized for mobile, tablet, and desktop views
+- **Accessibility**: Full keyboard navigation and ARIA labels
+
+## Usage
+
+```tsx
+import MilestoneTimeline, { MilestoneDetail } from './MilestoneTimeline/MilestoneTimeline';
+
+const milestones: MilestoneDetail[] = [
+ {
+ id: '1',
+ name: 'Order Confirmed',
+ timestamp: '2026-02-20 09:15 AM EST',
+ location: 'New York, NY',
+ blockchainAddress: 'GABCD1234567890WXYZ1234567890ABCDEF',
+ status: 'completed',
+ notes: 'Order successfully confirmed and payment verified.',
+ sensorReadings: {
+ temperature: '22°C',
+ humidity: '45%',
+ pressure: '1013 hPa',
+ },
+ },
+ // ... more milestones
+];
+
+
+```
+
+## Props
+
+### MilestoneTimelineProps
+
+| Prop | Type | Required | Description |
+|------|------|----------|-------------|
+| milestones | `MilestoneDetail[]` | Yes | Array of milestone objects to display |
+
+### MilestoneDetail
+
+| Property | Type | Required | Description |
+|----------|------|----------|-------------|
+| id | `string` | Yes | Unique identifier for the milestone |
+| name | `string` | Yes | Display name of the milestone event |
+| timestamp | `string` | Yes | Timestamp of the milestone |
+| location | `string` | Yes | Geographic location of the milestone |
+| blockchainAddress | `string` | Yes | Blockchain address that recorded the event |
+| status | `'completed' \| 'current' \| 'upcoming'` | Yes | Visual state of the milestone |
+| notes | `string` | No | Additional notes or description (shown when expanded) |
+| sensorReadings | `object` | No | Sensor data readings (shown when expanded) |
+
+### SensorReadings
+
+The `sensorReadings` object accepts any key-value pairs where values are strings:
+
+```tsx
+sensorReadings: {
+ temperature?: string;
+ humidity?: string;
+ pressure?: string;
+ [key: string]: string | undefined;
+}
+```
+
+## Status States
+
+### Completed
+- Blue checkmark icon
+- Solid blue connector line
+- Full color text and details
+- Indicates milestone has been reached
+
+### Current
+- Pulsing cyan icon with glow effect
+- Dashed connector line to next milestone
+- Highlighted border on card
+- Indicates active/in-progress milestone
+
+### Upcoming
+- Gray outlined icon
+- Dashed gray connector line
+- Muted text colors
+- Indicates future milestone
+
+## Expandable Details
+
+Milestones with `notes` or `sensorReadings` display an expand/collapse button. Click to reveal:
+
+- **Notes Section**: Detailed description or additional information
+- **Sensor Readings Section**: Grid of sensor data with labels and values
+
+## Blockchain Address Truncation
+
+Long blockchain addresses are automatically truncated for readability:
+- Full: `GABCD1234567890WXYZ1234567890ABCDEF`
+- Displayed: `GABCD...CDEF`
+
+Addresses 12 characters or shorter are displayed in full.
+
+## Styling
+
+The component uses custom CSS with design tokens from the Tailwind config:
+- Background colors: `#0F1419` (card), `#050505` (page)
+- Border colors: `#1E2433` (default), `rgba(0, 217, 255, 0.3)` (current)
+- Accent colors: `#00D9FF` (primary), `#3B82F6` (completed)
+- Text colors: `rgba(255, 255, 255, 0.87)` (primary), `#9CA3AF` (secondary)
+
+## Responsive Breakpoints
+
+- **Desktop** (>768px): Full layout with multi-column sensor grid
+- **Tablet** (480px-768px): Adjusted spacing and single-column sensor grid
+- **Mobile** (<480px): Compact layout with smaller icons and stacked elements
+
+## Accessibility
+
+- Semantic HTML with proper ARIA labels
+- Keyboard navigation support
+- Focus indicators on interactive elements
+- Screen reader friendly status icons
+- Proper heading hierarchy
+
+## Examples
+
+See `MilestoneTimeline.example.tsx` for complete usage examples:
+- Standard domestic shipment
+- International shipment with customs
+- Cold chain pharmaceutical delivery
+
+## Testing
+
+Run tests with:
+```bash
+pnpm run test MilestoneTimeline.test.tsx
+```
+
+The test suite covers:
+- Rendering all milestones
+- Blockchain address truncation
+- Expand/collapse functionality
+- Sensor readings display
+- Status state styling
+- Empty state handling
diff --git a/frontend/src/pages/ShipmentDetail/PaymentStatus/PaymentStatus.tsx b/frontend/src/pages/ShipmentDetail/PaymentStatus/PaymentStatus.tsx
new file mode 100644
index 0000000..3669583
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/PaymentStatus/PaymentStatus.tsx
@@ -0,0 +1,139 @@
+import React from "react";
+import { ExternalLink, Wallet, CreditCard, ArrowRightLeft, Hash } from "lucide-react";
+
+export type PaymentStatusType = "pending" | "escrowed" | "released" | "failed";
+
+export interface PaymentData {
+ amount: string;
+ tokenSymbol: string;
+ status: PaymentStatusType;
+ payerAddress: string;
+ payeeAddress: string;
+ transactionHash: string;
+}
+
+export interface PaymentStatusProps {
+ payment?: PaymentData | null;
+}
+
+const PaymentStatus: React.FC = ({ payment }) => {
+ const statusStyles: Record = {
+ pending: "bg-yellow-500/20 text-yellow-300 border border-yellow-500/40",
+ escrowed: "bg-blue-500/20 text-blue-300 border border-blue-500/40",
+ released: "bg-green-500/20 text-green-300 border border-green-500/40",
+ failed: "bg-red-500/20 text-red-300 border border-red-500/40",
+ };
+
+ const formatStatus = (status: PaymentStatusType): string =>
+ status.charAt(0).toUpperCase() + status.slice(1);
+
+ const truncateAddress = (address: string): string => {
+ if (address.length <= 12) return address;
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+ };
+
+ const getStellarExplorerUrl = (txHash: string): string =>
+ `https://stellar.expert/explorer/testnet/tx/${txHash}`;
+
+ return (
+
+
+ PAYMENT STATUS
+
+
+ {payment ? (
+
+ {/* Payment Amount Header */}
+
+
+
+
+
+
+
Payment Amount
+
+ {payment.amount} {payment.tokenSymbol}
+
+
+
+
+
+ {formatStatus(payment.status)}
+
+
+
+ {/* Payment Details */}
+
+ {/* Payer Address */}
+
+
+
+
+
+
Payer Address
+
+ {truncateAddress(payment.payerAddress)}
+
+
+
+
+ {/* Payee Address */}
+
+
+
+
Payee Address
+
+ {truncateAddress(payment.payeeAddress)}
+
+
+
+
+ {/* Transaction Hash */}
+
+
+
+ ) : (
+ /* Empty State */
+
+
+
+
+
Payment Not Yet Initiated
+
+ No payment has been made for this shipment yet. Payment details will appear here once a transaction is initiated.
+
+
+ )}
+
+ );
+};
+
+export default PaymentStatus;
\ No newline at end of file
diff --git a/frontend/src/pages/ShipmentDetail/SensorDataCards/SensorDataCards.tsx b/frontend/src/pages/ShipmentDetail/SensorDataCards/SensorDataCards.tsx
new file mode 100644
index 0000000..f97e606
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/SensorDataCards/SensorDataCards.tsx
@@ -0,0 +1,163 @@
+import React from "react";
+import { Thermometer, Droplets, MapPin, AlertTriangle } from "lucide-react";
+
+export interface SensorData {
+ temperature?: {
+ value: number;
+ unit: string;
+ lastUpdated: string;
+ };
+ humidity?: {
+ value: number;
+ unit: string;
+ lastUpdated: string;
+ };
+ gps?: {
+ latitude: number;
+ longitude: number;
+ lastUpdated: string;
+ };
+ shockTilt?: {
+ eventCount: number;
+ lastUpdated: string;
+ };
+}
+
+export interface SensorDataCardsProps {
+ sensorData?: SensorData | null;
+}
+
+interface SensorCardProps {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+ subValue?: string;
+ lastUpdated: string;
+}
+
+const SensorCard: React.FC = ({
+ icon,
+ label,
+ value,
+ subValue,
+ lastUpdated,
+}) => (
+
+
+
+
+ {value}
+ {subValue && (
+ {subValue}
+ )}
+
+
+ Last updated: {lastUpdated}
+
+
+
+);
+
+const SensorDataCards: React.FC = ({ sensorData }) => {
+ const formatCoordinate = (coord: number, type: "lat" | "lng"): string => {
+ const direction = type === "lat" ? (coord >= 0 ? "N" : "S") : (coord >= 0 ? "E" : "W");
+ return `${Math.abs(coord).toFixed(4)}° ${direction}`;
+ };
+
+ const hasAnyData = sensorData && (
+ sensorData.temperature ||
+ sensorData.humidity ||
+ sensorData.gps ||
+ sensorData.shockTilt
+ );
+
+ return (
+
+
+ SENSOR DATA
+
+
+ {hasAnyData ? (
+
+ {/* Temperature Card */}
+ {sensorData.temperature ? (
+
}
+ label="Temperature"
+ value={`${sensorData.temperature.value}`}
+ subValue={sensorData.temperature.unit}
+ lastUpdated={sensorData.temperature.lastUpdated}
+ />
+ ) : (
+
+ )}
+
+ {/* Humidity Card */}
+ {sensorData.humidity ? (
+
}
+ label="Humidity"
+ value={`${sensorData.humidity.value}`}
+ subValue={sensorData.humidity.unit}
+ lastUpdated={sensorData.humidity.lastUpdated}
+ />
+ ) : (
+
+ )}
+
+ {/* GPS Location Card */}
+ {sensorData.gps ? (
+
}
+ label="GPS Location"
+ value={formatCoordinate(sensorData.gps.latitude, "lat")}
+ subValue={formatCoordinate(sensorData.gps.longitude, "lng")}
+ lastUpdated={sensorData.gps.lastUpdated}
+ />
+ ) : (
+
+ )}
+
+ {/* Shock/Tilt Events Card */}
+ {sensorData.shockTilt ? (
+
}
+ label="Shock/Tilt Events"
+ value={`${sensorData.shockTilt.eventCount}`}
+ subValue="events"
+ lastUpdated={sensorData.shockTilt.lastUpdated}
+ />
+ ) : (
+
+ )}
+
+ ) : (
+ /* Empty State */
+
+
+
+
+
No Sensor Data Available
+
+ IoT sensor readings will appear here once the shipment tracking devices start transmitting data.
+
+
+ )}
+
+ );
+};
+
+export default SensorDataCards;
\ No newline at end of file
diff --git a/frontend/src/pages/ShipmentDetail/ShipmentDetail.tsx b/frontend/src/pages/ShipmentDetail/ShipmentDetail.tsx
new file mode 100644
index 0000000..9e62001
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/ShipmentDetail.tsx
@@ -0,0 +1,217 @@
+import React from "react";
+import MilestoneTimeline, {
+ MilestoneDetail,
+} from "./MilestoneTimeline/MilestoneTimeline";
+import ShipmentDetailHeader from "./ShipmentDetailHeader/ShipmentDetailHeader";
+import ShipmentMap from "./ShipmentMap/ShipmentMap";
+import DeliveryProofUpload from "./DeliveryProofUpload/DeliveryProofUpload";
+import DeliveryConfirmation from "../../components/shipment/DeliveryConfirmation/DeliveryConfirmation";
+import PaymentStatus, { PaymentData } from "./PaymentStatus/PaymentStatus";
+import SensorDataCards, { SensorData } from "./SensorDataCards/SensorDataCards";
+
+const ShipmentDetail: React.FC = () => {
+ const shipmentHeaderData = {
+ shipmentId: "#SHP-992834",
+ status: "in_transit" as const,
+ originAddress: "New York Distribution Center, NY 10001",
+ destinationAddress: "123 Main Street, Boston, MA 02101",
+ expectedDeliveryDate: "Oct 24, 2026 by 5:00 PM EST",
+ userRole: "company" as const,
+ };
+
+ const handleUpdateStatus = () => {
+ console.log("Update status clicked");
+ };
+ const handleTrack = () => {
+ console.log("Track clicked");
+ };
+
+ // Mock payment data - set to null to show empty state
+ const mockPaymentData: PaymentData | null = {
+ amount: "1,500.00",
+ tokenSymbol: "XLM",
+ status: "escrowed",
+ payerAddress:
+ "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI",
+ payeeAddress:
+ "GCFXHS4GXL6BVUCXBWXGTITROWLVYXQKQLF4YH5O5JT3YZXCYPAFBJZB",
+ transactionHash:
+ "a]b c9d4e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9",
+ };
+
+ // Mock sensor data - set to null to show empty state
+ const mockSensorData: SensorData | null = {
+ temperature: {
+ value: 22,
+ unit: "°C",
+ lastUpdated: "2026-02-23 09:15 AM EST",
+ },
+ humidity: {
+ value: 45,
+ unit: "%",
+ lastUpdated: "2026-02-23 09:15 AM EST",
+ },
+ gps: {
+ latitude: 42.3601,
+ longitude: -71.0589,
+ lastUpdated: "2026-02-23 09:10 AM EST",
+ },
+ shockTilt: {
+ eventCount: 2,
+ lastUpdated: "2026-02-22 03:45 PM EST",
+ },
+ };
+
+ const mockMilestones: MilestoneDetail[] = [
+ {
+ id: "1",
+ name: "Order Confirmed",
+ timestamp: "2026-02-20 09:15 AM EST",
+ location: "New York Distribution Center, NY",
+ blockchainAddress: "GABCD1234567890WXYZ1234567890ABCDEF",
+ status: "completed",
+ notes: "Order successfully confirmed and payment verified on blockchain. Shipment prepared for pickup.",
+ sensorReadings: {
+ temperature: "22°C",
+ humidity: "45%",
+ pressure: "1013 hPa",
+ },
+ },
+ {
+ id: "2",
+ name: "Picked Up by Carrier",
+ timestamp: "2026-02-20 02:30 PM EST",
+ location: "New York Distribution Center, NY",
+ blockchainAddress: "GEFGH2345678901YZAB2345678901BCDEFG",
+ status: "completed",
+ notes: "Package picked up by carrier. Driver ID verified and logged on-chain.",
+ sensorReadings: {
+ temperature: "21°C",
+ humidity: "48%",
+ pressure: "1012 hPa",
+ },
+ },
+ {
+ id: "3",
+ name: "In Transit - Philadelphia Hub",
+ timestamp: "2026-02-21 08:45 AM EST",
+ location: "Philadelphia Logistics Hub, PA",
+ blockchainAddress: "GIJKL3456789012ZABC3456789012CDEFGH",
+ status: "completed",
+ notes: "Shipment arrived at Philadelphia hub. Passed quality inspection.",
+ sensorReadings: {
+ temperature: "20°C",
+ humidity: "50%",
+ pressure: "1014 hPa",
+ },
+ },
+ {
+ id: "4",
+ name: "Departed Philadelphia Hub",
+ timestamp: "2026-02-21 03:20 PM EST",
+ location: "Philadelphia Logistics Hub, PA",
+ blockchainAddress: "GMNOP4567890123ABCD4567890123DEFGHI",
+ status: "completed",
+ notes: "Package departed Philadelphia hub en route to Boston.",
+ sensorReadings: {
+ temperature: "19°C",
+ humidity: "52%",
+ pressure: "1015 hPa",
+ },
+ },
+ {
+ id: "5",
+ name: "Arrived at Boston Facility",
+ timestamp: "2026-02-22 07:10 AM EST",
+ location: "Boston Regional Facility, MA",
+ blockchainAddress: "GQRST5678901234BCDE5678901234EFGHIJ",
+ status: "completed",
+ notes: "Shipment arrived at Boston facility. Sorted and prepared for final delivery.",
+ sensorReadings: {
+ temperature: "18°C",
+ humidity: "55%",
+ pressure: "1016 hPa",
+ },
+ },
+ {
+ id: "6",
+ name: "Out for Delivery",
+ timestamp: "2026-02-23 09:00 AM EST",
+ location: "Boston, MA",
+ blockchainAddress: "GUVWX6789012345CDEF6789012345FGHIJK",
+ status: "current",
+ notes: "Package is currently out for delivery. Driver en route to destination address.",
+ sensorReadings: {
+ temperature: "17°C",
+ humidity: "58%",
+ pressure: "1017 hPa",
+ },
+ },
+ {
+ id: "7",
+ name: "Delivered",
+ timestamp: "Expected: 2026-02-23 05:00 PM EST",
+ location: "Boston, MA",
+ blockchainAddress: "GYZAB7890123456DEFG7890123456GHIJKL",
+ status: "upcoming",
+ notes: "Estimated delivery time. Signature will be required upon delivery.",
+ },
+ ];
+
+ return (
+
+
+ {/* Page header */}
+
+
+ SHIPMENT DETAILS
+
+
+ Track your shipment's journey with blockchain-verified
+ milestones
+
+
+
+ {/* Content card */}
+
+
+
+
+
+ {/* Divider */}
+
+
+
+ MILESTONE{" "}
+ TIMELINE
+
+
+
+
+
+
+
+
{
+ console.log("Delivery confirmed", {
+ id,
+ rating,
+ feedback,
+ });
+ }}
+ />
+
+
+ );
+};
+
+export default ShipmentDetail;
diff --git a/frontend/src/pages/ShipmentDetail/ShipmentDetailHeader/ShipmentDetailHeader.tsx b/frontend/src/pages/ShipmentDetail/ShipmentDetailHeader/ShipmentDetailHeader.tsx
new file mode 100644
index 0000000..b73f4a1
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/ShipmentDetailHeader/ShipmentDetailHeader.tsx
@@ -0,0 +1,115 @@
+import React from "react";
+import { Package, ArrowRight } from "lucide-react";
+
+export type ShipmentStatus =
+ | "pending"
+ | "in_transit"
+ | "out_for_delivery"
+ | "delivered";
+
+export type UserRole = "company" | "customer";
+
+export interface ShipmentDetailHeaderProps {
+ shipmentId: string;
+ status: ShipmentStatus;
+ expectedDeliveryDate: string;
+ userRole: UserRole;
+ originAddress?: string;
+ destinationAddress?: string;
+ onUpdateStatus?: () => void;
+ onTrack?: () => void;
+}
+
+const ShipmentDetailHeader: React.FC = ({
+ shipmentId,
+ status,
+ expectedDeliveryDate,
+ userRole,
+ originAddress,
+ destinationAddress,
+ onUpdateStatus,
+ onTrack,
+}) => {
+ const statusColors: Record = {
+ pending: "bg-yellow-500/20 text-yellow-300 border border-yellow-500/40",
+ in_transit: "bg-blue-500/20 text-blue-300 border border-blue-500/40",
+ out_for_delivery:
+ "bg-purple-500/20 text-purple-300 border border-purple-500/40",
+ delivered: "bg-green-500/20 text-green-300 border border-green-500/40",
+ };
+
+ const formatStatus = (status: ShipmentStatus): string =>
+ status
+ .split("_")
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
+ .join(" ");
+
+ return (
+
+ {/* Left Section */}
+
+ {/* Icon Container */}
+
+
+ {/* Shipment Info */}
+
+ {/* Title + Status */}
+
+
+ {shipmentId}
+
+
+
+
+ {formatStatus(status)}
+
+
+
+
+ ETA: {expectedDeliveryDate}
+
+
+ {/* Origin to Destination */}
+ {originAddress && destinationAddress && (
+
+
+ {originAddress}
+
+
+
+ {destinationAddress}
+
+
+ )}
+
+
+
+ {/* Right Section - Actions */}
+
+ {userRole === "company" && (
+
+ )}
+
+ {userRole === "customer" && (
+
+ )}
+
+
+ );
+};
+
+export default ShipmentDetailHeader;
diff --git a/frontend/src/pages/ShipmentDetail/ShipmentDetailHeader/index.ts b/frontend/src/pages/ShipmentDetail/ShipmentDetailHeader/index.ts
new file mode 100644
index 0000000..70c9c90
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/ShipmentDetailHeader/index.ts
@@ -0,0 +1,6 @@
+export { default } from "./ShipmentDetailHeader";
+export type {
+ ShipmentDetailHeaderProps,
+ ShipmentStatus,
+ UserRole,
+} from "./ShipmentDetailHeader";
diff --git a/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.css b/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.css
new file mode 100644
index 0000000..df12e27
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.css
@@ -0,0 +1,124 @@
+/* frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.css */
+.shipment-map-container {
+ margin-bottom: 2.5rem;
+ width: 100%;
+}
+
+.map-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ margin-bottom: 1.5rem;
+}
+
+.map-title {
+ font-family: 'Bebas Neue', sans-serif;
+ font-size: 2.5rem;
+ font-weight: 400;
+ letter-spacing: 0.04em;
+ color: white;
+ margin: 0;
+}
+
+.map-title .highlight {
+ color: #00d4c8;
+}
+
+.map-stats {
+ display: flex;
+ gap: 2rem;
+ text-align: right;
+}
+
+.map-stats .stat {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.map-stats .label {
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: rgba(0, 212, 200, 0.6);
+ letter-spacing: 0.1em;
+}
+
+.map-stats .value {
+ font-size: 0.85rem;
+ font-weight: 400;
+ color: rgba(200, 230, 240, 0.9);
+ max-width: 250px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.map-placeholder {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 2 / 1;
+ border-radius: 1.5rem;
+ overflow: hidden;
+ border: 1.5px solid rgba(0, 212, 200, 0.2);
+ background: rgba(8, 40, 50, 0.2);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+}
+
+.map-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ opacity: 0.8;
+ filter: saturate(0.8) brightness(0.9);
+}
+
+.map-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: radial-gradient(circle, rgba(0, 212, 200, 0.05) 0%, transparent 70%);
+}
+
+.view-full-map-btn {
+ font-family: 'Bebas Neue', sans-serif;
+ font-size: 1.25rem;
+ letter-spacing: 0.05em;
+ padding: 0.75rem 2.5rem;
+ background: rgba(0, 212, 200, 0.1);
+ color: #62ffff;
+ border: 1.5px solid #00d4c8;
+ border-radius: 0.75rem;
+ cursor: pointer;
+ backdrop-filter: blur(8px);
+ transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+ box-shadow: 0 0 15px rgba(0, 212, 200, 0.2);
+}
+
+.view-full-map-btn:hover {
+ background: #00d4c8;
+ color: #000;
+ transform: translateY(-2px);
+ box-shadow: 0 0 25px rgba(0, 212, 200, 0.4);
+}
+
+@media (max-width: 768px) {
+ .map-header {
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .map-stats {
+ text-align: center;
+ }
+
+ .map-stats .value {
+ max-width: 100%;
+ }
+}
diff --git a/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.test.tsx b/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.test.tsx
new file mode 100644
index 0000000..becce6b
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.test.tsx
@@ -0,0 +1,21 @@
+import { describe, test, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import ShipmentMap from './ShipmentMap';
+
+describe('ShipmentMap placeholder', () => {
+ test('renders map view with origin/destination and view button', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('heading', { name: /map view/i })).toBeInTheDocument();
+ expect(screen.getByText('ORIGIN:')).toBeInTheDocument();
+ expect(screen.getByText('DESTINATION:')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /View full map/i })).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.tsx b/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.tsx
new file mode 100644
index 0000000..432703a
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.tsx
@@ -0,0 +1,51 @@
+// frontend/src/pages/ShipmentDetail/ShipmentMap/ShipmentMap.tsx
+import React from 'react';
+import './ShipmentMap.css';
+
+interface Coords {
+ lat: number;
+ lng: number;
+}
+
+interface ShipmentMapProps {
+ origin: string;
+ destination: string;
+ originCoords?: Coords;
+ destinationCoords?: Coords;
+}
+
+const ShipmentMap: React.FC = ({ origin, destination }) => {
+ return (
+
+
+
MAP VIEW
+
+
+ ORIGIN:
+ {origin}
+
+
+ DESTINATION:
+ {destination}
+
+
+
+
+
+

+
+
+
+
+
+
+ );
+};
+
+export default ShipmentMap;
diff --git a/frontend/src/pages/ShipmentDetail/index.ts b/frontend/src/pages/ShipmentDetail/index.ts
new file mode 100644
index 0000000..1a1a175
--- /dev/null
+++ b/frontend/src/pages/ShipmentDetail/index.ts
@@ -0,0 +1,3 @@
+export { default } from './ShipmentDetail';
+export { default as MilestoneTimeline } from './MilestoneTimeline/MilestoneTimeline';
+export type { MilestoneDetail, MilestoneTimelineProps } from './MilestoneTimeline/MilestoneTimeline';
diff --git a/frontend/src/pages/auth/ForgotPassword/ForgotPassword.css b/frontend/src/pages/auth/ForgotPassword/ForgotPassword.css
deleted file mode 100644
index ab20c46..0000000
--- a/frontend/src/pages/auth/ForgotPassword/ForgotPassword.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.success-card {
- text-align: center;
- display: flex;
- flex-direction: column;
- align-items: center;
- animation: fadeIn 0.5s ease-out;
-}
-
-.success-icon-wrapper {
- margin-bottom: 24px;
- background: rgba(0, 218, 193, 0.1);
- padding: 20px;
- border-radius: 50%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-
-.success-icon {
- color: var(--teal-primary);
-}
-
-.back-to-login-btn {
- width: 100%;
- margin-top: 8px;
- text-decoration: none;
-}
-
-.input-icon {
- position: absolute;
- right: 16px;
- top: 50%;
- transform: translateY(-50%);
- color: var(--text-muted);
- display: flex;
- align-items: center;
- pointer-events: none;
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
\ No newline at end of file
diff --git a/frontend/src/pages/auth/ForgotPassword/ForgotPassword.tsx b/frontend/src/pages/auth/ForgotPassword/ForgotPassword.tsx
index 57cf8b0..d7fcdd5 100644
--- a/frontend/src/pages/auth/ForgotPassword/ForgotPassword.tsx
+++ b/frontend/src/pages/auth/ForgotPassword/ForgotPassword.tsx
@@ -1,8 +1,9 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Mail, ArrowLeft, CheckCircle2 } from 'lucide-react';
-import '../Signup/Signup.css';
-import './ForgotPassword.css';
+
+const authCardClass = "min-h-screen flex items-center justify-center bg-[#050505] text-white relative overflow-hidden font-sans";
+const cardInnerClass = "bg-[rgba(20,20,20,0.7)] backdrop-blur-[20px] border border-[rgba(255,255,255,0.1)] rounded-3xl p-10 w-full max-w-[480px] z-10 shadow-[0_8px_32px_0_rgba(0,0,0,0.8)] sm:p-8 sm:rounded-none sm:min-h-screen sm:flex sm:flex-col sm:justify-center";
const ForgotPassword: React.FC = () => {
const [email, setEmail] = useState('');
@@ -12,108 +13,109 @@ const ForgotPassword: React.FC = () => {
const [touched, setTouched] = useState(false);
const validateEmail = (val: string) => {
- if (!val) {
- return 'Email is required';
- } else if (!/\S+@\S+\.\S+/.test(val)) {
- return 'Invalid email format';
- }
+ if (!val) return 'Email is required';
+ if (!/\S+@\S+\.\S+/.test(val)) return 'Invalid email format';
return '';
};
const handleChange = (e: React.ChangeEvent) => {
const val = e.target.value;
setEmail(val);
- if (touched) {
- setError(validateEmail(val));
- }
+ if (touched) setError(validateEmail(val));
};
- const handleBlur = () => {
- setTouched(true);
- setError(validateEmail(email));
- };
+ const handleBlur = () => { setTouched(true); setError(validateEmail(email)); };
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setTouched(true);
const emailError = validateEmail(email);
- if (emailError) {
- setError(emailError);
- return;
- }
-
+ if (emailError) { setError(emailError); return; }
setLoading(true);
- // Simulate API call
- setTimeout(() => {
- setLoading(false);
- setSubmitted(true);
- }, 1500);
+ setTimeout(() => { setLoading(false); setSubmitted(true); }, 1500);
};
+ const inputBase = "w-full bg-[rgba(255,255,255,0.05)] border border-[rgba(255,255,255,0.1)] rounded-xl px-4 py-3.5 pr-12 text-white text-base transition-all box-border focus:outline-none focus:border-[#00DAC1] focus:bg-[rgba(255,255,255,0.08)] focus:shadow-[0_0_0_4px_rgba(0,218,193,0.1)]";
+
+ const glowTop = (
+
+ );
+ const glowBottom = (
+
+ );
+
if (submitted) {
return (
-
-
-
-
-
-
-
Check your email
-
If this email exists, you'll receive a reset link shortly.
+
+ {glowTop}{glowBottom}
+
+
+
+
+
+
+
+ Check your email
+
+
If this email exists, you'll receive a reset link shortly.
+
+
+
Back to Login
+
-
-
- Back to Login
-
);
}
return (
-
-
-
-
Reset Password
-
Enter your email address and we'll send you a link to reset your password.
+
+ {glowTop}{glowBottom}
+
+
+
+ Reset Password
+
+
Enter your email address and we'll send you a link to reset your password.
-