Mikkel Manniche
Dansk English Deutsch

Vorlage · React · HTML

Gallery shop

Eine Galerie- und Shopseite in Tiles: ein Seitenmenü, eine zweifarbige Überschrift, Zähler, Veranstaltungen und Kollektionen mit optionalen Bildern.

Vorschau

React

Installiere die Vorlage mit shadcn. Sie landet als normale Komponenten in deinem Projekt, die du selbst anpasst.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/gallery-shop.json
npx shadcn@latest add @manniche/gallery-shop
gallery-shop.tsx
// A gallery shop screen: vertical side menu, a two-tone headline, counters, events and collections.
// Each collection takes an optional image; without one it is drawn as a colour tile, so the demo carries no third-party media.
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { BigNumber } from '@/registry/manniche/chart-kit/chart-kit'
import { DataTile, TileFact } from '@/registry/manniche/data-tile/data-tile'

export type GalleryShopData = {
  menu: { id: string; label: string }[]
  headline: { strong: string; soft: string }
  counters: { id: string; title: string; value: number }[]
  events: { title: string; items: { id: string; date: string; title: string; place: string }[] }
  collections: { title: string; items: { id: string; name: string; works: number; color: string; /** Cover image URL. Falls back to the colour tile. */ image?: string }[] }
}

export type GalleryShopLabels = { note?: string; menuLabel?: string; works?: (n: number) => string }

export type GalleryShopProps = { data: GalleryShopData; labels?: GalleryShopLabels; className?: string }

export function GalleryShop({ data, labels, className }: GalleryShopProps) {
  const note = labels?.note ?? 'Example data.'
  const works = labels?.works ?? ((n: number) => `${n} works`)
  const [current, setCurrent] = useState(data.menu[0]?.id)
  return (
    <div className={cn('@container w-full rounded-2xl bg-background p-3 text-foreground', className)}>
      <div className="flex flex-col gap-3 @lg:flex-row">
        <nav aria-label={labels?.menuLabel ?? 'Gallery'} className="flex shrink-0 gap-1 overflow-x-auto @lg:w-40 @lg:flex-col">
          {data.menu.map((m) => (
            <button
              key={m.id}
              type="button"
              aria-current={m.id === current ? 'page' : undefined}
              onClick={() => setCurrent(m.id)}
              className={cn('shrink-0 rounded-xl px-3 py-2 text-left text-sm text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring', m.id === current && 'bg-foreground text-background hover:bg-foreground hover:text-background')}
            >
              {m.label}
            </button>
          ))}
        </nav>
        <div className="grid min-w-0 flex-1 grid-cols-1 gap-3 @2xl:grid-cols-6">
          <h2 className="px-1 text-[clamp(28px,4vw,48px)] font-semibold leading-tight tracking-tight @2xl:col-span-6">
            {data.headline.strong} <span className="text-muted-foreground">{data.headline.soft}</span>
          </h2>
          {data.counters.map((c) => (
            <div key={c.id} className="@2xl:col-span-2">
              <DataTile className="h-full" title={c.title} density="compact">
                <TileFact>
                  <BigNumber value={c.value} size="lg" />
                </TileFact>
              </DataTile>
            </div>
          ))}
          <div className="@2xl:col-span-3">
            <DataTile className="h-full" title={data.events.title} inverted>
              <ul className="flex flex-col gap-3">
                {data.events.items.map((e) => (
                  <li key={e.id} className="flex flex-col text-sm">
                    <span className="text-xs opacity-70">{e.date} · {e.place}</span>
                    <span className="font-medium">{e.title}</span>
                  </li>
                ))}
              </ul>
            </DataTile>
          </div>
          <div className="@2xl:col-span-3">
            <DataTile className="h-full" title={data.collections.title} footer={note}>
              <ul className="grid grid-cols-2 gap-2">
                {data.collections.items.map((c) => (
                  <li key={c.id} className="flex flex-col gap-1.5">
                    {c.image ? (
                      <img src={c.image} alt="" loading="lazy" className="aspect-[4/3] w-full rounded-xl object-cover" style={{ background: c.color }} />
                    ) : (
                      <span aria-hidden className="aspect-[4/3] rounded-xl" style={{ background: c.color }} />
                    )}
                    <span className="text-sm font-medium">{c.name}</span>
                    <span className="text-xs text-muted-foreground">{works(c.works)}</span>
                  </li>
                ))}
              </ul>
            </DataTile>
          </div>
        </div>
      </div>
    </div>
  )
}
gallery-shop-demo.tsx
import { GalleryShop, type GalleryShopData } from '@/components/gallery-shop'

// Example data only. All names, places and figures are invented.
const data: GalleryShopData = {
  menu: [
    { id: 'home', label: 'Home' },
    { id: 'works', label: 'Works' },
    { id: 'events', label: 'Events' },
    { id: 'shop', label: 'Shop' },
  ],
  headline: { strong: 'Sample gallery.', soft: 'Example art for everyone.' },
  counters: [
    { id: 'a', title: 'Works', value: 240 },
    { id: 'b', title: 'Artists', value: 36 },
    { id: 'c', title: 'Events a year', value: 12 },
  ],
  events: {
    title: 'Events',
    items: [
      { id: 'e1', date: '12 Oct', place: 'Example Hall', title: 'Sample opening night' },
      { id: 'e2', date: '24 Oct', place: 'Demo Room', title: 'Test artist talk' },
    ],
  },
  collections: {
    title: 'Collections',
    items: [
      { id: 'c1', name: 'Sample Blues', works: 18, color: 'oklch(0.7 0.08 250)' },
      { id: 'c2', name: 'Demo Earth', works: 24, color: 'oklch(0.7 0.08 60)' },
      { id: 'c3', name: 'Example Green', works: 9, color: 'oklch(0.7 0.08 150)' },
      { id: 'c4', name: 'Test Rose', works: 14, color: 'oklch(0.7 0.08 10)' },
    ],
  },
}

export default function GalleryShopDemo() {
  return <GalleryShop data={data} />
}

HTML

Die ganze Seite als eine Datei mit eingebautem CSS und ohne JavaScript. Umschalter, Tabs und Filter funktionieren allein mit HTML und CSS.

In neuem Tab öffnen

Die Farbe in deiner eigenen App

Wenn shadcn bereits eingerichtet ist, ergeben diese Variablen dieselbe Farbe. Hell in :root, dunkel in .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Installieren

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/gallery-shop.json

Abhängigkeiten

    Dateien

    • gallery-shop.tsx
    • gallery-shop.html

    Manniche Lab · Open Source

    Kostenlose Werkzeuge für KI-Agenten und Webentwicklung.