diff --git a/frontend/package/pages/Marketplace/MarketplaceLoadBoard.tsx b/frontend/package/pages/Marketplace/MarketplaceLoadBoard.tsx new file mode 100644 index 00000000..9150f628 --- /dev/null +++ b/frontend/package/pages/Marketplace/MarketplaceLoadBoard.tsx @@ -0,0 +1,261 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ShipmentCardSkeleton } from '@/components/ui/skeleton'; +import { shipmentApi } from '@/lib/api/shipment.api'; +import { ShipmentCard } from '@/components/shipment/shipment-card'; +import { BidSubmissionForm } from '../../components/BidForm/BidSubmissionForm'; +import type { QueryShipmentParams } from '@/types/shipment.types'; + +const CARGO_CATEGORIES = [ + 'All', 'Electronics', 'Furniture', 'Food & Beverage', + 'Clothing', 'Machinery', 'Chemicals', 'Automotive', 'Medical', 'Other', +]; + +type SortOption = 'newest' | 'price_asc' | 'price_desc' | 'weight_asc' | 'weight_desc'; + +const SORT_OPTIONS: { label: string; value: SortOption }[] = [ + { label: 'Newest', value: 'newest' }, + { label: 'Price: Low → High', value: 'price_asc' }, + { label: 'Price: High → Low', value: 'price_desc' }, + { label: 'Weight: Light → Heavy', value: 'weight_asc' }, + { label: 'Weight: Heavy → Light', value: 'weight_desc' }, +]; + +export function MarketplaceLoadBoard() { + const [origin, setOrigin] = useState(''); + const [destination, setDestination] = useState(''); + const [cargoCategory, setCargoCategory] = useState('All'); + const [minPrice, setMinPrice] = useState(''); + const [maxPrice, setMaxPrice] = useState(''); + const [sort, setSort] = useState('newest'); + const [page, setPage] = useState(1); + const [bidShipmentId, setBidShipmentId] = useState(null); + const [filters, setFilters] = useState({ + page: 1, + limit: 12, + }); + + const { data: result, isLoading, error } = useQuery({ + queryKey: ['marketplace-loadboard', filters], + queryFn: () => shipmentApi.marketplace({ ...filters, page: filters.page }), + }); + + if (error) { + toast.error('Failed to load marketplace'); + } + + const applyFilters = useCallback( + (pg = 1) => { + setPage(pg); + setFilters({ + origin: origin || undefined, + destination: destination || undefined, + page: pg, + limit: 12, + cargoCategory: cargoCategory !== 'All' ? cargoCategory : undefined, + minPrice: minPrice ? Number(minPrice) : undefined, + maxPrice: maxPrice ? Number(maxPrice) : undefined, + }); + }, + [origin, destination, cargoCategory, minPrice, maxPrice], + ); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + applyFilters(1); + }; + + const handleClear = () => { + setOrigin(''); + setDestination(''); + setCargoCategory('All'); + setMinPrice(''); + setMaxPrice(''); + setSort('newest'); + setPage(1); + setFilters({ page: 1, limit: 12 }); + }; + + const sorted = result?.data + ? [...result.data].sort((a, b) => { + if (sort === 'price_asc') return Number(a.price) - Number(b.price); + if (sort === 'price_desc') return Number(b.price) - Number(a.price); + if (sort === 'weight_asc') return Number(a.weightKg) - Number(b.weightKg); + if (sort === 'weight_desc') return Number(b.weightKg) - Number(a.weightKg); + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }) + : []; + + const hasFilters = origin || destination || cargoCategory !== 'All' || minPrice || maxPrice; + + return ( +
+ {/* Filter Panel */} + + + Filters + + +
+
+ setOrigin(e.target.value)} + className="w-36" + /> + setDestination(e.target.value)} + className="w-36" + /> + + setMinPrice(e.target.value)} + className="w-28" + min={0} + /> + setMaxPrice(e.target.value)} + className="w-28" + min={0} + /> + + + {hasFilters && ( + + )} +
+
+
+
+ + {/* Load Cards */} + {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : !result || sorted.length === 0 ? ( +
+

+ No available shipments match your filters. Try adjusting your search criteria. +

+ {hasFilters && ( + + )} +
+ ) : ( + <> +
+ {sorted.map((s) => ( +
+ +
+ +
+
+ ))} +
+ + {/* Pagination */} + {result.totalPages > 1 && ( +
+ + + Page {page} of {result.totalPages} + + +
+ )} + +

+ {result.total} shipment{result.total !== 1 ? 's' : ''} available +

+ + )} + + {/* Bid Modal */} + {bidShipmentId && ( +
+
+
+

Place a Bid

+ +
+ setBidShipmentId(null)} + /> +
+
+ )} +
+ ); +} diff --git a/frontend/package/pages/Marketplace/index.ts b/frontend/package/pages/Marketplace/index.ts new file mode 100644 index 00000000..8f86d587 --- /dev/null +++ b/frontend/package/pages/Marketplace/index.ts @@ -0,0 +1 @@ +export { MarketplaceLoadBoard } from './MarketplaceLoadBoard';