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)
50 lines
1.3 KiB
TypeScript
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>
|
|
);
|
|
}
|