- Header: shrink-0 logo + whitespace-nowrap + gap, nav shrinks/scrolls so it can never collide with (or glow over) the SkateSpots wordmark; tighter item padding. Fixes the top-left logo/nav overlap. - Amber: useReadState keeps read markers LOCAL-ONLY for interactive signers, so merely opening the bell/DM no longer publishes (encrypt + sign = 2 popups). - Reputation: lead with verifiedCounterparties (distinct people), cap each partner at 3 in the WoT-weighted score, warn when every confirmation is with one account. NIP.md documents the collusion limit honestly. - Directions: skip the live GPS call when the cached fix is <2 min old; the blocking 'Finding your location' overlay only shows with no seed (silent refine chip otherwise). Near Me also flips the sort to proximity. - .gitignore: cvm-server/package.json (local dev copy of package.cvm.json).
83 lines
3.7 KiB
TypeScript
83 lines
3.7 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { useTradeReputation } from '@/hooks/useTradeAttestations';
|
|
import { useBatchTrustScores } from '@/hooks/useBatchTrustScores';
|
|
import { cn } from '@/lib/utils';
|
|
import { Handshake } from 'lucide-react';
|
|
|
|
function Stat({ label, value, accent }: { label: string; value: number; accent?: boolean }) {
|
|
return (
|
|
<div className="rounded-lg border border-white/10 bg-white/[0.03] px-2 py-3">
|
|
<p className={cn('text-xl font-black', accent ? 'text-[#ccff00]' : 'text-white')}>{value}</p>
|
|
<p className="text-[10px] uppercase tracking-wide text-gray-500">{label}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Marketplace reputation for a profile, derived from mutually-signed trade
|
|
* attestations (kind 31871, see NIP.md → Trade Attestations).
|
|
*
|
|
* "Verified" is the strong signal: the seller AND the counterparty both signed
|
|
* an attestation for the same listing. Renders nothing when there are no trades.
|
|
*/
|
|
export function TradeReputationCard({ pubkey, className }: { pubkey: string; className?: string }) {
|
|
const { data } = useTradeReputation(pubkey);
|
|
const verifiedWith = data?.verifiedWith ?? [];
|
|
// Trust-weight each mutually-confirmed trade by the counterparty's web-of-trust
|
|
// score, so a ring of fresh accounts can't inflate reputation.
|
|
const { data: scores } = useBatchTrustScores(verifiedWith.map((v) => v.pubkey));
|
|
|
|
const total = (data?.sales ?? 0) + (data?.purchases ?? 0);
|
|
if (!data || total === 0) return null;
|
|
|
|
const people = data.verifiedCounterparties;
|
|
// Cap each counterparty's contribution at 3 so one partner — or a colluding
|
|
// pair confirming each other over and over — can't inflate the score.
|
|
const weighted = verifiedWith.reduce(
|
|
(sum, v) => sum + Math.min(v.count, 3) * ((scores?.get(v.pubkey) ?? 0) / 100),
|
|
0,
|
|
);
|
|
const trustLabel =
|
|
weighted >= 5 ? 'Highly trusted trader' : weighted >= 1 ? 'Building reputation' : 'New trader';
|
|
// The classic fake-reputation pattern: several "trades", all with one account.
|
|
const singlePartnerWarning = data.verified >= 3 && people <= 1;
|
|
|
|
return (
|
|
<Card className={cn('bg-black/90 border-2 border-[#ff6600]/25', className)}>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-lg text-[#ff6600]">
|
|
<Handshake className="h-5 w-5" />
|
|
Trades
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div className="grid grid-cols-3 gap-2 text-center">
|
|
<Stat label="Verified" value={data.verified} accent />
|
|
<Stat label="Sales" value={data.sales} />
|
|
<Stat label="Purchases" value={data.purchases} />
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
{data.verified > 0
|
|
? `${data.verified} mutually-confirmed ${data.verified === 1 ? 'trade' : 'trades'} with ${people} ${people === 1 ? 'person' : 'people'}`
|
|
: 'Awaiting mutual confirmation'}
|
|
</p>
|
|
{singlePartnerWarning && (
|
|
<p className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-xs text-amber-300">
|
|
⚠ Every confirmation is with the same account. Mutual confirmation is
|
|
self-reported, so treat this record with care.
|
|
</p>
|
|
)}
|
|
{data.verified > 0 && (
|
|
<p className="text-xs text-gray-500">
|
|
Trust-weighted <span className="font-bold text-[#ccff00]">{weighted.toFixed(1)}</span>
|
|
{' · '}
|
|
{trustLabel}
|
|
</p>
|
|
)}
|
|
<p className="text-[10px] leading-tight text-gray-600">
|
|
Self-reported and mutually signed — a signal, not proof of payment.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|