TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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