@bearmenu/ui 0.8.8 → 0.8.9
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-62ES6N32.js +27 -0
- package/dist/chunk-AXV5O2IZ.js +58 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-THOVPZNN.js +37 -0
- package/dist/chunk-YS6BHKZO.js +92 -0
- 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/command.d.ts +7 -7
- package/dist/components/concept-compare.d.ts +73 -0
- package/dist/components/concept-compare.js +162 -0
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +471 -174
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.js +1 -1
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +20 -0
- package/dist/components/menu-family-card.d.ts +59 -0
- package/dist/components/menu-family-card.js +121 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +3 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-mocks.d.ts +55 -0
- package/dist/components/menu-mocks.js +379 -0
- package/dist/components/menu-types.d.ts +245 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-set.js +15 -3
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/ranked-venue-list.d.ts +49 -0
- package/dist/components/ranked-venue-list.js +87 -0
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/signature-line-list.d.ts +45 -0
- package/dist/components/signature-line-list.js +99 -0
- package/dist/components/snap-rail.d.ts +24 -4
- package/dist/components/snap-rail.js +66 -8
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/venue-busyness-strip.d.ts +35 -0
- package/dist/components/venue-busyness-strip.js +37 -0
- package/dist/components/venue-grade-dimensions.d.ts +36 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-signature-block.d.ts +39 -0
- package/dist/components/venue-signature-block.js +6 -0
- package/dist/components/venue-spotlight.d.ts +74 -0
- package/dist/components/venue-spotlight.js +138 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare.stories.tsx +104 -0
- package/src/components/concept-compare.tsx +276 -0
- package/src/components/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +40 -0
- package/src/components/menu-family-card.stories.tsx +112 -0
- package/src/components/menu-family-card.tsx +211 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-mocks.ts +417 -0
- package/src/components/menu-types.ts +259 -0
- package/src/components/motion-icon-set.tsx +30 -3
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/ranked-venue-list.stories.tsx +80 -0
- package/src/components/ranked-venue-list.tsx +177 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/signature-line-list.stories.tsx +85 -0
- package/src/components/signature-line-list.tsx +136 -0
- package/src/components/snap-rail.tsx +90 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/venue-busyness-strip.stories.tsx +66 -0
- package/src/components/venue-busyness-strip.tsx +64 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-signature-block.stories.tsx +68 -0
- package/src/components/venue-signature-block.tsx +71 -0
- package/src/components/venue-spotlight.stories.tsx +141 -0
- package/src/components/venue-spotlight.tsx +236 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
|
@@ -1,81 +1,127 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { ExternalLink, Navigation, Phone, Share2, X } from "lucide-react";
|
|
3
2
|
import {
|
|
3
|
+
ExternalLink,
|
|
4
|
+
Navigation,
|
|
5
|
+
Phone,
|
|
6
|
+
Share2,
|
|
7
|
+
X,
|
|
8
|
+
CalendarClock,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { Button } from "./button";
|
|
11
|
+
import {
|
|
12
|
+
EventDetailFooter,
|
|
13
|
+
EventDetailGhostAction,
|
|
4
14
|
EventDetailPanel,
|
|
15
|
+
EventDetailPanelError,
|
|
5
16
|
EventDetailPanelSkeleton,
|
|
6
17
|
type EventDetailPanelLabels,
|
|
7
18
|
} from "./event-detail-panel";
|
|
8
19
|
import type { EventFormatters, EventPreview } from "./event-types";
|
|
9
20
|
|
|
10
21
|
const labels: EventDetailPanelLabels = {
|
|
11
|
-
free: "Free",
|
|
12
|
-
liveNow: "
|
|
22
|
+
free: "Free entry",
|
|
23
|
+
liveNow: "Now",
|
|
24
|
+
ended: "Ended",
|
|
13
25
|
venueHeading: "Event venue",
|
|
26
|
+
venueNoPage: "name only — no venue page yet",
|
|
27
|
+
lineupHeading: "Line-up",
|
|
14
28
|
aboutHeading: "About this event",
|
|
29
|
+
noDescription: "Event details coming soon.",
|
|
30
|
+
readMore: "Read more",
|
|
31
|
+
readLess: "Read less",
|
|
15
32
|
};
|
|
16
33
|
|
|
17
34
|
const formatters: EventFormatters = {
|
|
18
|
-
time: () => "
|
|
19
|
-
day: () => "
|
|
35
|
+
time: () => "17:30",
|
|
36
|
+
day: () => "Tue, Sep 8",
|
|
20
37
|
price: (amount) => `${amount} RON`,
|
|
21
38
|
};
|
|
22
39
|
|
|
40
|
+
const description =
|
|
41
|
+
"An event for future brides featuring the new Rosa Clara 2027 collection and wedding planning advice from industry specialists. Doors at 17:00. The first look runs about an hour, then the atelier stays open for fittings until half seven. Bubbles throughout, and the kitchen puts out canapés at six.";
|
|
42
|
+
|
|
23
43
|
const event: EventPreview = {
|
|
24
44
|
id: "e1",
|
|
25
|
-
title: "
|
|
26
|
-
description
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
starts_at: "2026-09-
|
|
45
|
+
title: "Brides & Bubbles at Sophie Wedding House",
|
|
46
|
+
description,
|
|
47
|
+
location: "Sophie Wedding House",
|
|
48
|
+
place_name: "Sophie Wedding House",
|
|
49
|
+
place_slug: "sophie-wedding-house",
|
|
50
|
+
starts_at: "2026-09-08T17:30:00",
|
|
31
51
|
entry_fee: 50,
|
|
52
|
+
hype_score: 72,
|
|
32
53
|
};
|
|
33
54
|
|
|
55
|
+
const performers = [
|
|
56
|
+
"Rosa Clara",
|
|
57
|
+
"Sophie Atelier",
|
|
58
|
+
"Mara Ionescu",
|
|
59
|
+
"DJ Vlad",
|
|
60
|
+
"Trio Boema",
|
|
61
|
+
"Ana Pop",
|
|
62
|
+
"Mihai Ionescu",
|
|
63
|
+
"Quartetto",
|
|
64
|
+
];
|
|
65
|
+
|
|
34
66
|
const venue = {
|
|
35
|
-
name: "
|
|
36
|
-
|
|
67
|
+
name: "Sophie Wedding House",
|
|
68
|
+
address: "Str. Iuliu Maniu 5",
|
|
37
69
|
ratingLabel: "4.7",
|
|
38
|
-
|
|
70
|
+
isOpenNow: true,
|
|
71
|
+
href: "/places/sophie-wedding-house",
|
|
39
72
|
};
|
|
40
73
|
|
|
41
|
-
const
|
|
42
|
-
"flex h-9 w-9 items-center justify-center rounded-full bg-
|
|
74
|
+
const overlayButton =
|
|
75
|
+
"flex h-9 w-9 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-sm";
|
|
43
76
|
|
|
44
|
-
const
|
|
77
|
+
const sheetActions = (
|
|
45
78
|
<>
|
|
46
|
-
<button type="button" aria-label="Share" className={
|
|
79
|
+
<button type="button" aria-label="Share" className={overlayButton}>
|
|
47
80
|
<Share2 className="h-4 w-4" aria-hidden />
|
|
48
81
|
</button>
|
|
49
|
-
<button type="button" aria-label="Close" className={`absolute right-3.5 top-3.5 ${roundBtn}`}>
|
|
50
|
-
<X className="h-[17px] w-[17px]" aria-hidden />
|
|
51
|
-
</button>
|
|
52
|
-
</>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const footer = (
|
|
56
|
-
<>
|
|
57
|
-
<button
|
|
58
|
-
type="button"
|
|
59
|
-
className="flex h-[46px] flex-1 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-bold text-primary-foreground"
|
|
60
|
-
>
|
|
61
|
-
<Phone className="h-4 w-4" aria-hidden />
|
|
62
|
-
Reserve a spot
|
|
63
|
-
</button>
|
|
64
82
|
<button
|
|
65
83
|
type="button"
|
|
66
|
-
|
|
84
|
+
aria-label="Close"
|
|
85
|
+
className={`ml-auto ${overlayButton}`}
|
|
67
86
|
>
|
|
68
|
-
<
|
|
69
|
-
Directions
|
|
87
|
+
<X className="h-[17px] w-[17px]" aria-hidden />
|
|
70
88
|
</button>
|
|
71
89
|
</>
|
|
72
90
|
);
|
|
73
91
|
|
|
74
|
-
const
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
const hypePill = (
|
|
93
|
+
<span className="inline-flex h-6 items-center gap-1 rounded-full bg-[image:var(--gradient-primary)] px-2.5 text-[11.5px] font-bold text-primary-foreground">
|
|
94
|
+
Popular
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const footer = (
|
|
99
|
+
<EventDetailFooter
|
|
100
|
+
ghosts={[
|
|
101
|
+
<EventDetailGhostAction
|
|
102
|
+
key="dir"
|
|
103
|
+
icon={<Navigation className="h-3.5 w-3.5" aria-hidden />}
|
|
104
|
+
href="https://maps.google.com"
|
|
105
|
+
target="_blank"
|
|
106
|
+
rel="noopener noreferrer"
|
|
107
|
+
>
|
|
108
|
+
Directions
|
|
109
|
+
</EventDetailGhostAction>,
|
|
110
|
+
<EventDetailGhostAction
|
|
111
|
+
key="page"
|
|
112
|
+
icon={<ExternalLink className="h-3.5 w-3.5" aria-hidden />}
|
|
113
|
+
href="/events/e/brides-and-bubbles"
|
|
114
|
+
>
|
|
115
|
+
Event page
|
|
116
|
+
</EventDetailGhostAction>,
|
|
117
|
+
]}
|
|
118
|
+
primary={
|
|
119
|
+
<Button fullWidth>
|
|
120
|
+
<Phone aria-hidden />
|
|
121
|
+
Get tickets · 50 RON
|
|
122
|
+
</Button>
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
79
125
|
);
|
|
80
126
|
|
|
81
127
|
const meta: Meta<typeof EventDetailPanel> = {
|
|
@@ -86,18 +132,20 @@ const meta: Meta<typeof EventDetailPanel> = {
|
|
|
86
132
|
event,
|
|
87
133
|
labels,
|
|
88
134
|
formatters,
|
|
89
|
-
dateLine: "
|
|
135
|
+
dateLine: "Tuesday, September 8 · Starts at 17:30",
|
|
90
136
|
venue,
|
|
91
|
-
tags: ["
|
|
92
|
-
|
|
137
|
+
tags: ["Workshop", "Tasting"],
|
|
138
|
+
performers,
|
|
139
|
+
showAllPerformersLabel: "Show all 8 performers",
|
|
140
|
+
hype: hypePill,
|
|
141
|
+
headerActions: sheetActions,
|
|
93
142
|
footer,
|
|
94
|
-
externalLink,
|
|
95
143
|
},
|
|
96
144
|
parameters: {
|
|
97
145
|
docs: {
|
|
98
146
|
description: {
|
|
99
147
|
component:
|
|
100
|
-
"The
|
|
148
|
+
"The body of the event detail sheet and the base the event page extends. Not a sheet: mount it in `Sheet` (desktop) or `Drawer` (mobile). The hero is capped per host so the venue card clears the fold; status, fee, hype and tags share one chip row; the footer is one primary over a ghost row.",
|
|
101
149
|
},
|
|
102
150
|
},
|
|
103
151
|
},
|
|
@@ -106,86 +154,146 @@ const meta: Meta<typeof EventDetailPanel> = {
|
|
|
106
154
|
export default meta;
|
|
107
155
|
type Story = StoryObj<typeof EventDetailPanel>;
|
|
108
156
|
|
|
109
|
-
/**
|
|
157
|
+
/** The desktop side sheet: 480px wide, hero capped at 300px. */
|
|
110
158
|
export const SideSheet: Story = {
|
|
111
159
|
decorators: [
|
|
112
160
|
(Story) => (
|
|
113
|
-
<div className="h-[820px] w-[
|
|
161
|
+
<div className="h-[820px] w-[480px] overflow-hidden rounded-2xl shadow-[var(--shadow-overlay)]">
|
|
114
162
|
<Story />
|
|
115
163
|
</div>
|
|
116
164
|
),
|
|
117
165
|
],
|
|
118
166
|
};
|
|
119
167
|
|
|
120
|
-
/**
|
|
121
|
-
export const
|
|
122
|
-
args: {
|
|
168
|
+
/** The phone drawer: `compact` caps the hero at 318px. The host draws the handle and a Back pill. */
|
|
169
|
+
export const PhoneDrawer: Story = {
|
|
170
|
+
args: {
|
|
171
|
+
size: "compact",
|
|
172
|
+
headerActions: (
|
|
173
|
+
<>
|
|
174
|
+
<span className="inline-flex h-9 items-center gap-1.5 rounded-full bg-black/40 pl-2.5 pr-3 text-[13px] font-semibold text-white backdrop-blur-sm">
|
|
175
|
+
Back
|
|
176
|
+
</span>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
aria-label="Share"
|
|
180
|
+
className={`ml-auto ${overlayButton}`}
|
|
181
|
+
>
|
|
182
|
+
<Share2 className="h-4 w-4" aria-hidden />
|
|
183
|
+
</button>
|
|
184
|
+
</>
|
|
185
|
+
),
|
|
186
|
+
},
|
|
123
187
|
decorators: [
|
|
124
188
|
(Story) => (
|
|
125
|
-
<div className="h-[720px] w-
|
|
126
|
-
<div className="mx-auto mt-2.5 mb-1 h-1 w-[38px] rounded-full bg-border" />
|
|
189
|
+
<div className="h-[720px] w-[390px] overflow-hidden rounded-t-[20px] shadow-[var(--shadow-overlay)]">
|
|
127
190
|
<Story />
|
|
128
191
|
</div>
|
|
129
192
|
),
|
|
130
193
|
],
|
|
131
194
|
};
|
|
132
195
|
|
|
133
|
-
/** Live
|
|
134
|
-
export const
|
|
135
|
-
args: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</div>
|
|
141
|
-
),
|
|
142
|
-
],
|
|
196
|
+
/** Live, free, no poster: the gradient fallback, the Now chip and the free chip. */
|
|
197
|
+
export const LiveFreeNoPoster: Story = {
|
|
198
|
+
args: {
|
|
199
|
+
event: { ...event, poster_url: undefined, entry_fee: 0 },
|
|
200
|
+
status: "live",
|
|
201
|
+
},
|
|
202
|
+
decorators: SideSheet.decorators,
|
|
143
203
|
};
|
|
144
204
|
|
|
145
|
-
/**
|
|
146
|
-
export const
|
|
147
|
-
args: {
|
|
148
|
-
decorators:
|
|
149
|
-
(Story) => (
|
|
150
|
-
<div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
151
|
-
<Story />
|
|
152
|
-
</div>
|
|
153
|
-
),
|
|
154
|
-
],
|
|
205
|
+
/** Starting soon: the accent countdown chip. */
|
|
206
|
+
export const Soon: Story = {
|
|
207
|
+
args: { status: "soon", soonLabel: "Starts in 2h 10m" },
|
|
208
|
+
decorators: SideSheet.decorators,
|
|
155
209
|
};
|
|
156
210
|
|
|
157
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Unknown hour, no booking, a venue with no page, no line-up, no description.
|
|
213
|
+
* Every optional region drops out; the venue card says why it is not a link.
|
|
214
|
+
*/
|
|
158
215
|
export const Sparse: Story = {
|
|
159
216
|
args: {
|
|
160
|
-
event: { ...event, description:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
217
|
+
event: { ...event, description: "", hype_score: 0 },
|
|
218
|
+
dateLine: "Tuesday, September 8 · time to be announced",
|
|
219
|
+
venue: { name: "Sophie Wedding House" },
|
|
220
|
+
performers: undefined,
|
|
221
|
+
hype: undefined,
|
|
222
|
+
footer: (
|
|
223
|
+
<EventDetailFooter
|
|
224
|
+
ghosts={[
|
|
225
|
+
<EventDetailGhostAction
|
|
226
|
+
key="page"
|
|
227
|
+
icon={<ExternalLink className="h-3.5 w-3.5" aria-hidden />}
|
|
228
|
+
href="/events/e/x"
|
|
229
|
+
>
|
|
230
|
+
See the event page
|
|
231
|
+
</EventDetailGhostAction>,
|
|
232
|
+
]}
|
|
233
|
+
primary={
|
|
234
|
+
<Button fullWidth>
|
|
235
|
+
<Navigation aria-hidden />
|
|
236
|
+
Directions
|
|
237
|
+
</Button>
|
|
238
|
+
}
|
|
239
|
+
/>
|
|
240
|
+
),
|
|
164
241
|
},
|
|
165
|
-
decorators:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
242
|
+
decorators: SideSheet.decorators,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Ended: poster desaturated, title muted, explicit chip, and the primary replaced rather than disabled. */
|
|
246
|
+
export const Ended: Story = {
|
|
247
|
+
args: {
|
|
248
|
+
status: "ended",
|
|
249
|
+
footer: (
|
|
250
|
+
<EventDetailFooter
|
|
251
|
+
ghosts={[
|
|
252
|
+
<EventDetailGhostAction
|
|
253
|
+
key="dir"
|
|
254
|
+
icon={<Navigation className="h-3.5 w-3.5" aria-hidden />}
|
|
255
|
+
href="https://maps.google.com"
|
|
256
|
+
target="_blank"
|
|
257
|
+
rel="noopener noreferrer"
|
|
258
|
+
>
|
|
259
|
+
Directions
|
|
260
|
+
</EventDetailGhostAction>,
|
|
261
|
+
<EventDetailGhostAction
|
|
262
|
+
key="page"
|
|
263
|
+
icon={<ExternalLink className="h-3.5 w-3.5" aria-hidden />}
|
|
264
|
+
href="/events/e/x"
|
|
265
|
+
>
|
|
266
|
+
Event page
|
|
267
|
+
</EventDetailGhostAction>,
|
|
268
|
+
]}
|
|
269
|
+
primary={
|
|
270
|
+
<Button fullWidth>
|
|
271
|
+
<CalendarClock aria-hidden />
|
|
272
|
+
See what's on tonight
|
|
273
|
+
</Button>
|
|
274
|
+
}
|
|
275
|
+
/>
|
|
170
276
|
),
|
|
171
|
-
|
|
277
|
+
},
|
|
278
|
+
decorators: SideSheet.decorators,
|
|
172
279
|
};
|
|
173
280
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
<div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
178
|
-
<EventDetailPanelSkeleton />
|
|
179
|
-
</div>
|
|
180
|
-
),
|
|
281
|
+
export const Loading: Story = {
|
|
282
|
+
render: () => <EventDetailPanelSkeleton headerActions={sheetActions} />,
|
|
283
|
+
decorators: SideSheet.decorators,
|
|
181
284
|
};
|
|
182
285
|
|
|
183
|
-
|
|
184
|
-
export const SkeletonCompact: Story = {
|
|
286
|
+
export const Error: Story = {
|
|
185
287
|
render: () => (
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
288
|
+
<EventDetailPanelError
|
|
289
|
+
title="We could not load this event"
|
|
290
|
+
description="It may have been taken down, or the connection dropped on the way. The rest of tonight is still there."
|
|
291
|
+
retryLabel="Try again"
|
|
292
|
+
onRetry={() => {}}
|
|
293
|
+
secondaryLabel="See what's on tonight"
|
|
294
|
+
secondaryHref="/events/schedule"
|
|
295
|
+
headerActions={sheetActions}
|
|
296
|
+
/>
|
|
190
297
|
),
|
|
298
|
+
decorators: SideSheet.decorators,
|
|
191
299
|
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
EventDetailFooter,
|
|
5
|
+
EventDetailGhostAction,
|
|
6
|
+
EventDetailPanel,
|
|
7
|
+
EventDetailPanelError,
|
|
8
|
+
type EventDetailPanelLabels,
|
|
9
|
+
} from "./event-detail-panel";
|
|
10
|
+
import type { EventFormatters, EventPreview } from "./event-types";
|
|
11
|
+
|
|
12
|
+
const labels: EventDetailPanelLabels = {
|
|
13
|
+
free: "Free entry",
|
|
14
|
+
liveNow: "Now",
|
|
15
|
+
ended: "Ended",
|
|
16
|
+
venueHeading: "Event venue",
|
|
17
|
+
venueNoPage: "name only — no venue page yet",
|
|
18
|
+
lineupHeading: "Line-up",
|
|
19
|
+
aboutHeading: "About this event",
|
|
20
|
+
noDescription: "Event details coming soon.",
|
|
21
|
+
readMore: "Read more",
|
|
22
|
+
readLess: "Read less",
|
|
23
|
+
};
|
|
24
|
+
const formatters: EventFormatters = {
|
|
25
|
+
time: () => "17:30",
|
|
26
|
+
day: () => "Tue",
|
|
27
|
+
price: (n) => `${n} RON`,
|
|
28
|
+
};
|
|
29
|
+
const event: EventPreview = {
|
|
30
|
+
id: "e1",
|
|
31
|
+
title: "Brides & Bubbles",
|
|
32
|
+
description: "",
|
|
33
|
+
location: "Sophie Wedding House",
|
|
34
|
+
starts_at: "2026-09-08T17:30:00",
|
|
35
|
+
entry_fee: 50,
|
|
36
|
+
};
|
|
37
|
+
const base = { event, labels, formatters, dateLine: "Tuesday · 17:30" };
|
|
38
|
+
|
|
39
|
+
describe("EventDetailPanel", () => {
|
|
40
|
+
it("shows the status chip for live, soon and ended, and none when upcoming", () => {
|
|
41
|
+
const { rerender } = render(<EventDetailPanel {...base} />);
|
|
42
|
+
expect(screen.queryByText("Now")).not.toBeInTheDocument();
|
|
43
|
+
rerender(<EventDetailPanel {...base} status="live" />);
|
|
44
|
+
expect(screen.getByText("Now")).toBeInTheDocument();
|
|
45
|
+
rerender(
|
|
46
|
+
<EventDetailPanel {...base} status="soon" soonLabel="Starts in 2h" />,
|
|
47
|
+
);
|
|
48
|
+
expect(screen.getByText("Starts in 2h")).toBeInTheDocument();
|
|
49
|
+
rerender(<EventDetailPanel {...base} status="ended" />);
|
|
50
|
+
expect(screen.getByText("Ended")).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders the fee as a chip: paid amount, free label, nothing when unknown", () => {
|
|
54
|
+
const { rerender } = render(<EventDetailPanel {...base} />);
|
|
55
|
+
expect(screen.getByText("50 RON")).toBeInTheDocument();
|
|
56
|
+
rerender(<EventDetailPanel {...base} event={{ ...event, entry_fee: 0 }} />);
|
|
57
|
+
expect(screen.getByText("Free entry")).toBeInTheDocument();
|
|
58
|
+
rerender(
|
|
59
|
+
<EventDetailPanel {...base} event={{ ...event, entry_fee: undefined }} />,
|
|
60
|
+
);
|
|
61
|
+
expect(screen.queryByText(/RON|Free entry/)).not.toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("links the venue card only when the venue has a page, and says so otherwise", () => {
|
|
65
|
+
const { rerender } = render(
|
|
66
|
+
<EventDetailPanel
|
|
67
|
+
{...base}
|
|
68
|
+
venue={{
|
|
69
|
+
name: "Sophie",
|
|
70
|
+
address: "Str. 5",
|
|
71
|
+
ratingLabel: "4.7",
|
|
72
|
+
href: "/places/sophie",
|
|
73
|
+
}}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
expect(screen.getByRole("link", { name: /Sophie/ })).toHaveAttribute(
|
|
77
|
+
"href",
|
|
78
|
+
"/places/sophie",
|
|
79
|
+
);
|
|
80
|
+
expect(screen.queryByText(labels.venueNoPage)).not.toBeInTheDocument();
|
|
81
|
+
|
|
82
|
+
rerender(<EventDetailPanel {...base} venue={{ name: "Sophie" }} />);
|
|
83
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText(labels.venueNoPage)).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("previews five performers and reveals the rest on request", () => {
|
|
88
|
+
const performers = ["A", "B", "C", "D", "E", "F", "G"];
|
|
89
|
+
render(
|
|
90
|
+
<EventDetailPanel
|
|
91
|
+
{...base}
|
|
92
|
+
performers={performers}
|
|
93
|
+
showAllPerformersLabel="Show all 7"
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
expect(screen.queryByText("F")).not.toBeInTheDocument();
|
|
97
|
+
fireEvent.click(screen.getByRole("button", { name: "Show all 7" }));
|
|
98
|
+
expect(screen.getByText("F")).toBeInTheDocument();
|
|
99
|
+
expect(
|
|
100
|
+
screen.queryByRole("button", { name: "Show all 7" }),
|
|
101
|
+
).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("clamps a long description behind Read more and prints the placeholder for none", () => {
|
|
105
|
+
const long = "x".repeat(300);
|
|
106
|
+
const { rerender } = render(
|
|
107
|
+
<EventDetailPanel {...base} event={{ ...event, description: long }} />,
|
|
108
|
+
);
|
|
109
|
+
expect(screen.getByText(long).className).toContain("line-clamp-4");
|
|
110
|
+
fireEvent.click(screen.getByRole("button", { name: "Read more" }));
|
|
111
|
+
expect(screen.getByText(long).className).not.toContain("line-clamp-4");
|
|
112
|
+
expect(
|
|
113
|
+
screen.getByRole("button", { name: "Read less" }),
|
|
114
|
+
).toBeInTheDocument();
|
|
115
|
+
|
|
116
|
+
rerender(<EventDetailPanel {...base} />);
|
|
117
|
+
expect(screen.getByText("Event details coming soon.")).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("dims the poster and mutes the title when ended, not the whole panel", () => {
|
|
121
|
+
render(
|
|
122
|
+
<EventDetailPanel
|
|
123
|
+
{...base}
|
|
124
|
+
status="ended"
|
|
125
|
+
event={{ ...event, poster_url: "p.jpg" }}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByRole("img").parentElement?.className).toContain(
|
|
129
|
+
"saturate-",
|
|
130
|
+
);
|
|
131
|
+
expect(
|
|
132
|
+
screen.getByRole("heading", { name: "Brides & Bubbles" }).className,
|
|
133
|
+
).toContain("text-muted-foreground");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("EventDetailFooter", () => {
|
|
138
|
+
it("lays out the ghosts in one row above the primary", () => {
|
|
139
|
+
render(
|
|
140
|
+
<EventDetailFooter
|
|
141
|
+
ghosts={[
|
|
142
|
+
<EventDetailGhostAction key="a" href="/a">
|
|
143
|
+
Directions
|
|
144
|
+
</EventDetailGhostAction>,
|
|
145
|
+
<EventDetailGhostAction key="b" href="/b">
|
|
146
|
+
Event page
|
|
147
|
+
</EventDetailGhostAction>,
|
|
148
|
+
]}
|
|
149
|
+
primary={<button type="button">Reserve</button>}
|
|
150
|
+
/>,
|
|
151
|
+
);
|
|
152
|
+
expect(
|
|
153
|
+
screen.getByRole("link", { name: "Directions" }),
|
|
154
|
+
).toBeInTheDocument();
|
|
155
|
+
expect(
|
|
156
|
+
screen.getByRole("link", { name: "Event page" }),
|
|
157
|
+
).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByRole("button", { name: "Reserve" })).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("EventDetailPanelError", () => {
|
|
163
|
+
it("offers a retry and a way onward", () => {
|
|
164
|
+
const onRetry = vi.fn();
|
|
165
|
+
render(
|
|
166
|
+
<EventDetailPanelError
|
|
167
|
+
title="We could not load this event"
|
|
168
|
+
description="Try again."
|
|
169
|
+
retryLabel="Try again"
|
|
170
|
+
onRetry={onRetry}
|
|
171
|
+
secondaryLabel="See what's on tonight"
|
|
172
|
+
secondaryHref="/events/schedule"
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
fireEvent.click(screen.getByRole("button", { name: "Try again" }));
|
|
176
|
+
expect(onRetry).toHaveBeenCalled();
|
|
177
|
+
expect(
|
|
178
|
+
screen.getByRole("link", { name: "See what's on tonight" }),
|
|
179
|
+
).toHaveAttribute("href", "/events/schedule");
|
|
180
|
+
});
|
|
181
|
+
});
|