← back to Govarbitrage

src/app/listings/[id]/page.tsx

420 lines

import { notFound } from "next/navigation";
import Link from "next/link";
import { getGatedListingDetail } from "@/lib/listing-detail";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { formatMoney, formatPercent, countdown } from "@/lib/utils";
import { auctionNetworkLinks, boardLinks, internationalNetworkLinks } from "@/lib/marketplace-links";

export const dynamic = "force-dynamic";

const n = (d: unknown): number => (d == null ? 0 : Number(d));

export default async function ListingDetail({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const { listing, gated } = await getGatedListingDetail(id);
  if (!listing) notFound();

  const r = listing.research;
  const c = listing.costBreakdown;

  const valuationRows: [string, number | null | undefined][] = [
    ["New Retail", n(r?.newRetail)],
    ["New Replacement", n(r?.newReplacement)],
    ["Avg Retail", n(r?.avgRetail)],
    ["Used Sold", n(r?.usedSoldPrice)],
    ["Used Asking", n(r?.usedAskingPrice)],
    ["Wholesale", n(r?.wholesaleValue)],
    ["Liquidation", n(r?.liquidationValue)],
    ["Sell Today", n(r?.sellTodayValue)],
    ["7-Day Value", n(r?.value7Day)],
    ["30-Day Value", n(r?.value30Day)],
    ["90-Day Value", n(r?.value90Day)],
    ["Expected Sale", n(r?.expectedSalePrice)],
  ];

  const costRows: [string, number][] = c
    ? [
        ["Winning Bid", n(c.winningBid)],
        ["Buyer Premium", n(c.buyerPremium)],
        ["Sales Tax", n(c.salesTax)],
        ["Shipping", n(c.shipping)],
        ["Freight", n(c.freight)],
        ["Insurance", n(c.insurance)],
        ["Packing", n(c.packing)],
        ["Pickup Labor", n(c.pickupLabor)],
        ["Testing", n(c.testing)],
        ["Repairs", n(c.repairs)],
        ["Certification", n(c.certification)],
        ["Marketplace Fees", n(c.marketplaceFees)],
        ["Payment Fees", n(c.paymentFees)],
        ["Storage", n(c.storage)],
        ["Photography", n(c.photography)],
        ["Listing Labor", n(c.listingLabor)],
      ]
    : [];

  return (
    <main className="mx-auto max-w-6xl space-y-5 p-5">
      <div className="flex items-center gap-3 text-sm">
        <Link href="/" className="text-primary hover:underline">
          ← Dashboard
        </Link>
        <Badge variant="primary">{listing.source.replace(/_/g, " ")}</Badge>
        <span className="text-muted-foreground">#{listing.sourceAuctionId}</span>
        {listing.sourceUrl && (
          <a href={listing.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
            View source ↗
          </a>
        )}
      </div>

      <h1 className="text-2xl font-semibold tracking-tight">{listing.title}</h1>

      {/* Gallery */}
      <div className="flex gap-3 overflow-x-auto">
        {listing.imageUrls.length ? (
          listing.imageUrls.map((u) => (
            // eslint-disable-next-line @next/next/no-img-element
            <img key={u} src={u} alt={listing.title} className="h-48 w-72 flex-shrink-0 rounded-lg border object-cover" />
          ))
        ) : (
          <div className="flex h-48 w-72 items-center justify-center rounded-lg border text-muted-foreground">
            No images
          </div>
        )}
      </div>

      {/* Headline metrics */}
      <div className="grid grid-cols-2 gap-3 md:grid-cols-5">
        <Metric label="Current Bid" value={formatMoney(n(listing.currentBid))} />
        <MetricOrGated label="Rec. Max Bid" value={formatMoney(n(c?.recommendedMaxBid))} gatedPlaceholder="$9,999" gated={gated} accent />
        <MetricOrGated label="Expected Net" value={formatMoney(n(c?.expectedNetProfit))} gatedPlaceholder="$9,999" gated={gated} accent />
        <MetricOrGated label="ROI" value={formatPercent(n(c?.roi))} gatedPlaceholder="$9,999" gated={gated} accent />
        <Metric label="Closes" value={countdown(listing.closingAt)} />
      </div>

      {gated && (
        <Card>
          <CardContent className="flex flex-wrap items-center justify-between gap-3 py-4 text-sm">
            <span>
              The money math — recommended max bid, valuation ladder, full cost breakdown,
              expected net profit, comparables, and buyer leads — is part of the paid tiers.
            </span>
            <Link href="/pricing" className="rounded-md bg-primary px-3 py-1.5 font-medium text-primary-foreground">
              See plans →
            </Link>
          </CardContent>
        </Card>
      )}

      {!gated && (
      <div className="grid gap-5 md:grid-cols-2">
        {/* Valuation ladder */}
        <Card>
          <CardHeader>
            <CardTitle>Valuation</CardTitle>
          </CardHeader>
          <CardContent className="space-y-1">
            {valuationRows.map(([label, val]) => (
              <Row key={label} label={label} value={formatMoney(val)} />
            ))}
            <Row label="Probability of Sale" value={formatPercent(n(r?.probabilityOfSale))} />
            <Row label="Days Until Sold" value={`${r?.daysUntilSold ?? "—"}`} />
            <Row label="Confidence" value={`${Math.round(n(r?.confidenceScore))}/100`} />
          </CardContent>
        </Card>

        {/* Cost breakdown */}
        <Card>
          <CardHeader>
            <CardTitle>Cost Breakdown &amp; Profit</CardTitle>
          </CardHeader>
          <CardContent className="space-y-1">
            {costRows.map(([label, val]) => (
              <Row key={label} label={label} value={formatMoney(val)} />
            ))}
            <div className="my-2 border-t" />
            <Row label="Total Investment" value={formatMoney(n(c?.totalInvestment))} strong />
            <Row label="Expected Returns" value={formatMoney(n(c?.expectedReturns))} strong />
            <Row label="Expected Net Profit" value={formatMoney(n(c?.expectedNetProfit))} strong />
            <Row label="ROI" value={formatPercent(n(c?.roi))} strong />
            <Row label="Annualized" value={formatPercent(n(c?.annualizedReturn))} />
          </CardContent>
        </Card>
      </div>
      )}

      {/* Score explanations */}
      <Card>
        <CardHeader>
          <CardTitle>Scoring — why each profile scored this listing</CardTitle>
        </CardHeader>
        <CardContent className="grid gap-3 md:grid-cols-3">
          {listing.scores.map((s) => (
            <div key={s.id} className="rounded-lg border p-3">
              <div className="mb-1 flex items-center justify-between">
                <span className="text-sm font-medium">{s.profile.replace(/_/g, " ")}</span>
                <Badge variant={s.value >= 70 ? "positive" : s.value >= 50 ? "warning" : "outline"}>
                  {Math.round(s.value)}
                </Badge>
              </div>
              <div className="mt-1.5 h-1 w-full rounded-full bg-muted">
                <div
                  className="h-1 rounded-full"
                  style={{
                    width: `${Math.max(0, Math.min(100, s.value))}%`,
                    background:
                      s.value >= 70
                        ? "var(--positive)"
                        : s.value >= 50
                          ? "var(--warning)"
                          : "var(--negative)",
                  }}
                />
              </div>
              <p className="text-xs text-muted-foreground">{s.explanation}</p>
            </div>
          ))}
        </CardContent>
      </Card>

      <div className="grid gap-5 md:grid-cols-2">
        {/* Comparables */}
        <Card>
          <CardHeader>
            <CardTitle>Comparable Sales &amp; Listings</CardTitle>
          </CardHeader>
          <CardContent className="space-y-1">
            {listing.comparables.length === 0 && <p className="text-sm text-muted-foreground">None.</p>}
            {listing.comparables.map((cp) => (
              <div key={cp.id} className="flex items-center justify-between text-sm">
                <span className="flex items-center gap-2">
                  <Badge variant="outline">{cp.kind}</Badge>
                  <span className="truncate">{cp.title}</span>
                </span>
                <span className="tabular-nums">{formatMoney(n(cp.price))}</span>
              </div>
            ))}
          </CardContent>
        </Card>

        {/* Terms + risk */}
        <Card>
          <CardHeader>
            <CardTitle>Auction Terms &amp; Risk</CardTitle>
          </CardHeader>
          <CardContent className="space-y-2 text-sm">
            <div>
              <span className="text-muted-foreground">Condition: </span>
              {listing.condition.replace(/_/g, " ")} · Qty {listing.quantity} · {n(listing.weightLbs)} lb
            </div>
            <div>
              <span className="text-muted-foreground">Location: </span>
              {[listing.locationCity, listing.locationState, listing.locationZip].filter(Boolean).join(", ") || "—"}
            </div>
            <p className="text-muted-foreground">{listing.auctionTerms}</p>
            {r?.summary && <p className="rounded-md bg-muted p-2 text-xs">{r.summary}</p>}
          </CardContent>
        </Card>
      </div>

      {/* Buyer workflow (compliant) */}
      <Card>
        <CardHeader>
          <CardTitle>Buyer Interest (contingent — no ownership represented pre-win)</CardTitle>
        </CardHeader>
        <CardContent className="space-y-2 text-sm">
          {listing.buyerPage ? (
            <div>
              Interest page: <span className="font-medium">/{listing.buyerPage.slug}</span> ·{" "}
              {listing.buyerPage.published ? <Badge variant="positive">Published</Badge> : <Badge variant="outline">Draft</Badge>} ·
              est. delivered {formatMoney(n(listing.buyerPage.estDelivered))}
            </div>
          ) : (
            <p className="text-muted-foreground">No buyer-interest page yet.</p>
          )}
          {listing.buyerLeads.map((l) => (
            <div key={l.id} className="flex items-center justify-between rounded-md border p-2">
              <span>
                {l.name} — {l.email} {l.contingent && <Badge variant="outline">contingent</Badge>}
              </span>
              <span className="tabular-nums">{formatMoney(n(l.offer))}</span>
            </div>
          ))}
        </CardContent>
      </Card>

      {/* CRE agents — deep-link to /agents pre-filled with this listing's location */}
      {(listing.locationCity || listing.locationState || listing.locationZip) && (
        <Card>
          <CardHeader>
            <CardTitle>Find Commercial Real Estate Agents Near This Property</CardTitle>
          </CardHeader>
          <CardContent className="space-y-2 text-sm">
            <p className="text-muted-foreground">
              Buyer / acquisition brokers and leasing agents sourced live from Google Places for{" "}
              {[listing.locationCity, listing.locationState].filter(Boolean).join(", ") || listing.locationZip}.
            </p>
            <div className="flex flex-wrap gap-2">
              <Link
                href={`/agents?${new URLSearchParams({
                  ...(listing.locationCity ? { city: listing.locationCity } : {}),
                  ...(listing.locationState ? { state: listing.locationState } : {}),
                  ...(listing.locationZip ? { zip: listing.locationZip } : {}),
                  type: "both",
                }).toString()}`}
                className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
              >
                Find agents (buyer + leasing) →
              </Link>
              <Link
                href={`/agents?${new URLSearchParams({
                  ...(listing.locationCity ? { city: listing.locationCity } : {}),
                  ...(listing.locationState ? { state: listing.locationState } : {}),
                  ...(listing.locationZip ? { zip: listing.locationZip } : {}),
                  type: "buyer",
                }).toString()}`}
                className="rounded-md border px-3 py-1.5 text-sm text-primary hover:bg-accent"
              >
                Buyer / acquisition only →
              </Link>
            </div>
          </CardContent>
        </Card>
      )}

      {/* Compare & source elsewhere */}
      <div className="grid gap-5 md:grid-cols-2">
        <Card>
          <CardHeader>
            <CardTitle>Actual offers on other networks</CardTitle>
          </CardHeader>
          <CardContent className="flex flex-wrap gap-2">
            {auctionNetworkLinks(listing).map((c) => (
              <a
                key={c.url}
                href={c.url}
                target="_blank"
                rel="noopener noreferrer"
                className="rounded-md border px-2 py-1 text-sm text-primary hover:bg-accent"
              >
                {c.label} ↗
              </a>
            ))}
          </CardContent>
        </Card>
        <Card>
          <CardHeader>
            <CardTitle>Goods-needed boards &amp; communities</CardTitle>
          </CardHeader>
          <CardContent className="flex flex-wrap gap-2">
            {boardLinks(listing).map((c) => (
              <a
                key={c.url}
                href={c.url}
                target="_blank"
                rel="noopener noreferrer"
                className="rounded-md border px-2 py-1 text-sm text-primary hover:bg-accent"
              >
                {c.label} ↗
              </a>
            ))}
          </CardContent>
        </Card>
      </div>

      {/* International offers */}
      <Card>
        <CardHeader>
          <CardTitle>International offers — Europe · Japan · HK · Australia · China</CardTitle>
        </CardHeader>
        <CardContent className="grid gap-3 md:grid-cols-5">
          {internationalNetworkLinks(listing).map((r) => (
            <div key={r.region}>
              <div className="mb-1 text-xs font-semibold text-muted-foreground">{r.region}</div>
              <div className="flex flex-col gap-1">
                {r.links.map((c) => (
                  <a
                    key={c.url}
                    href={c.url}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-sm text-primary hover:underline"
                  >
                    {c.label} ↗
                  </a>
                ))}
              </div>
            </div>
          ))}
        </CardContent>
      </Card>

      {/* History */}
      <Card>
        <CardHeader>
          <CardTitle>History</CardTitle>
        </CardHeader>
        <CardContent className="space-y-1 text-xs text-muted-foreground">
          {listing.events.map((e) => (
            <div key={e.id} className="flex justify-between">
              <span>{e.message}</span>
              <span>{new Date(e.createdAt).toLocaleString()}</span>
            </div>
          ))}
        </CardContent>
      </Card>
    </main>
  );
}

function Metric({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
  return (
    <Card>
      <CardContent className="p-3">
        <div className="text-xs text-muted-foreground">{label}</div>
        <div className={`text-lg font-semibold tabular-nums ${accent ? "text-primary" : ""}`}>{value}</div>
      </CardContent>
    </Card>
  );
}

function MetricOrGated({
  label,
  value,
  gatedPlaceholder,
  gated,
  accent,
}: {
  label: string;
  value: string;
  gatedPlaceholder: string;
  gated: boolean;
  accent?: boolean;
}) {
  return (
    <Card>
      <CardContent className="p-3">
        <div className="text-xs text-muted-foreground">{label}</div>
        <div className={`text-lg font-semibold tabular-nums ${accent ? "text-primary" : ""}`}>
          {gated ? (
            <span className="blur-sm select-none">{gatedPlaceholder}</span>
          ) : (
            value
          )}
        </div>
      </CardContent>
    </Card>
  );
}

function Row({ label, value, strong }: { label: string; value: string; strong?: boolean }) {
  return (
    <div className={`flex justify-between text-sm ${strong ? "font-semibold" : ""}`}>
      <span className="text-muted-foreground">{label}</span>
      <span className="tabular-nums">{value}</span>
    </div>
  );
}