@bearmenu/ui 0.8.3 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2WDJR6K7.js +76 -0
- package/dist/chunk-637L7XQ3.js +85 -0
- package/dist/chunk-6XMYEZVR.js +26 -0
- package/dist/chunk-DNSXMHYX.js +8 -0
- package/dist/chunk-FDITZ2VM.js +16 -0
- package/dist/chunk-GPH6L4DX.js +55 -0
- package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
- package/dist/chunk-TT3WFS6S.js +379 -0
- package/dist/chunk-UXWAOISG.js +170 -0
- package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
- package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +4 -127
- package/dist/components/category-deck.d.ts +50 -0
- package/dist/components/category-deck.js +128 -0
- package/dist/components/command.d.ts +15 -15
- package/dist/components/concept-card.d.ts +43 -0
- package/dist/components/concept-card.js +81 -0
- package/dist/components/coverflow.d.ts +123 -0
- package/dist/components/coverflow.js +300 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/day-rail-chip.d.ts +82 -0
- package/dist/components/day-rail-chip.js +105 -0
- package/dist/components/drift-rail.d.ts +28 -0
- package/dist/components/drift-rail.js +3 -0
- package/dist/components/event-card.d.ts +97 -0
- package/dist/components/event-card.js +161 -0
- package/dist/components/event-compact-row.d.ts +72 -0
- package/dist/components/event-compact-row.js +133 -0
- package/dist/components/event-day-header.d.ts +65 -0
- package/dist/components/event-day-header.js +95 -0
- package/dist/components/event-day-separator.d.ts +46 -0
- package/dist/components/event-day-separator.js +50 -0
- package/dist/components/event-detail-panel.d.ts +80 -0
- package/dist/components/event-detail-panel.js +251 -0
- package/dist/components/event-hub-hero.d.ts +34 -0
- package/dist/components/event-hub-hero.js +57 -0
- package/dist/components/event-index-row.d.ts +68 -0
- package/dist/components/event-index-row.js +84 -0
- package/dist/components/event-list-row-wide.d.ts +101 -0
- package/dist/components/event-list-row-wide.js +218 -0
- package/dist/components/event-list-row.d.ts +82 -0
- package/dist/components/event-list-row.js +173 -0
- package/dist/components/event-poster-fallback.d.ts +20 -0
- package/dist/components/event-poster-fallback.js +3 -0
- package/dist/components/event-types.d.ts +118 -0
- package/dist/components/event-types.js +2 -0
- package/dist/components/events-empty-state.d.ts +44 -0
- package/dist/components/events-empty-state.js +73 -0
- package/dist/components/facet-group.d.ts +93 -0
- package/dist/components/facet-group.js +222 -0
- package/dist/components/featured-rail-item.d.ts +42 -0
- package/dist/components/featured-rail-item.js +88 -0
- package/dist/components/hour-band.d.ts +68 -0
- package/dist/components/hour-band.js +53 -0
- package/dist/components/hour-group.d.ts +67 -0
- package/dist/components/hour-group.js +59 -0
- package/dist/components/icon.d.ts +18 -0
- package/dist/components/icon.js +31 -0
- package/dist/components/item.d.ts +2 -2
- package/dist/components/motion-icon-controls.d.ts +26 -0
- package/dist/components/motion-icon-controls.js +4 -0
- package/dist/components/motion-icon-set.d.ts +27 -0
- package/dist/components/motion-icon-set.js +139 -0
- package/dist/components/motion-icon.d.ts +44 -0
- package/dist/components/motion-icon.js +3 -0
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/place-about-tab.js +3 -3
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/poster-stack.d.ts +30 -0
- package/dist/components/poster-stack.js +104 -0
- package/dist/components/poster-tape.d.ts +36 -0
- package/dist/components/poster-tape.js +64 -0
- package/dist/components/poster-wall.d.ts +25 -0
- package/dist/components/poster-wall.js +183 -0
- package/dist/components/quiet-stretch.d.ts +43 -0
- package/dist/components/quiet-stretch.js +54 -0
- package/dist/components/section-header.d.ts +53 -0
- package/dist/components/section-header.js +63 -0
- package/dist/components/segmented-nav.d.ts +62 -0
- package/dist/components/segmented-nav.js +54 -0
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.d.ts +17 -0
- package/dist/components/skeleton.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag-group.js +3 -26
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +1 -1
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-band.d.ts +46 -0
- package/dist/components/timeline-band.js +5 -0
- package/dist/components/venue-events-block.d.ts +143 -0
- package/dist/components/venue-events-block.js +222 -0
- package/dist/components/weekend-panel.d.ts +44 -0
- package/dist/components/weekend-panel.js +231 -0
- package/dist/lib/icon-registry.d.ts +58 -0
- package/dist/lib/icon-registry.js +39 -0
- package/package.json +2 -1
- package/src/components/accordion.stories.tsx +1 -1
- package/src/components/card.tsx +44 -1
- package/src/components/category-deck.stories.tsx +96 -0
- package/src/components/category-deck.tsx +227 -0
- package/src/components/combobox.stories.tsx +1 -1
- package/src/components/concept-card.stories.tsx +80 -0
- package/src/components/concept-card.tsx +143 -0
- package/src/components/coverflow.stories.tsx +240 -0
- package/src/components/coverflow.test.tsx +193 -0
- package/src/components/coverflow.tsx +550 -0
- package/src/components/currency-input.stories.tsx +3 -3
- package/src/components/day-rail-chip.stories.tsx +145 -0
- package/src/components/day-rail-chip.tsx +201 -0
- package/src/components/drift-rail.stories.tsx +92 -0
- package/src/components/drift-rail.tsx +86 -0
- package/src/components/empty-state.stories.tsx +3 -3
- package/src/components/event-card.stories.tsx +248 -0
- package/src/components/event-card.test.tsx +177 -0
- package/src/components/event-card.tsx +321 -0
- package/src/components/event-compact-row.stories.tsx +221 -0
- package/src/components/event-compact-row.tsx +268 -0
- package/src/components/event-day-header.stories.tsx +124 -0
- package/src/components/event-day-header.tsx +180 -0
- package/src/components/event-day-separator.stories.tsx +70 -0
- package/src/components/event-day-separator.tsx +95 -0
- package/src/components/event-detail-panel.stories.tsx +191 -0
- package/src/components/event-detail-panel.tsx +360 -0
- package/src/components/event-hub-hero.stories.tsx +66 -0
- package/src/components/event-hub-hero.tsx +110 -0
- package/src/components/event-index-row.stories.tsx +116 -0
- package/src/components/event-index-row.tsx +181 -0
- package/src/components/event-list-row-wide.stories.tsx +204 -0
- package/src/components/event-list-row-wide.tsx +386 -0
- package/src/components/event-list-row.stories.tsx +159 -0
- package/src/components/event-list-row.tsx +335 -0
- package/src/components/event-poster-fallback.stories.tsx +52 -0
- package/src/components/event-poster-fallback.tsx +31 -0
- package/src/components/event-types.ts +133 -0
- package/src/components/events-empty-state.stories.tsx +59 -0
- package/src/components/events-empty-state.tsx +131 -0
- package/src/components/facet-group.stories.tsx +247 -0
- package/src/components/facet-group.test.tsx +225 -0
- package/src/components/facet-group.tsx +352 -0
- package/src/components/featured-rail-item.stories.tsx +86 -0
- package/src/components/featured-rail-item.tsx +153 -0
- package/src/components/form.stories.tsx +1 -1
- package/src/components/hour-band.stories.tsx +159 -0
- package/src/components/hour-band.tsx +126 -0
- package/src/components/hour-group.stories.tsx +100 -0
- package/src/components/hour-group.tsx +124 -0
- package/src/components/icon.tsx +57 -0
- package/src/components/input-otp.stories.tsx +1 -1
- package/src/components/markdown-renderer.stories.tsx +1 -1
- package/src/components/motion-icon-controls.test.tsx +78 -0
- package/src/components/motion-icon-controls.tsx +552 -0
- package/src/components/motion-icon-set.stories.tsx +185 -0
- package/src/components/motion-icon-set.tsx +219 -0
- package/src/components/motion-icon.test.tsx +159 -0
- package/src/components/motion-icon.tsx +163 -0
- package/src/components/multi-select-combobox.stories.tsx +1 -1
- package/src/components/phone-frame.stories.tsx +1 -1
- package/src/components/poster-stack.stories.tsx +68 -0
- package/src/components/poster-stack.tsx +155 -0
- package/src/components/poster-tape.stories.tsx +70 -0
- package/src/components/poster-tape.tsx +122 -0
- package/src/components/poster-wall.stories.tsx +95 -0
- package/src/components/poster-wall.tsx +220 -0
- package/src/components/quiet-stretch.stories.tsx +62 -0
- package/src/components/quiet-stretch.tsx +115 -0
- package/src/components/rating.stories.tsx +3 -3
- package/src/components/searchable-select.stories.tsx +1 -1
- package/src/components/section-header.stories.tsx +70 -0
- package/src/components/section-header.tsx +134 -0
- package/src/components/segmented-nav.stories.tsx +118 -0
- package/src/components/segmented-nav.tsx +113 -0
- package/src/components/selection-bar.stories.tsx +1 -1
- package/src/components/skeleton-card.stories.tsx +3 -3
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sliding-panels.stories.tsx +1 -1
- package/src/components/stepper.stories.tsx +1 -1
- package/src/components/sticky-container.stories.tsx +3 -3
- package/src/components/tag.tsx +11 -1
- package/src/components/timeline-band.stories.tsx +206 -0
- package/src/components/timeline-band.tsx +207 -0
- package/src/components/toggle-group.stories.tsx +1 -1
- package/src/components/tooltip.stories.tsx +2 -2
- package/src/components/venue-events-block.stories.tsx +216 -0
- package/src/components/venue-events-block.tsx +408 -0
- package/src/components/weekend-panel.stories.tsx +92 -0
- package/src/components/weekend-panel.tsx +322 -0
- package/src/globals.css +19 -0
- package/src/lib/icon-registry.ts +139 -0
- package/src/test/setup.ts +5 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { EventHubHero, EventHubHeroSkeleton } from "./event-hub-hero";
|
|
3
|
+
import { PosterStack } from "./poster-stack";
|
|
4
|
+
|
|
5
|
+
function posterDataUri(label: string, fill: string) {
|
|
6
|
+
return (
|
|
7
|
+
"data:image/svg+xml;utf8," +
|
|
8
|
+
encodeURIComponent(
|
|
9
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400"><rect width="300" height="400" fill="${fill}"/><text x="50%" y="50%" font-family="monospace" font-size="20" fill="#fff" text-anchor="middle">${label}</text></svg>`
|
|
10
|
+
)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Explicitly annotated rather than `satisfies`: the meta-level decorator makes
|
|
15
|
+
// the inferred type reference storybook's internal CSF module, which tsc cannot
|
|
16
|
+
// name portably (TS2742).
|
|
17
|
+
const meta: Meta<typeof EventHubHero> = {
|
|
18
|
+
title: "Events/EventHubHero",
|
|
19
|
+
component: EventHubHero,
|
|
20
|
+
tags: ["autodocs"],
|
|
21
|
+
args: {
|
|
22
|
+
eyebrow: "Updated today",
|
|
23
|
+
title: "Everything happening in Bucharest",
|
|
24
|
+
intro:
|
|
25
|
+
"Concerts, parties, exhibitions and everything in between — hand-curated and updated every morning.",
|
|
26
|
+
stats: [
|
|
27
|
+
{ value: "312", label: "events this month" },
|
|
28
|
+
{ value: "48", label: "venues" },
|
|
29
|
+
{ value: "19", label: "free tonight" },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
decorators: [
|
|
33
|
+
(Story) => (
|
|
34
|
+
<div className="p-6">
|
|
35
|
+
<Story />
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof EventHubHero>;
|
|
43
|
+
|
|
44
|
+
export const Default: Story = {};
|
|
45
|
+
|
|
46
|
+
export const WithPosterStackAside: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
aside: (
|
|
49
|
+
<PosterStack
|
|
50
|
+
posters={[
|
|
51
|
+
{ src: posterDataUri("1", "#7a5c46"), alt: "Psihotrop poster" },
|
|
52
|
+
{ src: posterDataUri("2", "#4a6a5c"), alt: "Warehouse rave poster" },
|
|
53
|
+
{ src: posterDataUri("3", "#6a5c8a"), alt: "Jazz night poster" },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const NoAside: Story = {
|
|
61
|
+
args: { aside: undefined },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Skeleton: Story = {
|
|
65
|
+
render: () => <EventHubHeroSkeleton aside={<div className="aspect-[3/2] rounded-xl bg-muted" />} />,
|
|
66
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Skeleton } from "./skeleton";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* EventHubHero — the hub's opening block: an eyebrow, a big headline, an
|
|
9
|
+
* intro line and a stats row on the left; an arbitrary slot (the poster
|
|
10
|
+
* stack, in the source design) on the right. Stacks on mobile.
|
|
11
|
+
*
|
|
12
|
+
* All copy and stat values arrive pre-formatted — the DS holds no `Intl`
|
|
13
|
+
* locale and does not format numbers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type EventHubHeroStat = {
|
|
17
|
+
value: string;
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type EventHubHeroProps = {
|
|
22
|
+
/** Small caps-style label above the title, e.g. "Updated today". */
|
|
23
|
+
eyebrow: string;
|
|
24
|
+
title: string;
|
|
25
|
+
intro: string;
|
|
26
|
+
stats: EventHubHeroStat[];
|
|
27
|
+
/** The right-column slot — the poster stack in the source design. */
|
|
28
|
+
aside?: React.ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function EventHubHero({ eyebrow, title, intro, stats, aside, className }: EventHubHeroProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={cn(
|
|
36
|
+
"grid grid-cols-1 gap-8 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-10",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<div className="flex flex-col gap-4">
|
|
41
|
+
<p className="text-[11.5px] font-extrabold tracking-[0.12em] text-muted-foreground">
|
|
42
|
+
{eyebrow}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<h1 className="text-[31px] font-black leading-[1.05] tracking-[-0.035em] text-foreground md:text-[42px] md:tracking-[-0.04em]">
|
|
46
|
+
{title}
|
|
47
|
+
</h1>
|
|
48
|
+
|
|
49
|
+
<p className="max-w-[520px] text-[14px] leading-[1.6] text-muted-foreground md:text-[15px] md:leading-[1.65]">
|
|
50
|
+
{intro}
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<div className="flex gap-[22px] pt-1.5 md:gap-[30px]">
|
|
54
|
+
{stats.map((stat) => (
|
|
55
|
+
<div key={stat.label} className="flex flex-col">
|
|
56
|
+
<span className="text-[24px] font-black leading-none tracking-[-0.03em] tabular-nums text-foreground md:text-[30px]">
|
|
57
|
+
{stat.value}
|
|
58
|
+
</span>
|
|
59
|
+
<span className="mt-1 text-[11px] text-muted-foreground md:mt-[5px] md:text-[12.5px]">
|
|
60
|
+
{stat.label}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{aside && <div className="min-w-0">{aside}</div>}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type EventHubHeroSkeletonProps = {
|
|
73
|
+
/** The right-column slot, e.g. a `PosterStack`-shaped placeholder. Mirrors `aside` on the real component. */
|
|
74
|
+
aside?: React.ReactNode;
|
|
75
|
+
className?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function EventHubHeroSkeleton({ aside, className }: EventHubHeroSkeletonProps) {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
className={cn(
|
|
83
|
+
"grid grid-cols-1 gap-8 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-10",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<div className="flex flex-col gap-4">
|
|
88
|
+
<Skeleton className="h-[11.5px] w-28" />
|
|
89
|
+
|
|
90
|
+
<Skeleton className="h-[31px] w-full max-w-[420px] md:h-[42px]" />
|
|
91
|
+
|
|
92
|
+
<div className="flex max-w-[520px] flex-col gap-1.5">
|
|
93
|
+
<Skeleton className="h-[14px] w-full md:h-[15px]" />
|
|
94
|
+
<Skeleton className="h-[14px] w-4/5 md:h-[15px]" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="flex gap-[22px] pt-1.5 md:gap-[30px]">
|
|
98
|
+
{Array.from({ length: 3 }, (_, index) => (
|
|
99
|
+
<div key={index} className="flex flex-col">
|
|
100
|
+
<Skeleton className="h-[24px] w-10 md:h-[30px]" />
|
|
101
|
+
<Skeleton className="mt-1 h-[11px] w-16 md:mt-[5px] md:h-[12.5px]" />
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{aside && <div className="min-w-0">{aside}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Music, Wine } from "lucide-react";
|
|
3
|
+
import { EventIndexRow } from "./event-index-row";
|
|
4
|
+
import { EventDaySeparator } from "./event-day-separator";
|
|
5
|
+
import { EventPosterFallback } from "./event-poster-fallback";
|
|
6
|
+
|
|
7
|
+
const POSTER = "https://images.unsplash.com/photo-1514525253161-7a46d19cd819?w=256&h=256&fit=crop";
|
|
8
|
+
|
|
9
|
+
const DESCRIPTION =
|
|
10
|
+
"Trupa cântă un set de două ore din repertoriul propriu plus reinterpretări de standarde, cu pauză de treizeci de minute la mijloc. Barul rămâne deschis până la ora două, iar bucătăria ia ultima comandă la unsprezece.";
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof EventIndexRow> = {
|
|
13
|
+
title: "Events/EventIndexRow",
|
|
14
|
+
component: EventIndexRow,
|
|
15
|
+
tags: ["autodocs"],
|
|
16
|
+
args: {
|
|
17
|
+
title: "Afterwork Karaoke",
|
|
18
|
+
href: "#event",
|
|
19
|
+
when: "26 iulie, 20:00–23:00",
|
|
20
|
+
venue: "Cafe Solas",
|
|
21
|
+
venueHref: "#place",
|
|
22
|
+
fee: "intrare liberă",
|
|
23
|
+
tags: ["Karaoke", "Live music"],
|
|
24
|
+
description: DESCRIPTION,
|
|
25
|
+
posterUrl: POSTER,
|
|
26
|
+
},
|
|
27
|
+
// The row sits on a tinted band on its real surface; the decorator mirrors
|
|
28
|
+
// that so the `outline` tags and the `bg-card/60` hover are judged against
|
|
29
|
+
// the ground they actually land on, not against plain `--background`.
|
|
30
|
+
decorators: [
|
|
31
|
+
(Story) => (
|
|
32
|
+
<div className="w-full max-w-[900px] bg-muted/50 p-6">
|
|
33
|
+
<Story />
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof EventIndexRow>;
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {};
|
|
43
|
+
|
|
44
|
+
/** The shape the section actually renders: a heading that ends in its own rule,
|
|
45
|
+
* then rows, the first of which suppresses its rule to avoid doubling. */
|
|
46
|
+
export const InAGroup: Story = {
|
|
47
|
+
render: (args) => (
|
|
48
|
+
<div>
|
|
49
|
+
<EventDaySeparator as="h2" label="Evenimente azi în Cluj-Napoca" count="3 evenimente" />
|
|
50
|
+
<EventIndexRow {...args} hideRule />
|
|
51
|
+
<EventIndexRow
|
|
52
|
+
{...args}
|
|
53
|
+
title="Degustare de vinuri naturale"
|
|
54
|
+
when="26 iulie, 19:00"
|
|
55
|
+
venue="Enoteca Veche"
|
|
56
|
+
fee="bilet 80 lei"
|
|
57
|
+
tags={["Wine tasting"]}
|
|
58
|
+
posterUrl={undefined}
|
|
59
|
+
fallback={<EventPosterFallback icon={<Wine className="h-5 w-5 md:h-7 md:w-7" aria-hidden />} />}
|
|
60
|
+
/>
|
|
61
|
+
<EventIndexRow
|
|
62
|
+
{...args}
|
|
63
|
+
title="Peggy Gou b2b special guest"
|
|
64
|
+
when="26 iulie, 23:00–05:00"
|
|
65
|
+
venue="Form Space"
|
|
66
|
+
fee="bilet 140 lei"
|
|
67
|
+
tags={["Techno", "Party"]}
|
|
68
|
+
fallback={<EventPosterFallback icon={<Music className="h-5 w-5 md:h-7 md:w-7" aria-hidden />} />}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** No poster: the gradient fallback is already layered behind the image slot,
|
|
75
|
+
* so this is what a missing OR a failed image renders — with no JS. */
|
|
76
|
+
export const NoPoster: Story = {
|
|
77
|
+
args: { posterUrl: undefined },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const TagDerivedFallback: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
posterUrl: undefined,
|
|
83
|
+
fallback: <EventPosterFallback icon={<Wine className="h-5 w-5 md:h-7 md:w-7" aria-hidden />} />,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** An event nobody wrote a description for. The row closes up rather than
|
|
88
|
+
* reserving empty space, so a group of mixed rows still scans. */
|
|
89
|
+
export const NoDescription: Story = {
|
|
90
|
+
args: { description: undefined },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A venue with no place page — the name stays plain text rather than becoming
|
|
94
|
+
* a link to nowhere. */
|
|
95
|
+
export const NoVenueLink: Story = {
|
|
96
|
+
args: { venueHref: undefined },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const NoTags: Story = {
|
|
100
|
+
args: { tags: undefined },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Long titles WRAP. They are document-outline headings, so truncating one
|
|
104
|
+
* would hide text from the outline the section exists to build. */
|
|
105
|
+
export const LongTitle: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
title:
|
|
108
|
+
"Concert aniversar: douăzeci de ani de la primul album, cu invitați speciali și orchestră de cameră",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** `hideRule` on its own, for the first row directly under any heading that
|
|
113
|
+
* already draws a rule. */
|
|
114
|
+
export const FirstInGroup: Story = {
|
|
115
|
+
args: { hideRule: true },
|
|
116
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { EventPosterFallback } from "./event-poster-fallback";
|
|
4
|
+
import { Separator } from "./separator";
|
|
5
|
+
import { Tag } from "./tag";
|
|
6
|
+
import { TagGroup } from "./tag-group";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EventIndexRow — the row of an exhaustive, crawlable event index.
|
|
10
|
+
*
|
|
11
|
+
* NOT `<EventListRowWide>`: that row belongs to the list VIEW, and it carries
|
|
12
|
+
* a price column and a Reserve chip because the list view is where you act.
|
|
13
|
+
* This row makes no claim to be operable — it is a catalogue entry. Its only
|
|
14
|
+
* affordances are two real links: the title (stretched over the whole row via
|
|
15
|
+
* `after:absolute after:inset-0`) and the venue (escaping that overlay with
|
|
16
|
+
* `relative z-10`, the same nested-link pattern `<EventListRowWide>` uses for
|
|
17
|
+
* `menuHref`). That venue link is why this could not be a variant of
|
|
18
|
+
* `<EventCompactRow>` or `<EventListRow>`: both wrap their entire body in one
|
|
19
|
+
* anchor, so a second link inside would be a nested `<a>` — invalid HTML that
|
|
20
|
+
* browsers unnest, breaking both links.
|
|
21
|
+
*
|
|
22
|
+
* NO `"use client"`, AND THAT IS LOAD-BEARING. This row's surface is a
|
|
23
|
+
* server-only prerendered page carrying ~50 of them, and every sibling row in
|
|
24
|
+
* this family is a client component for one reason: `useState` to catch
|
|
25
|
+
* `<img onError>`. Here the fallback is layered BEHIND the poster instead, so
|
|
26
|
+
* a failed image reveals the designed gradient with no JS at all. The
|
|
27
|
+
* consequence to accept: an image that returns 200 but decodes to garbage
|
|
28
|
+
* shows an empty box rather than the fallback.
|
|
29
|
+
*
|
|
30
|
+
* THE DESCRIPTION IS NEVER TRUNCATED IN THE MARKUP — it is clamped with CSS
|
|
31
|
+
* (`line-clamp`), so the full text stays in the DOM. Callers must pass the
|
|
32
|
+
* whole string. On this surface that is the entire point: the text is the
|
|
33
|
+
* page's body copy, and a row that cut it at N characters would be deleting
|
|
34
|
+
* the thing the section exists to carry.
|
|
35
|
+
*
|
|
36
|
+
* ROW RULES: this row draws its own top rule, same as `<EventListRow>` and
|
|
37
|
+
* `<EventListRowWide>`. Pass `hideRule` on the first row under a heading that
|
|
38
|
+
* already ends in one (`<EventDaySeparator>` does), or you get two rules a few
|
|
39
|
+
* pixels apart.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type EventIndexRowLinkProps = {
|
|
43
|
+
href: string;
|
|
44
|
+
className?: string;
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function DefaultLink({ href, ...props }: EventIndexRowLinkProps) {
|
|
49
|
+
return <a href={href} {...props} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type EventIndexRowProps = {
|
|
53
|
+
/** Rendered inside `as` and allowed to WRAP — a document-outline heading is never truncated. */
|
|
54
|
+
title: string;
|
|
55
|
+
/** Heading level for `title`. Defaults to `h3`, one below the group headings. */
|
|
56
|
+
as?: "h2" | "h3" | "h4";
|
|
57
|
+
href: string;
|
|
58
|
+
/** Leading emphasised fact on the meta line — caller-composed date + time, e.g. "26 iulie, 20:00–23:00". */
|
|
59
|
+
when: string;
|
|
60
|
+
venue?: string;
|
|
61
|
+
/** Makes the venue a real second link. Omit for a venue with no place page. */
|
|
62
|
+
venueHref?: string;
|
|
63
|
+
/** Plain text, never a chip: "intrare liberă" / "bilet 40 lei". */
|
|
64
|
+
fee?: string;
|
|
65
|
+
/** Display labels. `EventPreview.tags` is taxonomy coordinates the DS cannot translate, so the caller resolves and caps them. */
|
|
66
|
+
tags?: string[];
|
|
67
|
+
/** FULL text. Clamped to 3 lines (mobile) / 2 (md+) in CSS only. */
|
|
68
|
+
description?: string;
|
|
69
|
+
posterUrl?: string;
|
|
70
|
+
fallback?: ReactNode;
|
|
71
|
+
/** Suppresses the top rule. Use on the first row under a heading that draws its own. */
|
|
72
|
+
hideRule?: boolean;
|
|
73
|
+
linkComponent?: ComponentType<EventIndexRowLinkProps>;
|
|
74
|
+
className?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function EventIndexRow({
|
|
78
|
+
title,
|
|
79
|
+
as: Heading = "h3",
|
|
80
|
+
href,
|
|
81
|
+
when,
|
|
82
|
+
venue,
|
|
83
|
+
venueHref,
|
|
84
|
+
fee,
|
|
85
|
+
tags,
|
|
86
|
+
description,
|
|
87
|
+
posterUrl,
|
|
88
|
+
fallback = <EventPosterFallback />,
|
|
89
|
+
hideRule,
|
|
90
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
91
|
+
className,
|
|
92
|
+
}: EventIndexRowProps) {
|
|
93
|
+
const dot = (
|
|
94
|
+
<span className="px-1 text-border" aria-hidden>
|
|
95
|
+
·
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className={cn("relative", className)}>
|
|
101
|
+
{!hideRule && <Separator className="absolute inset-x-0 top-0" />}
|
|
102
|
+
|
|
103
|
+
<div className="relative -mx-3 flex gap-3 rounded-lg px-3 py-4 transition-colors duration-150 hover:bg-card/60 motion-reduce:transition-none md:gap-4 md:py-5">
|
|
104
|
+
{/* Square poster at the same 72/104 the rest of the row family uses —
|
|
105
|
+
the editorial surfaces above this one are 4:3, but a third ratio
|
|
106
|
+
inside the ROW family would break the sibling-shape rule for one
|
|
107
|
+
section's sake. The fallback sits behind the image rather than
|
|
108
|
+
beside it in a ternary, which is what lets this whole row stay a
|
|
109
|
+
server component. */}
|
|
110
|
+
<div className="relative aspect-square w-[72px] shrink-0 overflow-hidden rounded-[9px] bg-muted md:w-[104px]">
|
|
111
|
+
<div className="absolute inset-0">{fallback}</div>
|
|
112
|
+
{posterUrl && (
|
|
113
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
114
|
+
<img
|
|
115
|
+
src={posterUrl}
|
|
116
|
+
alt=""
|
|
117
|
+
loading="lazy"
|
|
118
|
+
decoding="async"
|
|
119
|
+
className="absolute inset-0 h-full w-full object-cover"
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="min-w-0 flex-1">
|
|
125
|
+
<Heading className="text-[15px] font-bold leading-snug tracking-[-0.01em] text-foreground md:text-[16.5px]">
|
|
126
|
+
<LinkComponent
|
|
127
|
+
href={href}
|
|
128
|
+
className="rounded-sm transition-colors duration-150 after:absolute after:inset-0 hover:text-primary motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
129
|
+
>
|
|
130
|
+
{title}
|
|
131
|
+
</LinkComponent>
|
|
132
|
+
</Heading>
|
|
133
|
+
|
|
134
|
+
<p className="mt-1 text-[12px] leading-5 text-muted-foreground md:text-[12.5px]">
|
|
135
|
+
<span className="font-semibold tabular-nums text-foreground">{when}</span>
|
|
136
|
+
{venue && (
|
|
137
|
+
<>
|
|
138
|
+
{dot}
|
|
139
|
+
{venueHref ? (
|
|
140
|
+
<LinkComponent
|
|
141
|
+
href={venueHref}
|
|
142
|
+
className="relative z-10 rounded-sm font-semibold text-foreground/85 underline decoration-border underline-offset-2 transition-colors duration-150 hover:text-primary hover:decoration-primary motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
143
|
+
>
|
|
144
|
+
{venue}
|
|
145
|
+
</LinkComponent>
|
|
146
|
+
) : (
|
|
147
|
+
<span className="font-semibold text-foreground/85">{venue}</span>
|
|
148
|
+
)}
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
{fee && (
|
|
152
|
+
<>
|
|
153
|
+
{dot}
|
|
154
|
+
{fee}
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</p>
|
|
158
|
+
|
|
159
|
+
{tags && tags.length > 0 && (
|
|
160
|
+
// `outline`, not the default: default `Tag` is `bg-muted`, and this
|
|
161
|
+
// row's surface is a `bg-muted/50` band — a 0.019-L delta in light
|
|
162
|
+
// mode, which is invisible. `outline` reads on both.
|
|
163
|
+
<TagGroup className="mt-2">
|
|
164
|
+
{tags.map((tag) => (
|
|
165
|
+
<Tag key={tag} size="sm" variant="outline">
|
|
166
|
+
{tag}
|
|
167
|
+
</Tag>
|
|
168
|
+
))}
|
|
169
|
+
</TagGroup>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{description && (
|
|
173
|
+
<p className="mt-2 line-clamp-3 max-w-[620px] text-[13px] leading-[1.6] text-muted-foreground md:line-clamp-2 md:text-[13.5px] md:leading-[1.55]">
|
|
174
|
+
{description}
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { EventListRowWide, EventListRowWideSkeleton } from "./event-list-row-wide";
|
|
3
|
+
import type { EventListRowWideLabels } from "./event-list-row-wide";
|
|
4
|
+
import type { EventDateBadgeFormatters, EventPreview } from "./event-types";
|
|
5
|
+
|
|
6
|
+
const labels: EventListRowWideLabels = {
|
|
7
|
+
free: "Free",
|
|
8
|
+
liveNow: "On now",
|
|
9
|
+
reserve: "Reserve",
|
|
10
|
+
notifyMe: "Notify me",
|
|
11
|
+
soldOut: "Sold out",
|
|
12
|
+
seeMenu: "See the menu",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const formatters: EventDateBadgeFormatters = {
|
|
16
|
+
time: (iso) => new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }),
|
|
17
|
+
day: (iso) =>
|
|
18
|
+
new Date(iso).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }),
|
|
19
|
+
price: (amount) => `${amount} RON`,
|
|
20
|
+
dayNumber: (iso) => String(new Date(iso).getDate()).padStart(2, "0"),
|
|
21
|
+
monthShort: (iso) =>
|
|
22
|
+
new Date(iso).toLocaleDateString("en-GB", { month: "short" }).toUpperCase(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const event = (
|
|
26
|
+
id: string,
|
|
27
|
+
title: string,
|
|
28
|
+
place: string,
|
|
29
|
+
startsAt: string,
|
|
30
|
+
fee?: number,
|
|
31
|
+
description?: string
|
|
32
|
+
): EventPreview => ({
|
|
33
|
+
id,
|
|
34
|
+
title,
|
|
35
|
+
location: place,
|
|
36
|
+
place_name: place,
|
|
37
|
+
starts_at: startsAt,
|
|
38
|
+
entry_fee: fee,
|
|
39
|
+
description,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const description =
|
|
43
|
+
"Rooftop set from sunset to close, resident DJs on the decks all night, kitchen open until midnight with the full grill menu.";
|
|
44
|
+
|
|
45
|
+
const meta: Meta<typeof EventListRowWide> = {
|
|
46
|
+
title: "Events/EventListRowWide",
|
|
47
|
+
component: EventListRowWide,
|
|
48
|
+
tags: ["autodocs"],
|
|
49
|
+
args: { labels, formatters },
|
|
50
|
+
decorators: [
|
|
51
|
+
(Story) => (
|
|
52
|
+
<div className="w-[860px]">
|
|
53
|
+
<Story />
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
parameters: {
|
|
58
|
+
docs: {
|
|
59
|
+
description: {
|
|
60
|
+
component:
|
|
61
|
+
"The desktop list row — denser than the poster card, richer than the mobile list row, with a description and a dedicated action column. The row root is a plain container: the title is the primary link with a stretched overlay (so the whole row stays one click target), and 'See the menu' — rendered only when `menuHref` is set — is a second, real link layered above the overlay so it wins clicks in its own area.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default meta;
|
|
68
|
+
type Story = StoryObj<typeof EventListRowWide>;
|
|
69
|
+
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
event: event("1", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description),
|
|
73
|
+
tags: ["Live music", "Rooftop"],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The nested-link pattern: the title is the primary link (stretched over the
|
|
79
|
+
* whole row via `after:absolute after:inset-0`), and "See the menu" is a real,
|
|
80
|
+
* independently targeted link that wins clicks in its own area.
|
|
81
|
+
*/
|
|
82
|
+
export const WithMenuLink: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
event: event("1b", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description),
|
|
85
|
+
tags: ["Live music", "Rooftop"],
|
|
86
|
+
href: "/events/psihotrop-live",
|
|
87
|
+
menuHref: "/places/atelier-cafe/menu",
|
|
88
|
+
onClick: (_event, mouseEvent) => mouseEvent.preventDefault(),
|
|
89
|
+
},
|
|
90
|
+
parameters: {
|
|
91
|
+
docs: {
|
|
92
|
+
description: {
|
|
93
|
+
story:
|
|
94
|
+
"Click the title anywhere in the row to open the event; click 'See the menu' to go to the venue's menu instead — both are real links, not one anchor with dead text nested inside it.",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** No `menuHref` — the affordance is omitted entirely rather than rendered as dead text. */
|
|
101
|
+
export const WithoutMenuLink: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
event: event("1c", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description),
|
|
104
|
+
tags: ["Live music", "Rooftop"],
|
|
105
|
+
href: "/events/psihotrop-live",
|
|
106
|
+
onClick: (_event, mouseEvent) => mouseEvent.preventDefault(),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const FreeEntry: Story = {
|
|
111
|
+
args: {
|
|
112
|
+
event: event("2", "Vinyl Happy Hour", "Zorki Photo Cafe", "2026-09-04T18:00:00", 0, description),
|
|
113
|
+
tags: ["Vinyl", "Happy hour"],
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** No published fee is "TBC", never "Free". */
|
|
118
|
+
export const UnknownPrice: Story = {
|
|
119
|
+
args: { event: event("3", "Quiz Night", "Flying Circus", "2026-09-03T19:00:00") },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** Live tints the date badge and adds the on-air chip. */
|
|
123
|
+
export const LiveNow: Story = {
|
|
124
|
+
args: {
|
|
125
|
+
event: event("4", "Charity Concert & Afterparty", "Flying Circus", "2026-09-03T21:00:00", 30, description),
|
|
126
|
+
isLive: true,
|
|
127
|
+
tags: ["Charity", "Concert"],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Sold out dims the row and swaps the CTA to "Notify me". */
|
|
132
|
+
export const SoldOut: Story = {
|
|
133
|
+
args: {
|
|
134
|
+
event: event("5", "Friday Night Techno", "Form Space", "2026-09-04T22:00:00", 50, description),
|
|
135
|
+
isSoldOut: true,
|
|
136
|
+
tags: ["Techno", "Club night"],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** No `description` on the event — the block collapses instead of leaving a gap. */
|
|
141
|
+
export const NoDescription: Story = {
|
|
142
|
+
args: {
|
|
143
|
+
event: event("6", "Stand-up Comedy Night", "Klausen Pubhouse", "2026-09-04T19:30:00", 45),
|
|
144
|
+
tags: ["Comedy"],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Loading state — identical geometry to `Default`, so nothing shifts on data arrival. */
|
|
149
|
+
export const Loading: Story = {
|
|
150
|
+
render: () => <EventListRowWideSkeleton />,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** `NoDescription`'s narrower geometry, loading. */
|
|
154
|
+
export const LoadingNoDescription: Story = {
|
|
155
|
+
render: () => <EventListRowWideSkeleton showDescription={false} />,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** A loading row above a loaded one, so any geometry drift is visible at a glance. */
|
|
159
|
+
export const LoadingVsLoaded: Story = {
|
|
160
|
+
render: () => (
|
|
161
|
+
<div>
|
|
162
|
+
<EventListRowWideSkeleton />
|
|
163
|
+
<EventListRowWide
|
|
164
|
+
event={event("1", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description)}
|
|
165
|
+
labels={labels}
|
|
166
|
+
formatters={formatters}
|
|
167
|
+
tags={["Live music", "Rooftop"]}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** A day's worth of rows, as the desktop list view stacks them. */
|
|
174
|
+
export const ADay: Story = {
|
|
175
|
+
render: () => (
|
|
176
|
+
<div>
|
|
177
|
+
<EventListRowWide
|
|
178
|
+
event={event("a", "Psihotrop Live", "Atelier Café", "2026-09-03T20:00:00", 50, description)}
|
|
179
|
+
labels={labels}
|
|
180
|
+
formatters={formatters}
|
|
181
|
+
tags={["Live music", "Rooftop"]}
|
|
182
|
+
isLive
|
|
183
|
+
/>
|
|
184
|
+
<EventListRowWide
|
|
185
|
+
event={event("b", "Quiz Night", "Flying Circus", "2026-09-03T20:00:00")}
|
|
186
|
+
labels={labels}
|
|
187
|
+
formatters={formatters}
|
|
188
|
+
/>
|
|
189
|
+
<EventListRowWide
|
|
190
|
+
event={event("c", "Beer Battle", "Irish Music Pub", "2026-09-03T20:00:00", 0, description)}
|
|
191
|
+
labels={labels}
|
|
192
|
+
formatters={formatters}
|
|
193
|
+
tags={["Nightlife"]}
|
|
194
|
+
/>
|
|
195
|
+
<EventListRowWide
|
|
196
|
+
event={event("e", "Schoolotheque Afterparty", "Flying Circus", "2026-09-03T22:00:00", 25, description)}
|
|
197
|
+
labels={labels}
|
|
198
|
+
formatters={formatters}
|
|
199
|
+
tags={["Afterparty"]}
|
|
200
|
+
isSoldOut
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
),
|
|
204
|
+
};
|