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>
87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
import type { SpotStatusValue } from '@/lib/types';
|
|
|
|
export interface SpotStatusMeta {
|
|
value: SpotStatusValue;
|
|
/** Short human label. */
|
|
label: string;
|
|
/** Emoji used in compact contexts. */
|
|
emoji: string;
|
|
/** One-line description for pickers. */
|
|
description: string;
|
|
/** Tailwind classes for badge text + background + border. */
|
|
badgeClass: string;
|
|
/** Hex dot color for map/legend use. */
|
|
dot: string;
|
|
}
|
|
|
|
export const SPOT_STATUS_META: Record<SpotStatusValue, SpotStatusMeta> = {
|
|
active: {
|
|
value: 'active',
|
|
label: 'Active',
|
|
emoji: '🟢',
|
|
description: 'Good to go — skate it.',
|
|
badgeClass: 'text-green-400 bg-green-500/10 border-green-500/30',
|
|
dot: '#22c55e',
|
|
},
|
|
bust: {
|
|
value: 'bust',
|
|
label: 'Bust',
|
|
emoji: '🟡',
|
|
description: 'Heavy kick-out risk — security or cops.',
|
|
badgeClass: 'text-yellow-400 bg-yellow-500/10 border-yellow-500/30',
|
|
dot: '#eab308',
|
|
},
|
|
skate_stopped: {
|
|
value: 'skate_stopped',
|
|
label: 'Skate-stopped',
|
|
emoji: '🟠',
|
|
description: 'Knobbed / capped, but partially skateable.',
|
|
badgeClass: 'text-orange-400 bg-orange-500/10 border-orange-500/30',
|
|
dot: '#f97316',
|
|
},
|
|
closed: {
|
|
value: 'closed',
|
|
label: 'Closed',
|
|
emoji: '⚪',
|
|
description: 'Temporarily closed — construction or event.',
|
|
badgeClass: 'text-gray-300 bg-gray-500/10 border-gray-500/30',
|
|
dot: '#9ca3af',
|
|
},
|
|
gone: {
|
|
value: 'gone',
|
|
label: 'Gone',
|
|
emoji: '🔴',
|
|
description: 'Demolished, paved over, or fenced off for good.',
|
|
badgeClass: 'text-red-400 bg-red-500/10 border-red-500/40',
|
|
dot: '#ef4444',
|
|
},
|
|
};
|
|
|
|
/** Ordered list for pickers/legends (best → worst). */
|
|
export const SPOT_STATUS_ORDER: SpotStatusValue[] = [
|
|
'active',
|
|
'bust',
|
|
'skate_stopped',
|
|
'closed',
|
|
'gone',
|
|
];
|
|
|
|
/** Human "time ago" for a unix-seconds timestamp. */
|
|
export function statusTimeAgo(unixSeconds: number): string {
|
|
const diff = Math.max(0, Math.floor(Date.now() / 1000) - unixSeconds);
|
|
const days = Math.floor(diff / 86400);
|
|
if (days >= 365) {
|
|
const y = Math.floor(days / 365);
|
|
return `${y}y ago`;
|
|
}
|
|
if (days >= 30) {
|
|
const m = Math.floor(days / 30);
|
|
return `${m}mo ago`;
|
|
}
|
|
if (days >= 1) return `${days}d ago`;
|
|
const hours = Math.floor(diff / 3600);
|
|
if (hours >= 1) return `${hours}h ago`;
|
|
const mins = Math.floor(diff / 60);
|
|
if (mins >= 1) return `${mins}m ago`;
|
|
return 'just now';
|
|
}
|