Solves the "spots that change appearance or disappear" problem with crowd-sourced, trust-weighted status reports — since the spot owner often isn't the one who notices a spot got knobbed/demolished/busted. New custom kind 2259 (regular, append-only): any user can report a spot's current physical state — active / bust / skate-stopped / closed / gone — with an optional note, referencing the spot via its `a` coordinate. Client aggregation (useSpotStatus / useBatchSpotStatus): - recency-weighted (30-day half-life) so fresh reports dominate and a rebuilt spot naturally recovers - trust-weighted via existing useBatchTrustScores (0.2–1.0 multiplier) - most-recent-report-per-reporter (no ballot stuffing) - reports older than 180 days ignored UI: - SpotStatusBadge (colored consensus badge + tooltip w/ report count) - ReportSpotStatusDialog (status picker, optional note, recent reports) - Badge + Report Status button on SpotDetailPage - Batched status badge on SpotCard; "gone" spots dimmed/grayscaled - "gone" spots hidden from map markers (still listed dimmed) to keep the map uncluttered while preserving the record Documented the kind, tags, status values, and aggregation rules in NIP.md. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
85 lines
2.4 KiB
TypeScript
85 lines
2.4 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import { SPOT_STATUS_META, statusTimeAgo } from '@/lib/spotStatus';
|
|
import type { AggregatedSpotStatus } from '@/hooks/useSpotStatus';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
|
|
interface SpotStatusBadgeProps {
|
|
aggregated: AggregatedSpotStatus | null | undefined;
|
|
/** Compact = dot + label only, no meta. */
|
|
size?: 'sm' | 'md';
|
|
className?: string;
|
|
/** Hide the badge entirely when there are no reports (default true). */
|
|
hideWhenEmpty?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Displays the community-consensus status for a spot.
|
|
* - No reports → renders nothing (unless hideWhenEmpty=false, then "Unverified").
|
|
* - Otherwise → colored badge with the winning status + reporter count tooltip.
|
|
*/
|
|
export function SpotStatusBadge({
|
|
aggregated,
|
|
size = 'md',
|
|
className,
|
|
hideWhenEmpty = true,
|
|
}: SpotStatusBadgeProps) {
|
|
const status = aggregated?.status ?? null;
|
|
|
|
if (!status) {
|
|
if (hideWhenEmpty) return null;
|
|
return (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium',
|
|
'text-muted-foreground bg-muted/40 border-border',
|
|
className,
|
|
)}
|
|
>
|
|
Unverified
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const meta = SPOT_STATUS_META[status];
|
|
const reporterCount = aggregated?.reporterCount ?? 0;
|
|
const lastAt = aggregated?.lastReportedAt ?? null;
|
|
|
|
const badge = (
|
|
<span
|
|
className={cn(
|
|
'inline-flex items-center gap-1 rounded-full border font-semibold',
|
|
meta.badgeClass,
|
|
size === 'sm' ? 'px-1.5 py-0.5 text-[10px]' : 'px-2 py-0.5 text-xs',
|
|
className,
|
|
)}
|
|
>
|
|
<span
|
|
className="inline-block rounded-full"
|
|
style={{
|
|
backgroundColor: meta.dot,
|
|
width: size === 'sm' ? 6 : 8,
|
|
height: size === 'sm' ? 6 : 8,
|
|
}}
|
|
/>
|
|
{meta.label}
|
|
</span>
|
|
);
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{badge}</TooltipTrigger>
|
|
<TooltipContent className="max-w-56">
|
|
<p className="font-medium">{meta.label}</p>
|
|
<p className="text-xs text-muted-foreground">{meta.description}</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{reporterCount} {reporterCount === 1 ? 'report' : 'reports'}
|
|
{lastAt ? ` · last ${statusTimeAgo(lastAt)}` : ''}
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|