@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
- package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
- package/dist/CreateOrderPage-CWHx8yys.js +490 -0
- package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
- package/dist/CustomersPage-Bbvwvn8k.js +205 -0
- package/dist/DashboardPage-DrEhNf7N.js +542 -0
- package/dist/DataTable-CqMUD4er.js +340 -0
- package/dist/DocumentsCard-edDkgnJj.js +169 -0
- package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
- package/dist/DriversPage-CHF2ZEzd.js +221 -0
- package/dist/FinancePage-Dn9GrW-1.js +175 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
- package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
- package/dist/InventoryPage-l5A1f753.js +259 -0
- package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
- package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
- package/dist/MetricCard-y3bWHKUQ.js +191 -0
- package/dist/Notice-DOO-tjpV.js +46 -0
- package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
- package/dist/OrdersListPage-DhZJoQeI.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
- package/dist/ProductsPage-C8sn-1tN.js +186 -0
- package/dist/ReportsPage-D1tb0dcz.js +377 -0
- package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
- package/dist/Sparkline-K2RMJ58u.js +16729 -0
- package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
- package/dist/TrackingPage-BZrg4RCu.js +368 -0
- package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
- package/dist/VehiclesPage-D5BELbtM.js +184 -0
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
- package/dist/WarehousesPage-C1Y1M867.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
- package/dist/arrow-right-CmKOmIkh.js +11 -0
- package/dist/arrow-up-BJijRLAZ.js +17 -0
- package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
- package/dist/boxes-B_OGvQiW.js +54 -0
- package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
- package/dist/charts-Tj_PPTfA.js +2018 -0
- package/dist/chevron-left-B1rZyWWb.js +8 -0
- package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
- package/dist/circle-check-Cs1OVjtV.js +13 -0
- package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
- package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
- package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
- package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
- package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
- package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
- package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
- package/dist/index.css +2 -0
- package/dist/index.js +150 -172
- package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
- package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-yzLqiK_C.js +5070 -0
- package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
- package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
- package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
- package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
- package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
- package/dist/route-CWjPrtAN.js +22 -0
- package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
- package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
- package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
- package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
- package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
- package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
- package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
- package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
- package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
- package/dist/upload-DanJY1Vo.js +21 -0
- package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
- package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
- package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
- package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
- package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
- package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
- package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
- package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
- package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
- package/dist/user-check-IMhk-qkX.js +20 -0
- package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
- package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
- package/dist/wrench-Dg-sgH2e.js +8 -0
- package/dist/x-C4ObLVEa.js +11 -0
- package/package.json +5 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/StatusBadge.tsx +7 -6
- package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
- package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
- package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
- package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
- package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
- package/src/movethewheels/components/charts/chartTokens.ts +43 -0
- package/src/movethewheels/components/charts/index.ts +9 -0
- package/src/movethewheels/components/index.ts +51 -1
- package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
- package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
- package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
- package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
- package/src/movethewheels/components/maps/index.ts +16 -0
- package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
- package/src/movethewheels/components/maps/map-markers.css +121 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +109 -4
- package/src/movethewheels/hooks/useCustomers.ts +11 -4
- package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
- package/src/movethewheels/hooks/useFleet.ts +26 -7
- package/src/movethewheels/hooks/useInventory.ts +70 -7
- package/src/movethewheels/hooks/useOrders.ts +32 -2
- package/src/movethewheels/hooks/useReports.ts +14 -4
- package/src/movethewheels/hooks/useRoutes.ts +20 -3
- package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +53 -28
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
- package/src/movethewheels/pages/DriversPage.tsx +59 -31
- package/src/movethewheels/pages/FinancePage.tsx +103 -82
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
- package/src/movethewheels/pages/ProductsPage.tsx +52 -28
- package/src/movethewheels/pages/ReportsPage.tsx +88 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +5 -21
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
- package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
- package/src/movethewheels/types/index.ts +9 -1
- package/src/movethewheels/utils/download.ts +62 -0
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/CustomersPage-BKxMS1AV.js +0 -161
- package/dist/DashboardPage-BVjQi0EB.js +0 -387
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
- package/dist/DriversPage-CH9rjQC-.js +0 -165
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/InventoryPage-e0ze63iX.js +0 -253
- package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/MetricCard-BY0QrMD4.js +0 -146
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
- package/dist/ProductsPage-QJfHHpqB.js +0 -169
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- package/dist/TrackingPage-W-jtdM3Y.js +0 -325
- package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
- package/dist/VehiclesPage-ApKThYeG.js +0 -165
- package/dist/WarehousesPage-DCbXXXWE.js +0 -162
- package/dist/useAnalytics-DS8n0xgg.js +0 -297
- /package/dist/{formatters-CF2S2l1m.js → formatters-FWJDwTFv.js} +0 -0
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Marketplace Page
|
|
3
|
+
*
|
|
4
|
+
* Browse third-party logistics service providers and their offerings. Providers
|
|
5
|
+
* are paginated server-side (offset); clicking a provider expands its offerings
|
|
6
|
+
* inline. The Filters control narrows the current page by type, verification and
|
|
7
|
+
* minimum rating.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Store,
|
|
13
|
+
Star,
|
|
14
|
+
MapPin,
|
|
15
|
+
Clock,
|
|
16
|
+
Filter,
|
|
17
|
+
Search,
|
|
18
|
+
ChevronDown,
|
|
19
|
+
ChevronUp,
|
|
20
|
+
ChevronLeft,
|
|
21
|
+
ChevronRight,
|
|
22
|
+
} from 'lucide-react';
|
|
7
23
|
import { PageLayout } from '../components/PageLayout';
|
|
8
24
|
import {
|
|
9
25
|
Card,
|
|
@@ -14,6 +30,11 @@ import {
|
|
|
14
30
|
Badge,
|
|
15
31
|
Input,
|
|
16
32
|
Skeleton,
|
|
33
|
+
Select,
|
|
34
|
+
SelectContent,
|
|
35
|
+
SelectItem,
|
|
36
|
+
SelectTrigger,
|
|
37
|
+
SelectValue,
|
|
17
38
|
} from '../components/ui';
|
|
18
39
|
import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
19
40
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
@@ -86,6 +107,15 @@ const OFFERINGS_QUERY = `
|
|
|
86
107
|
}
|
|
87
108
|
`;
|
|
88
109
|
|
|
110
|
+
const PROVIDERS_PAGE_SIZE = 12;
|
|
111
|
+
|
|
112
|
+
const RATING_OPTIONS: { value: string; label: string }[] = [
|
|
113
|
+
{ value: '0', label: 'Any rating' },
|
|
114
|
+
{ value: '3', label: '3.0+' },
|
|
115
|
+
{ value: '4', label: '4.0+' },
|
|
116
|
+
{ value: '4.5', label: '4.5+' },
|
|
117
|
+
];
|
|
118
|
+
|
|
89
119
|
export default function MarketplacePage() {
|
|
90
120
|
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
91
121
|
useMoveTheWheels();
|
|
@@ -94,6 +124,17 @@ export default function MarketplacePage() {
|
|
|
94
124
|
const [search, setSearch] = useState('');
|
|
95
125
|
const [isLoading, setIsLoading] = useState(false);
|
|
96
126
|
const [error, setError] = useState<Error | null>(null);
|
|
127
|
+
const [page, setPage] = useState(0);
|
|
128
|
+
const [hasMore, setHasMore] = useState(false);
|
|
129
|
+
|
|
130
|
+
// Inline filters (toggled by the "Filters" button in the header).
|
|
131
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
132
|
+
const [typeFilter, setTypeFilter] = useState('all');
|
|
133
|
+
const [verifiedOnly, setVerifiedOnly] = useState(false);
|
|
134
|
+
const [minRating, setMinRating] = useState('0');
|
|
135
|
+
|
|
136
|
+
// Provider whose offerings are currently expanded.
|
|
137
|
+
const [expandedProviderId, setExpandedProviderId] = useState<string | null>(null);
|
|
97
138
|
|
|
98
139
|
const graphQLOptions = useMemo(
|
|
99
140
|
() => ({
|
|
@@ -115,7 +156,9 @@ export default function MarketplacePage() {
|
|
|
115
156
|
executeGraphQL<{ serviceProviders: ApiServiceProvider[] }>(
|
|
116
157
|
graphQLOptions,
|
|
117
158
|
PROVIDERS_QUERY,
|
|
118
|
-
{
|
|
159
|
+
{
|
|
160
|
+
pagination: { limit: PROVIDERS_PAGE_SIZE, offset: page * PROVIDERS_PAGE_SIZE },
|
|
161
|
+
}
|
|
119
162
|
),
|
|
120
163
|
executeGraphQL<{ serviceOfferings: ApiServiceOffering[] }>(
|
|
121
164
|
graphQLOptions,
|
|
@@ -127,28 +170,42 @@ export default function MarketplacePage() {
|
|
|
127
170
|
]);
|
|
128
171
|
|
|
129
172
|
setProviders(providersData.serviceProviders);
|
|
173
|
+
setHasMore(providersData.serviceProviders.length === PROVIDERS_PAGE_SIZE);
|
|
130
174
|
setOfferings(offeringsData.serviceOfferings);
|
|
131
175
|
} catch (fetchError) {
|
|
132
176
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load marketplace'));
|
|
133
177
|
} finally {
|
|
134
178
|
setIsLoading(false);
|
|
135
179
|
}
|
|
136
|
-
}, [graphQLOptions]);
|
|
180
|
+
}, [graphQLOptions, page]);
|
|
137
181
|
|
|
138
182
|
useEffect(() => {
|
|
139
183
|
void load();
|
|
140
184
|
}, [load]);
|
|
141
185
|
|
|
186
|
+
const providerTypes = useMemo(() => {
|
|
187
|
+
const set = new Set<string>();
|
|
188
|
+
providers.forEach((provider) => set.add(provider.type));
|
|
189
|
+
return Array.from(set).sort();
|
|
190
|
+
}, [providers]);
|
|
191
|
+
|
|
142
192
|
const filteredProviders = useMemo(() => {
|
|
143
193
|
const term = search.trim().toLowerCase();
|
|
144
|
-
|
|
145
|
-
return providers.filter(
|
|
146
|
-
(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
194
|
+
const rating = Number(minRating);
|
|
195
|
+
return providers.filter((provider) => {
|
|
196
|
+
if (term) {
|
|
197
|
+
const matches =
|
|
198
|
+
provider.name.toLowerCase().includes(term) ||
|
|
199
|
+
(provider.description ?? '').toLowerCase().includes(term) ||
|
|
200
|
+
provider.type.toLowerCase().includes(term);
|
|
201
|
+
if (!matches) return false;
|
|
202
|
+
}
|
|
203
|
+
if (typeFilter !== 'all' && provider.type !== typeFilter) return false;
|
|
204
|
+
if (verifiedOnly && !provider.isVerified) return false;
|
|
205
|
+
if (rating > 0 && (provider.rating ?? 0) < rating) return false;
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
}, [providers, search, typeFilter, verifiedOnly, minRating]);
|
|
152
209
|
|
|
153
210
|
const filteredOfferings = useMemo(() => {
|
|
154
211
|
const term = search.trim().toLowerCase();
|
|
@@ -161,13 +218,23 @@ export default function MarketplacePage() {
|
|
|
161
218
|
);
|
|
162
219
|
}, [offerings, search]);
|
|
163
220
|
|
|
221
|
+
const offeringsByProvider = useCallback(
|
|
222
|
+
(providerId: string) => offerings.filter((offering) => offering.providerId === providerId),
|
|
223
|
+
[offerings]
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const filtersActive = typeFilter !== 'all' || verifiedOnly || minRating !== '0';
|
|
227
|
+
|
|
164
228
|
return (
|
|
165
229
|
<PageLayout
|
|
166
230
|
title="Marketplace"
|
|
167
231
|
subtitle="Find logistics service providers"
|
|
168
232
|
icon={<Store size={24} />}
|
|
169
233
|
headerActions={
|
|
170
|
-
<Button
|
|
234
|
+
<Button
|
|
235
|
+
variant={showFilters || filtersActive ? 'default' : 'outline'}
|
|
236
|
+
onClick={() => setShowFilters((prev) => !prev)}
|
|
237
|
+
>
|
|
171
238
|
<Filter size={16} /> Filters
|
|
172
239
|
</Button>
|
|
173
240
|
}
|
|
@@ -179,7 +246,7 @@ export default function MarketplacePage() {
|
|
|
179
246
|
</PagePurpose>
|
|
180
247
|
|
|
181
248
|
<Card>
|
|
182
|
-
<CardContent className="p-4">
|
|
249
|
+
<CardContent className="p-4 space-y-4">
|
|
183
250
|
<div className="relative">
|
|
184
251
|
<Search
|
|
185
252
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
|
|
@@ -192,6 +259,62 @@ export default function MarketplacePage() {
|
|
|
192
259
|
onChange={(event) => setSearch(event.target.value)}
|
|
193
260
|
/>
|
|
194
261
|
</div>
|
|
262
|
+
|
|
263
|
+
{showFilters && (
|
|
264
|
+
<div className="flex flex-wrap items-center gap-3 border-t border-border-seam pt-4">
|
|
265
|
+
<div className="flex items-center gap-2">
|
|
266
|
+
<span className="text-sm font-medium text-text-secondary">Type</span>
|
|
267
|
+
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
|
268
|
+
<SelectTrigger className="w-[160px]">
|
|
269
|
+
<SelectValue placeholder="All types" />
|
|
270
|
+
</SelectTrigger>
|
|
271
|
+
<SelectContent>
|
|
272
|
+
<SelectItem value="all">All types</SelectItem>
|
|
273
|
+
{providerTypes.map((type) => (
|
|
274
|
+
<SelectItem key={type} value={type}>
|
|
275
|
+
{type}
|
|
276
|
+
</SelectItem>
|
|
277
|
+
))}
|
|
278
|
+
</SelectContent>
|
|
279
|
+
</Select>
|
|
280
|
+
</div>
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
<span className="text-sm font-medium text-text-secondary">Min rating</span>
|
|
283
|
+
<Select value={minRating} onValueChange={setMinRating}>
|
|
284
|
+
<SelectTrigger className="w-[130px]">
|
|
285
|
+
<SelectValue />
|
|
286
|
+
</SelectTrigger>
|
|
287
|
+
<SelectContent>
|
|
288
|
+
{RATING_OPTIONS.map((option) => (
|
|
289
|
+
<SelectItem key={option.value} value={option.value}>
|
|
290
|
+
{option.label}
|
|
291
|
+
</SelectItem>
|
|
292
|
+
))}
|
|
293
|
+
</SelectContent>
|
|
294
|
+
</Select>
|
|
295
|
+
</div>
|
|
296
|
+
<Button
|
|
297
|
+
variant={verifiedOnly ? 'default' : 'outline'}
|
|
298
|
+
size="sm"
|
|
299
|
+
onClick={() => setVerifiedOnly((prev) => !prev)}
|
|
300
|
+
>
|
|
301
|
+
Verified only
|
|
302
|
+
</Button>
|
|
303
|
+
{filtersActive && (
|
|
304
|
+
<Button
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="sm"
|
|
307
|
+
onClick={() => {
|
|
308
|
+
setTypeFilter('all');
|
|
309
|
+
setVerifiedOnly(false);
|
|
310
|
+
setMinRating('0');
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
Clear filters
|
|
314
|
+
</Button>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
195
318
|
</CardContent>
|
|
196
319
|
</Card>
|
|
197
320
|
|
|
@@ -217,11 +340,15 @@ export default function MarketplacePage() {
|
|
|
217
340
|
<Card>
|
|
218
341
|
<CardContent className="p-0">
|
|
219
342
|
<IllustratedEmptyState
|
|
220
|
-
illustration={search.trim() ? 'empty-search' : 'empty-people'}
|
|
221
|
-
title={
|
|
343
|
+
illustration={search.trim() || filtersActive ? 'empty-search' : 'empty-people'}
|
|
344
|
+
title={
|
|
345
|
+
search.trim() || filtersActive
|
|
346
|
+
? 'No providers match your filters'
|
|
347
|
+
: 'No providers yet'
|
|
348
|
+
}
|
|
222
349
|
description={
|
|
223
|
-
search.trim()
|
|
224
|
-
? '
|
|
350
|
+
search.trim() || filtersActive
|
|
351
|
+
? 'Adjust your search or filters to find logistics partners.'
|
|
225
352
|
: 'Verified logistics service providers will appear here as they join the marketplace.'
|
|
226
353
|
}
|
|
227
354
|
/>
|
|
@@ -230,42 +357,130 @@ export default function MarketplacePage() {
|
|
|
230
357
|
)}
|
|
231
358
|
{!isLoading &&
|
|
232
359
|
!error &&
|
|
233
|
-
filteredProviders.map((provider) =>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<
|
|
256
|
-
|
|
360
|
+
filteredProviders.map((provider) => {
|
|
361
|
+
const expanded = expandedProviderId === provider.id;
|
|
362
|
+
const providerOfferings = offeringsByProvider(provider.id);
|
|
363
|
+
return (
|
|
364
|
+
<Card key={provider.id}>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
aria-expanded={expanded}
|
|
368
|
+
className="w-full text-left"
|
|
369
|
+
onClick={() => setExpandedProviderId(expanded ? null : provider.id)}
|
|
370
|
+
>
|
|
371
|
+
<CardContent className="p-6">
|
|
372
|
+
<div className="flex items-start justify-between gap-3">
|
|
373
|
+
<div className="min-w-0">
|
|
374
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
375
|
+
<h4 className="font-semibold text-lg">{provider.name}</h4>
|
|
376
|
+
{provider.isVerified && <Badge variant="default">Verified</Badge>}
|
|
377
|
+
<Badge variant={provider.isActive ? 'default' : 'secondary'}>
|
|
378
|
+
{provider.isActive ? 'active' : 'inactive'}
|
|
379
|
+
</Badge>
|
|
380
|
+
</div>
|
|
381
|
+
<p className="text-text-secondary mt-1">{provider.description}</p>
|
|
382
|
+
<div className="flex items-center gap-4 mt-3 text-sm text-text-secondary">
|
|
383
|
+
<span className="flex items-center gap-1">
|
|
384
|
+
<Star size={14} className="text-status-warning-text fill-current" />{' '}
|
|
385
|
+
{provider.rating ?? 0}
|
|
386
|
+
</span>
|
|
387
|
+
<span className="flex items-center gap-1">
|
|
388
|
+
<MapPin size={14} /> {provider.type}
|
|
389
|
+
</span>
|
|
390
|
+
<span className="flex items-center gap-1">
|
|
391
|
+
<Clock size={14} /> Listed
|
|
392
|
+
</span>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="text-right shrink-0">
|
|
396
|
+
<p className="text-sm text-text-secondary">Services</p>
|
|
397
|
+
<p className="text-lg font-bold">{providerOfferings.length}</p>
|
|
398
|
+
<span className="mt-1 inline-flex items-center gap-1 text-xs text-primary">
|
|
399
|
+
{expanded ? (
|
|
400
|
+
<>
|
|
401
|
+
Hide <ChevronUp size={14} />
|
|
402
|
+
</>
|
|
403
|
+
) : (
|
|
404
|
+
<>
|
|
405
|
+
View <ChevronDown size={14} />
|
|
406
|
+
</>
|
|
407
|
+
)}
|
|
408
|
+
</span>
|
|
409
|
+
</div>
|
|
257
410
|
</div>
|
|
258
|
-
</
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
411
|
+
</CardContent>
|
|
412
|
+
</button>
|
|
413
|
+
{expanded && (
|
|
414
|
+
<div className="border-t border-border-seam px-6 py-4 space-y-3">
|
|
415
|
+
<p className="text-sm font-medium text-text-secondary">
|
|
416
|
+
Offerings from {provider.name}
|
|
263
417
|
</p>
|
|
418
|
+
{providerOfferings.length === 0 ? (
|
|
419
|
+
<p className="text-sm text-text-secondary">
|
|
420
|
+
This provider hasn't listed any offerings yet.
|
|
421
|
+
</p>
|
|
422
|
+
) : (
|
|
423
|
+
providerOfferings.map((offering) => (
|
|
424
|
+
<div
|
|
425
|
+
key={offering.id}
|
|
426
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-border-seam p-3"
|
|
427
|
+
>
|
|
428
|
+
<div className="min-w-0">
|
|
429
|
+
<p className="font-medium truncate">{offering.name}</p>
|
|
430
|
+
<p className="text-sm text-text-secondary truncate">
|
|
431
|
+
{offering.category ?? 'general'}
|
|
432
|
+
</p>
|
|
433
|
+
</div>
|
|
434
|
+
<div className="text-right shrink-0">
|
|
435
|
+
<p className="font-bold">{formatCurrency(offering.price)}</p>
|
|
436
|
+
<p className="text-xs text-text-secondary">{offering.currency}</p>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
))
|
|
440
|
+
)}
|
|
264
441
|
</div>
|
|
265
|
-
|
|
266
|
-
</
|
|
267
|
-
|
|
268
|
-
)
|
|
442
|
+
)}
|
|
443
|
+
</Card>
|
|
444
|
+
);
|
|
445
|
+
})}
|
|
446
|
+
|
|
447
|
+
{!isLoading && !error && (providers.length > 0 || page > 0) && (
|
|
448
|
+
<div className="flex flex-wrap items-center justify-between gap-3 pt-1">
|
|
449
|
+
<span className="text-sm text-text-secondary">
|
|
450
|
+
Showing {providers.length === 0 ? 0 : page * PROVIDERS_PAGE_SIZE + 1} to{' '}
|
|
451
|
+
{page * PROVIDERS_PAGE_SIZE + providers.length}
|
|
452
|
+
</span>
|
|
453
|
+
<div className="flex items-center gap-1">
|
|
454
|
+
<Button
|
|
455
|
+
variant="outline"
|
|
456
|
+
size="icon"
|
|
457
|
+
className="h-8 w-8"
|
|
458
|
+
aria-label="Previous page"
|
|
459
|
+
onClick={() => {
|
|
460
|
+
setExpandedProviderId(null);
|
|
461
|
+
setPage((p) => Math.max(0, p - 1));
|
|
462
|
+
}}
|
|
463
|
+
disabled={page === 0}
|
|
464
|
+
>
|
|
465
|
+
<ChevronLeft className="h-4 w-4" />
|
|
466
|
+
</Button>
|
|
467
|
+
<span className="px-3 text-sm">Page {page + 1}</span>
|
|
468
|
+
<Button
|
|
469
|
+
variant="outline"
|
|
470
|
+
size="icon"
|
|
471
|
+
className="h-8 w-8"
|
|
472
|
+
aria-label="Next page"
|
|
473
|
+
onClick={() => {
|
|
474
|
+
setExpandedProviderId(null);
|
|
475
|
+
setPage((p) => p + 1);
|
|
476
|
+
}}
|
|
477
|
+
disabled={!hasMore}
|
|
478
|
+
>
|
|
479
|
+
<ChevronRight className="h-4 w-4" />
|
|
480
|
+
</Button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
269
484
|
</div>
|
|
270
485
|
|
|
271
486
|
{/* Popular Services — the page's single emphasis zone */}
|