SkateSpots/src/components/DeckRating.tsx
SkateSpots efa8b2e521 feat(marketplace): 1-10 deck ratings + seller dashboard
- Reviews: confirming a trade now lets the reviewer set a 1-10 skateboard-deck
  rating, stored as ["rating","n"] on the kind-31871 attestation (NIP.md updated).
  The Reputation tab shows each review's decks plus an average.
- New DeckRating component (self-drawn deck glyph) and TradeConfirmRow (rating
  picker + confirm), used for both seller-confirm and buyer-confirm in My Trades.
- New 'Selling' tab: your marketplace listings (active/sold) with one-tap
  Mark sold.
2026-09-27 16:26:24 +07:00

63 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { cn } from '@/lib/utils';
/** A little skateboard-deck glyph for the rating. */
function Deck({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 10" className={className} fill="currentColor" aria-hidden>
<rect x="1" y="2.2" width="22" height="3.6" rx="1.8" />
<circle cx="7" cy="7.6" r="1.4" />
<circle cx="17" cy="7.6" r="1.4" />
</svg>
);
}
interface DeckRatingProps {
/** Current rating, 0 = none, 1–10 = decks. */
value: number;
/** Omit for a read-only display. */
onChange?: (value: number) => void;
/** `sm` for inline rows, `md` for dialogs. */
size?: 'sm' | 'md';
/** When true, only render the filled decks (for display). */
compact?: boolean;
className?: string;
}
/**
* A 1–10 skateboard-deck rating. 1 deck = bad, 10 decks = best-in-class.
* Interactive by default; read-only when no `onChange` is given.
*/
export function DeckRating({ value, onChange, size = 'md', compact = false, className }: DeckRatingProps) {
const readOnly = !onChange;
const count = readOnly && compact ? Math.max(value, 0) : 10;
const deckCls = size === 'sm' ? 'h-3 w-6' : 'h-3.5 w-7';
return (
<span className={cn('inline-flex items-center gap-0.5', className)} title={`${value}/10 decks`}>
{Array.from({ length: count }).map((_, i) => {
const n = i + 1;
const filled = n <= value;
if (readOnly) {
return <Deck key={n} className={cn(deckCls, 'text-[#ccff00]')} />;
}
return (
<button
key={n}
type="button"
onClick={() => onChange?.(n === value ? 0 : n)}
aria-label={`${n} of 10`}
aria-pressed={filled}
className={cn(deckCls, 'transition-transform hover:scale-110 active:scale-95', filled ? 'text-[#ccff00]' : 'text-gray-600')}
>
<Deck className="h-full w-full" />
</button>
);
})}
{(value > 0 || !readOnly) && (
<span className="ml-1 text-[11px] font-bold text-gray-400 tabular-nums">
{value > 0 ? `${value}/10` : ''}
</span>
)}
</span>
);
}