SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.2 KB · 47 lines tsx
Raw Blame History
1import { imageProps } from '@/lib/images';2import { SmartImage } from '@/components/ui/smart-image';3import { glyphForFamily } from '@/lib/image-glyph';4import Link from 'next/link';5import type { MarketRow } from '@/lib/queries/markets';6import { fmtNum, cn } from '@/lib/format';7import { Delta } from '@/components/ui/primitives';8import { Sparkline } from '@/components/charts/sparkline';910/** Browse-by-category tiles: representative image, live counts, 30-day change and an optional sparkline. */11export function CategoryGrid({ rows, thumbs, className, limit, sparklines, mobileLimit }: { rows: MarketRow[]; thumbs: Map<string, string>; className?: string; limit?: number; sparklines?: Map<string, number[]>; /** hide tiles beyond this index below the sm breakpoint */ mobileLimit?: number }) {12  const sorted = [...rows].sort((a, b) => b.counts.sales - a.counts.sales || b.counts.assets - a.counts.assets || a.node.sortOrder - b.node.sortOrder);13  const shown = limit ? sorted.slice(0, limit) : sorted;14  return (15    <div className={cn('grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6', className)}>16      {shown.map(({ node, counts, snapshot }, idx) => {17        const hiddenOnMobile = mobileLimit !== undefined && idx >= mobileLimit;18        const empty = counts.assets === 0;19        const src = thumbs.get(node.slug);20        const spark = sparklines?.get(node.slug);21        return (22          <Link key={node.slug} href={`/markets/${node.slug}`} className={cn('card card-hover group flex flex-col overflow-hidden', empty && 'opacity-60', hiddenOnMobile && 'hidden sm:flex')}>23            <span className="relative block aspect-[5/3] w-full overflow-hidden bg-sunken">24              {(() => {25                const ip = imageProps(src, { maxWidth: 384 });26                return <SmartImage src={ip?.src ?? null} srcSet={ip?.srcSet ?? null} sizes="(min-width: 1280px) 220px, (min-width: 640px) 33vw, 50vw" alt="" imgClassName="p-3 transition-transform duration-300 ease-out group-hover:scale-[1.04]" placeholder={{ label: empty ? 'No data yet' : node.short ?? node.name, glyph: glyphForFamily(node.familySlug) }} />;27              })()}28              {spark && spark.length > 2 ? (29                <span className="absolute bottom-1.5 right-1.5 rounded-sm bg-elevated/80 px-1 backdrop-blur-sm">30                  <Sparkline values={spark} width={64} height={18} />31                </span>32              ) : null}33            </span>34            <span className="flex flex-col gap-0.5 px-2.5 py-2">35              <span className="flex items-center justify-between gap-2">36                <span className="truncate text-[13px] font-semibold text-fg">{node.name}</span>37                {snapshot?.change30d != null ? <Delta value={snapshot.change30d} className="text-[11px]" /> : null}38              </span>39              <span className="num truncate text-[11px] text-muted">{empty ? 'catalogue pending' : `${fmtNum(counts.assets, { compact: true })} assets${counts.sales ? ` · ${fmtNum(counts.sales, { compact: true })} sales` : counts.priced ? ` · ${fmtNum(counts.priced, { compact: true })} priced` : ''}`}</span>40            </span>41          </Link>42        );43      })}44    </div>45  );46}47