Skip to content

Commit 991d837

Browse files
authored
Merge pull request #350 from wheval/fix/issues-207
feat(frontend): build reusable order list table
2 parents c31822e + 1aa0fc3 commit 991d837

2 files changed

Lines changed: 297 additions & 155 deletions

File tree

frontend/src/components/marketplace/OrderBookList.tsx

Lines changed: 24 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { useMemo, useState, useEffect } from "react";
44
import { usePathname, useRouter, useSearchParams } from "next/navigation";
55
import { Order, OrderSide, OrderStatus } from "@/types";
66
import { Badge, Button, Modal } from "@/components/ui";
7-
import { ArrowUpDown, Filter, Search, Zap, Eye, X } from "lucide-react";
7+
import { Search, Zap, X } from "lucide-react";
88
import { clsx } from "clsx";
99
import { useUnifiedWallet } from "@/components/wallet/UnifiedWalletProvider";
10+
import { OrderListTable, OrderSortKey } from "@/components/marketplace/OrderListTable";
1011

1112
interface OrderBookListProps {
1213
orders: Order[];
@@ -95,7 +96,7 @@ export function OrderBookList({ orders, onTakeOrder }: OrderBookListProps) {
9596
const [assetFilter, setAssetFilter] = useState(() => searchParams.get("asset") ?? "all");
9697
const [detailsOrder, setDetailsOrder] = useState<Order | null>(null);
9798
const [sortConfig, setSortConfig] = useState<{
98-
key: "price" | "amount" | "timestamp";
99+
key: OrderSortKey;
99100
direction: "asc" | "desc";
100101
}>({ key: "timestamp", direction: "desc" });
101102

@@ -140,10 +141,6 @@ export function OrderBookList({ orders, onTakeOrder }: OrderBookListProps) {
140141
setAssetFilter(searchParams.get("asset") ?? "all");
141142
}, [searchParams]);
142143

143-
function parseNumericValue(value: string) {
144-
return Number(value.replace(/,/g, "")) || 0;
145-
}
146-
147144
const filteredOrders = useMemo(() => {
148145
return orders
149146
.filter((o) => {
@@ -166,32 +163,14 @@ export function OrderBookList({ orders, onTakeOrder }: OrderBookListProps) {
166163
o.status === OrderStatus.OPEN &&
167164
notExpired
168165
);
169-
})
170-
.sort((a, b) => {
171-
const direction = sortConfig.direction === "asc" ? 1 : -1;
172-
173-
if (sortConfig.key === "timestamp") {
174-
return (
175-
(new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) * direction
176-
);
177-
}
178-
179-
if (sortConfig.key === "price") {
180-
return (parseNumericValue(a.price) - parseNumericValue(b.price)) * direction;
181-
}
182-
183-
return (parseNumericValue(a.amount) - parseNumericValue(b.amount)) * direction;
184166
});
185-
}, [assetFilter, chainPairFilter, orders, search, sideFilter, sortConfig]);
186-
187-
const handleSort = (key: "price" | "amount" | "timestamp") => {
188-
let direction: "asc" | "desc" = "desc";
189-
if (sortConfig.key === key && sortConfig.direction === "desc") {
190-
direction = "asc";
191-
} else if (sortConfig.key === key && sortConfig.direction === "asc") {
192-
direction = "desc";
193-
}
194-
setSortConfig({ key, direction });
167+
}, [assetFilter, chainPairFilter, orders, search, sideFilter]);
168+
169+
const handleSort = (key: OrderSortKey) => {
170+
setSortConfig((current) => {
171+
if (current.key !== key) return { key, direction: "desc" };
172+
return { key, direction: current.direction === "desc" ? "asc" : "desc" };
173+
});
195174
};
196175

197176
const resetFilters = () => {
@@ -241,6 +220,7 @@ export function OrderBookList({ orders, onTakeOrder }: OrderBookListProps) {
241220
Sells
242221
</Button>
243222
<select
223+
aria-label="Filter by chain route"
244224
value={chainPairFilter}
245225
onChange={(event) => setChainPairFilter(event.target.value)}
246226
className="h-8 rounded-xl border border-border bg-surface-raised px-3 text-xs text-text-primary"
@@ -253,6 +233,7 @@ export function OrderBookList({ orders, onTakeOrder }: OrderBookListProps) {
253233
))}
254234
</select>
255235
<select
236+
aria-label="Filter by asset"
256237
value={assetFilter}
257238
onChange={(event) => setAssetFilter(event.target.value)}
258239
className="h-8 rounded-xl border border-border bg-surface-raised px-3 text-xs text-text-primary"
@@ -278,130 +259,18 @@ export function OrderBookList({ orders, onTakeOrder }: OrderBookListProps) {
278259
</div>
279260
</div>
280261

281-
<div className="rounded-2xl border border-border bg-background/50 overflow-hidden backdrop-blur-sm shadow-xl">
282-
<div className="overflow-x-auto">
283-
<table className="w-full text-left border-collapse">
284-
<thead>
285-
<tr className="bg-surface-overlay/50 border-b border-border">
286-
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-text-muted">
287-
Pair
288-
</th>
289-
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-text-muted">
290-
Side
291-
</th>
292-
<th
293-
className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-text-muted cursor-pointer hover:text-text-primary"
294-
onClick={() => handleSort("amount")}
295-
>
296-
<div className="flex items-center gap-2">
297-
Amount <ArrowUpDown size={12} />
298-
</div>
299-
</th>
300-
<th
301-
className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-text-muted cursor-pointer hover:text-text-primary"
302-
onClick={() => handleSort("price")}
303-
>
304-
<div className="flex items-center gap-2">
305-
Price <ArrowUpDown size={12} />
306-
</div>
307-
</th>
308-
<th
309-
className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-text-muted cursor-pointer hover:text-text-primary"
310-
onClick={() => handleSort("timestamp")}
311-
>
312-
<div className="flex items-center gap-2">
313-
Created <ArrowUpDown size={12} />
314-
</div>
315-
</th>
316-
<th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-text-muted">
317-
Total
318-
</th>
319-
<th className="px-4 py-4"></th>
320-
</tr>
321-
</thead>
322-
<tbody className="divide-y divide-border/50">
323-
{filteredOrders.length > 0 ? (
324-
filteredOrders.map((order) => (
325-
<tr
326-
key={order.id}
327-
className="group hover:bg-surface-overlay/30 transition-colors"
328-
>
329-
<td className="px-6 py-4 whitespace-nowrap">
330-
<div className="flex flex-col">
331-
<span className="font-bold text-text-primary">{order.pair}</span>
332-
<span className="text-[10px] text-text-muted uppercase tracking-tighter">
333-
{order.chainIn}{order.chainOut}
334-
</span>
335-
</div>
336-
</td>
337-
<td className="px-6 py-4 whitespace-nowrap">
338-
<Badge variant={order.side === OrderSide.BUY ? "success" : "error"}>
339-
{order.side.toUpperCase()}
340-
</Badge>
341-
</td>
342-
<td className="px-6 py-4 whitespace-nowrap font-mono text-sm text-text-primary">
343-
{order.amount} {order.tokenIn}
344-
</td>
345-
<td className="px-6 py-4 whitespace-nowrap font-mono text-sm text-text-secondary">
346-
{order.price}
347-
</td>
348-
<td className="px-6 py-4 whitespace-nowrap text-xs text-text-secondary">
349-
{new Date(order.timestamp).toLocaleString([], {
350-
dateStyle: "short",
351-
timeStyle: "short",
352-
})}
353-
</td>
354-
<td className="px-6 py-4 whitespace-nowrap font-mono text-sm font-bold text-text-primary">
355-
{order.total} {order.tokenOut}
356-
</td>
357-
<td className="px-4 py-4 text-right">
358-
<div className="flex justify-end gap-2">
359-
<Button
360-
variant="secondary"
361-
size="sm"
362-
onClick={() => setDetailsOrder(order)}
363-
icon={<Eye size={14} />}
364-
>
365-
Details
366-
</Button>
367-
<Button
368-
variant="primary"
369-
size="sm"
370-
className="shadow-glow-sm hover:shadow-glow-md"
371-
onClick={() => onTakeOrder(order)}
372-
icon={<Zap size={14} />}
373-
>
374-
Take
375-
</Button>
376-
</div>
377-
</td>
378-
</tr>
379-
))
380-
) : (
381-
<tr>
382-
<td colSpan={7} className="px-6 py-20 text-center">
383-
<div className="flex flex-col items-center gap-3">
384-
<div className="h-12 w-12 rounded-full bg-surface-overlay flex items-center justify-center border border-border">
385-
<Filter className="text-text-muted" />
386-
</div>
387-
<p className="text-text-secondary font-medium">
388-
No active orders matching filters.
389-
</p>
390-
<Button
391-
variant="ghost"
392-
size="sm"
393-
onClick={resetFilters}
394-
>
395-
Clear Filters
396-
</Button>
397-
</div>
398-
</td>
399-
</tr>
400-
)}
401-
</tbody>
402-
</table>
403-
</div>
404-
</div>
262+
<OrderListTable
263+
orders={filteredOrders}
264+
sortKey={sortConfig.key}
265+
sortDirection={sortConfig.direction}
266+
onSort={handleSort}
267+
onTakeOrder={onTakeOrder}
268+
onViewDetails={setDetailsOrder}
269+
onClearFilters={hasActiveFilters ? resetFilters : undefined}
270+
emptyTitle="No active orders matching filters."
271+
emptyDescription="Adjust filters or clear them to browse all open orders."
272+
pageSize={8}
273+
/>
405274

406275
<OrderDetailsModal
407276
order={detailsOrder}

0 commit comments

Comments
 (0)