1
0
Fork 0
polaris-task-force/src/components/frontend/market/Countdown.tsx
Z8MB1E 89b8474e23 feat(market): add trading marketplace with listings, haggling, and NPC vendors
Tarkov-style flea market where users list locker items at a fixed price
and buyers pay through the banking system; items transfer directly
into the buyer's locker.

- market-listings + market-negotiations collections and the market page
  with search/rarity filters and a Countdown timer on each card
- service lib (buyListing with partial-quantity + negotiated-price
  support, clean-stock rule, creditLockerQuantity, auto pricing)
- negotiation flow: make/accept/reject/counter/withdraw, strictly-
  increasing buyer offers, NPC vendor auto-resolution
- NPC vendors: game-npcs attribution, patience meter, dialogue, chat
  bubbles, historical threads dialog
- 30d user / 7d auto listing durations, market-tick bin for expiry +
  vendor restock, SSE notify on tick
- market event types + game-npcs/market target collections in the
  event log; sidebar Market entry under Logistics
- partial-quantity purchases (buyListing/buyMarketListing/makeMarketOffer)
  with full int test coverage (25 tests)
2026-08-10 16:05:57 -04:00

50 lines
1.3 KiB
TypeScript

"use client";
import { ClockIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { useNow } from "@/hooks/useNow";
function formatCountdown(ms: number): string {
if (ms <= 0) return "Expired";
const totalSeconds = Math.floor(ms / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (days > 0) {
return `${days}d ${hours}h`;
}
if (hours > 0) {
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
}
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
interface CountdownProps {
expiresAt: string | null | undefined;
className?: string;
}
export function Countdown({ expiresAt, className }: CountdownProps) {
const now = useNow();
if (!expiresAt) return null;
const ms = new Date(expiresAt).getTime() - now;
const urgent = ms > 0 && ms < 60 * 60 * 1000;
return (
<span
className={cn(
"inline-flex items-center gap-1 font-mono text-xs",
ms <= 0 ? "text-red-400" : urgent ? "text-amber-400" : "text-muted-foreground",
className,
)}
title={new Date(expiresAt).toLocaleString()}
>
<ClockIcon className="size-3" />
{formatCountdown(ms)}
</span>
);
}