@eventlook/sdk 1.5.0-beta.6 → 1.5.0-beta.8

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.
Files changed (91) hide show
  1. package/.claude/settings.local.json +6 -10
  2. package/.env.example +1 -0
  3. package/README.md +18 -16
  4. package/dist/cjs/{index-DvUR1fp8.js → index-CUIxdwQn.js} +3340 -577
  5. package/dist/cjs/index-CUIxdwQn.js.map +1 -0
  6. package/dist/cjs/index-D5rQiSGP.js +38574 -0
  7. package/dist/cjs/index-D5rQiSGP.js.map +1 -0
  8. package/dist/cjs/index.js +2 -2
  9. package/dist/cjs/{index.umd-6SU6nkkJ.js → index.umd-BoFEW91M.js} +9 -19
  10. package/dist/cjs/index.umd-BoFEW91M.js.map +1 -0
  11. package/dist/cjs/index.umd-BzSM62qM.js +13397 -0
  12. package/dist/cjs/index.umd-BzSM62qM.js.map +1 -0
  13. package/dist/esm/index-Cm7V8Zl3.js +38571 -0
  14. package/dist/esm/index-Cm7V8Zl3.js.map +1 -0
  15. package/dist/esm/{index-BlTqx0jm.js → index-fvLIN6eP.js} +3327 -563
  16. package/dist/esm/index-fvLIN6eP.js.map +1 -0
  17. package/dist/esm/index.js +2 -2
  18. package/dist/esm/{index.umd-Dn0hjh7E.js → index.umd-BKBHcCnm.js} +9 -19
  19. package/dist/esm/index.umd-BKBHcCnm.js.map +1 -0
  20. package/dist/esm/index.umd-bIV_YpEF.js +13395 -0
  21. package/dist/esm/index.umd-bIV_YpEF.js.map +1 -0
  22. package/dist/types/components/hook-form/FormProvider.d.ts +2 -1
  23. package/dist/types/form/PaymentOverviewBox.d.ts +2 -0
  24. package/dist/types/form/PaymentOverviewDrawer.d.ts +10 -0
  25. package/dist/types/form/TicketForm.d.ts +1 -0
  26. package/dist/types/form/index.d.ts +2 -1
  27. package/dist/types/form/merchandise/MerchandiseSelection.d.ts +9 -0
  28. package/dist/types/form/merchandise/MerchandiseSlider.d.ts +10 -0
  29. package/dist/types/form/payment/PaymentOverviewCheckbox.d.ts +0 -4
  30. package/dist/types/form/product/ProductVariantsDialog.d.ts +3 -1
  31. package/dist/types/form/services/index.d.ts +7 -0
  32. package/dist/types/form/style.d.ts +1 -0
  33. package/dist/types/form/tickets/ReleaseDescription.d.ts +10 -0
  34. package/dist/types/form/tickets/ReleaseWithMerchandise.d.ts +12 -0
  35. package/dist/types/form/tickets/TicketQuantityControl.d.ts +13 -0
  36. package/dist/types/form/tickets/TicketSelectionMobile.d.ts +16 -0
  37. package/dist/types/hooks/useScrollToFirstError.d.ts +4 -0
  38. package/dist/types/locales/cs.d.ts +22 -0
  39. package/dist/types/locales/en.d.ts +22 -0
  40. package/dist/types/locales/es.d.ts +22 -0
  41. package/dist/types/locales/pl.d.ts +22 -0
  42. package/dist/types/locales/sk.d.ts +22 -0
  43. package/dist/types/locales/uk.d.ts +22 -0
  44. package/dist/types/utils/data/global.d.ts +1 -0
  45. package/dist/types/utils/data/ticket.d.ts +1 -0
  46. package/package.json +10 -4
  47. package/rollup.config.mjs +7 -12
  48. package/src/components/hook-form/FormProvider.tsx +5 -2
  49. package/src/form/ChildEventDialog.tsx +3 -3
  50. package/src/form/ContactPerson.tsx +1 -1
  51. package/src/form/PaymentOverviewBox.tsx +96 -123
  52. package/src/form/PaymentOverviewDrawer.tsx +445 -0
  53. package/src/form/PaymentPending.tsx +19 -4
  54. package/src/form/ReleaseWithMerchandise.tsx +4 -4
  55. package/src/form/Shipping.tsx +48 -33
  56. package/src/form/TicketForm.tsx +146 -41
  57. package/src/form/index.tsx +3 -1
  58. package/src/form/merchandise/MerchandiseSelection.tsx +24 -0
  59. package/src/form/merchandise/MerchandiseSlider.tsx +62 -0
  60. package/src/form/payment/FeeBox.tsx +4 -31
  61. package/src/form/payment/PaymentOverviewCheckbox.tsx +68 -69
  62. package/src/form/product/ProductCard.tsx +258 -59
  63. package/src/form/product/ProductVariantsDialog.tsx +292 -139
  64. package/src/form/services/index.tsx +262 -0
  65. package/src/form/style.ts +16 -4
  66. package/src/form/tickets/ReleaseDescription.tsx +46 -0
  67. package/src/form/tickets/ReleaseWithMerchandise.tsx +267 -0
  68. package/src/form/tickets/TicketQuantityControl.tsx +100 -0
  69. package/src/form/tickets/TicketSelection.tsx +236 -0
  70. package/src/form/{TicketSelectionMap.tsx → tickets/TicketSelectionMap.tsx} +18 -2
  71. package/src/form/tickets/TicketSelectionMobile.tsx +188 -0
  72. package/src/form/{TicketWithMerchandiseSelection.tsx → tickets/TicketWithMerchandiseSelection.tsx} +52 -38
  73. package/src/hooks/useScrollToFirstError.ts +99 -0
  74. package/src/locales/cs.tsx +25 -3
  75. package/src/locales/en.tsx +23 -1
  76. package/src/locales/es.tsx +23 -1
  77. package/src/locales/pl.tsx +23 -1
  78. package/src/locales/sk.tsx +24 -2
  79. package/src/locales/uk.tsx +23 -1
  80. package/src/utils/data/global.ts +1 -0
  81. package/src/utils/data/ticket.ts +1 -0
  82. package/tsconfig.json +1 -1
  83. package/README +0 -1
  84. package/dist/cjs/index-DvUR1fp8.js.map +0 -1
  85. package/dist/cjs/index.umd-6SU6nkkJ.js.map +0 -1
  86. package/dist/esm/index-BlTqx0jm.js.map +0 -1
  87. package/dist/esm/index.umd-Dn0hjh7E.js.map +0 -1
  88. package/src/form/TicketSelection.tsx +0 -307
  89. /package/dist/types/form/{TicketSelection.d.ts → tickets/TicketSelection.d.ts} +0 -0
  90. /package/dist/types/form/{TicketSelectionMap.d.ts → tickets/TicketSelectionMap.d.ts} +0 -0
  91. /package/dist/types/form/{TicketWithMerchandiseSelection.d.ts → tickets/TicketWithMerchandiseSelection.d.ts} +0 -0
@@ -0,0 +1,100 @@
1
+ import React from 'react';
2
+ import { Box, Button, IconButton, Stack } from '@mui/material';
3
+ import { Iconify } from '@components/iconify';
4
+ import useGlobal from '@hooks/useGlobal';
5
+
6
+ interface TicketQuantityControlProps {
7
+ quantity: number;
8
+ isDisabled: boolean;
9
+ canAddFirst: boolean;
10
+ canAddMore: boolean;
11
+ addLabel: string;
12
+ onDecrement: () => void;
13
+ onIncrement: () => void;
14
+ onAddFirst: () => void;
15
+ }
16
+
17
+ const TicketQuantityControl: React.FC<TicketQuantityControlProps> = ({
18
+ quantity,
19
+ isDisabled,
20
+ canAddFirst,
21
+ canAddMore,
22
+ addLabel,
23
+ onDecrement,
24
+ onIncrement,
25
+ onAddFirst,
26
+ }) => {
27
+ const { t } = useGlobal();
28
+
29
+ if (quantity > 0) {
30
+ return (
31
+ <Stack direction="row" spacing={{ xs: 0.5, md: 1 }} alignItems="center">
32
+ <IconButton
33
+ onClick={onDecrement}
34
+ aria-label={t('form.labels.ticket_quantity_decrease')}
35
+ disabled={isDisabled || quantity <= 0}
36
+ sx={{
37
+ width: { xs: 36, md: 40 },
38
+ height: { xs: 36, md: 40 },
39
+ borderRadius: 1,
40
+ border: '1px solid',
41
+ borderColor: 'grey.300',
42
+ }}
43
+ >
44
+ <Iconify icon="eva:minus-fill" />
45
+ </IconButton>
46
+ <Box
47
+ sx={{
48
+ width: { xs: 36, md: 40 },
49
+ height: { xs: 36, md: 40 },
50
+ borderRadius: 1,
51
+ border: '1px solid',
52
+ borderColor: 'grey.300',
53
+ display: 'flex',
54
+ alignItems: 'center',
55
+ justifyContent: 'center',
56
+ fontWeight: 600,
57
+ }}
58
+ >
59
+ {quantity}
60
+ </Box>
61
+ <IconButton
62
+ onClick={onIncrement}
63
+ aria-label={t('form.labels.ticket_quantity_increase')}
64
+ disabled={isDisabled || !canAddMore}
65
+ sx={{
66
+ width: { xs: 36, md: 40 },
67
+ height: { xs: 36, md: 40 },
68
+ borderRadius: 1,
69
+ bgcolor: 'primary.main',
70
+ color: 'primary.contrastText',
71
+ '&:hover': { bgcolor: 'primary.dark' },
72
+ }}
73
+ >
74
+ <Iconify icon="eva:plus-fill" />
75
+ </IconButton>
76
+ </Stack>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <Button
82
+ variant="contained"
83
+ onClick={onAddFirst}
84
+ aria-label={addLabel}
85
+ disabled={isDisabled || !canAddFirst}
86
+ sx={{
87
+ height: { xs: 36, md: 40 },
88
+ width: { xs: 116, md: 136 },
89
+ borderRadius: 1,
90
+ px: 3,
91
+ textTransform: 'none',
92
+ fontWeight: 600,
93
+ }}
94
+ >
95
+ {addLabel}
96
+ </Button>
97
+ );
98
+ };
99
+
100
+ export default TicketQuantityControl;
@@ -0,0 +1,236 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { useFormContext, useWatch } from 'react-hook-form';
3
+ import { Box, Divider, Stack, Typography } from '@mui/material';
4
+ import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
5
+ import useEventActiveReleases from '@hooks/data/useEventActiveReleases';
6
+ import { groupBy } from '@utils/global';
7
+ import { IReleaseShort } from '@utils/types/release.type';
8
+ import { IEvent } from '@utils/types/event.type';
9
+ import useResponsive from '@hooks/useResponsive';
10
+ import ReleaseExtraFields from '@form/extra-field/ReleaseExtraFields';
11
+ import { EventType } from '@utils/data/event';
12
+ import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
13
+ import useGlobal from '@hooks/useGlobal';
14
+ import TicketSelectionMobile from './TicketSelectionMobile';
15
+
16
+ interface Props {
17
+ event: IEvent;
18
+ }
19
+
20
+ const TicketSelection: React.FC<Props> = ({ event }) => {
21
+ const { t } = useGlobal();
22
+ const isMobile = useResponsive('down', 'md');
23
+ const { setValue, watch } = useFormContext<ITicketForm>();
24
+ const tickets = useWatch({
25
+ name: `tickets.${event.id}`,
26
+ defaultValue: [],
27
+ }) as ITicketFormTicket[];
28
+ const eventTimeslotId = watch('eventTimeslotId');
29
+ const isProcessingRef = useRef(false);
30
+ const { data: activeReleases, mutate } = useEventActiveReleases(
31
+ event.id,
32
+ false,
33
+ event.type === EventType.RECURRING ? eventTimeslotId : undefined
34
+ );
35
+
36
+ const showLoading = !activeReleases && event.type !== EventType.RECURRING;
37
+
38
+ useEffect(() => {
39
+ if (!isProcessingRef.current) selectedTickets();
40
+ }, [tickets, activeReleases]);
41
+
42
+ const isReleaseSelected = (id: number) => !!tickets.find((ticket) => ticket.releaseId === id);
43
+
44
+ const getRelease = (releaseId: number) => {
45
+ const release = activeReleases?.find((activeRelease) => activeRelease.id === releaseId);
46
+ return release ? release : null;
47
+ };
48
+
49
+ const getExtraFields = (releaseId: number | '', index: number) => {
50
+ if (!releaseId) return null;
51
+
52
+ const release = getRelease(releaseId);
53
+
54
+ if (!release || !release?.extraFields || release?.extraFields.length <= 0) return null;
55
+
56
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
57
+ const countTickets = addedRelease?.quantity || 0;
58
+ return (
59
+ <ReleaseExtraFields
60
+ release={release}
61
+ eventId={event.id}
62
+ releaseIndex={index}
63
+ quantity={countTickets}
64
+ />
65
+ );
66
+ };
67
+
68
+ // const getAvailableTicketsForRelease = (release: ITicketFormTicket): number => {
69
+ // const selectedRelease = activeReleases?.find((item) => item.id === release.releaseId);
70
+ // const availableQuantity = selectedRelease ? selectedRelease.availableTickets : 0;
71
+ // return availableQuantity > MAX_TICKETS_PER_ORDER ? MAX_TICKETS_PER_ORDER : availableQuantity;
72
+ // };
73
+
74
+ const countReleaseCategories = (): number => {
75
+ const grouped = groupBy(activeReleases || [], 'releaseCategoryName');
76
+ return Object.keys(grouped).length;
77
+ };
78
+
79
+ const selectedReleaseIsSoldOut = (releases: IReleaseShort[] | undefined) => {
80
+ const lockedSelectedReleases: boolean[] | undefined = releases?.map((item, index) => {
81
+ const nextRelease = releases?.find(
82
+ (item2) =>
83
+ item2.releaseCategoryName === item.releaseCategoryName && item2.order === item.order + 1
84
+ );
85
+ const selected = tickets.find((ticket) => ticket.releaseId === item.id);
86
+ const maxSelectable = Math.min(item.availableTickets || 0, MAX_TICKETS_PER_ORDER);
87
+
88
+ return (
89
+ !!nextRelease &&
90
+ item.locked &&
91
+ !!selected &&
92
+ Number(selected.quantity) >= maxSelectable &&
93
+ index + 1 == tickets.length
94
+ );
95
+ });
96
+ return lockedSelectedReleases && lockedSelectedReleases.includes(true);
97
+ };
98
+
99
+ const countUnlockedReleases = () => activeReleases?.filter((item) => !item.locked).length || 0;
100
+
101
+ const countSelectedTickets = () => {
102
+ let count = 0;
103
+ for (const ticket of tickets) {
104
+ count += +ticket.quantity;
105
+ }
106
+
107
+ return count;
108
+ };
109
+
110
+ const isQuantityDisabled = (value: number, releaseId: number | '') => {
111
+ const releaseSelected = tickets.find((item) => item.releaseId === releaseId);
112
+ return releaseSelected && releaseSelected.quantity
113
+ ? countSelectedTickets() + value - releaseSelected.quantity > MAX_TICKETS_PER_ORDER
114
+ : countSelectedTickets() + value > MAX_TICKETS_PER_ORDER;
115
+ };
116
+
117
+ const removeTicket = (indexToRemove: number) => {
118
+ const activeReleases = tickets.filter((_ticket, index) => index !== indexToRemove);
119
+ setValue(`tickets.${event.id}`, activeReleases);
120
+ };
121
+
122
+ const selectedTickets = async () => {
123
+ const releases = await mutate();
124
+ const currentReleases = releases || activeReleases || [];
125
+ const allFilled = tickets.filter((item) => !item.releaseId || !item.quantity);
126
+
127
+ const soldOutReleaseCategories = currentReleases.filter((release) =>
128
+ tickets.find(
129
+ (ticket) =>
130
+ release.id === ticket.releaseId &&
131
+ Number(ticket.quantity) >= Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER)
132
+ )
133
+ );
134
+
135
+ if (currentReleases.length) {
136
+ let hasChanges = false;
137
+ const updatedReleases = currentReleases.map((release) => {
138
+ const previousRelease = currentReleases.find(
139
+ (item) =>
140
+ item.releaseCategoryName === release.releaseCategoryName &&
141
+ item.order === release.order - 1
142
+ );
143
+
144
+ if (!release.locked || !previousRelease) return release;
145
+
146
+ const previousTicket = tickets.find((ticket) => ticket.releaseId === previousRelease.id);
147
+ const previousMaxSelectable = Math.min(
148
+ previousRelease.availableTickets || 0,
149
+ MAX_TICKETS_PER_ORDER
150
+ );
151
+ const shouldUnlock = Number(previousTicket?.quantity || 0) >= previousMaxSelectable;
152
+
153
+ if (!shouldUnlock) return release;
154
+
155
+ hasChanges = true;
156
+ return { ...release, locked: false };
157
+ });
158
+
159
+ if (hasChanges) {
160
+ await mutate(updatedReleases, false);
161
+ }
162
+ }
163
+
164
+ const hasSelectableRelease = currentReleases.some(
165
+ (release) => !isReleaseSelected(release.id) && !release.locked
166
+ );
167
+
168
+ const shouldAddRow =
169
+ (soldOutReleaseCategories &&
170
+ selectedReleaseIsSoldOut(releases) &&
171
+ tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
172
+ !allFilled.length) ||
173
+ (currentReleases.length &&
174
+ soldOutReleaseCategories.length &&
175
+ currentReleases.length > tickets.length &&
176
+ tickets.length < soldOutReleaseCategories.length + countUnlockedReleases() &&
177
+ !allFilled.length) ||
178
+ (tickets.length < countReleaseCategories() && !allFilled.length);
179
+
180
+ const shouldRemoveEmptyRows =
181
+ allFilled.length > 0 &&
182
+ tickets.length > 1 &&
183
+ !hasSelectableRelease &&
184
+ (!soldOutReleaseCategories?.length || !selectedReleaseIsSoldOut(releases));
185
+
186
+ if (shouldAddRow) {
187
+ isProcessingRef.current = true;
188
+ setValue(`tickets.${event.id}`, [
189
+ ...tickets,
190
+ {
191
+ releaseId: '',
192
+ quantity: '',
193
+ itemName: '',
194
+ price: 0,
195
+ products: [],
196
+ extraFields: [],
197
+ },
198
+ ]);
199
+ setTimeout(() => (isProcessingRef.current = false), 0);
200
+ } else if (shouldRemoveEmptyRows) {
201
+ // Only remove completely empty rows (no releaseId), keep rows where user started selecting
202
+ const nonEmptyTickets = tickets.filter((item) => item.releaseId);
203
+ if (nonEmptyTickets.length < tickets.length) {
204
+ isProcessingRef.current = true;
205
+ setValue(`tickets.${event.id}`, nonEmptyTickets);
206
+ setTimeout(() => (isProcessingRef.current = false), 0);
207
+ }
208
+ }
209
+ };
210
+
211
+ return (
212
+ <Stack
213
+ spacing={3}
214
+ direction="column"
215
+ divider={!isMobile ? <Divider sx={{ borderStyle: 'dashed' }} /> : undefined}
216
+ >
217
+ <TicketSelectionMobile
218
+ event={event}
219
+ activeReleases={activeReleases}
220
+ showLoading={showLoading}
221
+ tickets={tickets}
222
+ isQuantityDisabled={isQuantityDisabled}
223
+ setValue={setValue as (name: string, value: any) => void}
224
+ removeTicket={removeTicket}
225
+ getExtraFields={getExtraFields}
226
+ />
227
+ <Box>
228
+ <Typography variant="caption" component="div" fontStyle="italic" mb={2}>
229
+ *{t('event.tickets.stepper.1.max_ticket_quantity')}
230
+ </Typography>
231
+ </Box>
232
+ </Stack>
233
+ );
234
+ };
235
+
236
+ export default TicketSelection;
@@ -5,6 +5,8 @@ import { Button } from '@mui/material';
5
5
  import { iframe, TicketSelection } from '@seat-picker/seat-picker-sdk';
6
6
  import { useFormContext } from 'react-hook-form';
7
7
  import { ITicketForm, ITicketFormTicket, ITicketLocation } from '@utils/types/ticket.type';
8
+ import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
9
+ import Iconify from '@components/iconify/Iconify';
8
10
 
9
11
  interface Props {
10
12
  event: IEvent;
@@ -42,11 +44,18 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
42
44
  [] as { quantity: number; seat: ITicketLocation; ticket: any }[]
43
45
  );
44
46
 
47
+ let remainingTicketCapacity = MAX_TICKETS_PER_ORDER;
48
+
45
49
  for (const groupedSeat of groupedSeatsByZone) {
50
+ if (remainingTicketCapacity <= 0) break;
51
+
52
+ const quantity = Math.min(groupedSeat.quantity, remainingTicketCapacity);
53
+ remainingTicketCapacity -= quantity;
54
+
46
55
  tickets.push({
47
56
  releaseId: groupedSeat.ticket.id,
48
57
  price: groupedSeat.ticket.price,
49
- quantity: groupedSeat.quantity,
58
+ quantity,
50
59
  itemName: `${groupedSeat.ticket.releaseCategoryName} - ${groupedSeat.ticket.name}`,
51
60
  products: [],
52
61
  extraFields: [],
@@ -65,7 +74,7 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
65
74
 
66
75
  return (
67
76
  <Button
68
- variant="contained"
77
+ variant="outlined"
69
78
  onClick={() =>
70
79
  iframe.openPicker({
71
80
  eventId: String(event.id),
@@ -74,6 +83,13 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
74
83
  clientId: uuid,
75
84
  })
76
85
  }
86
+ sx={{
87
+ width: { xs: '100%' },
88
+ color: 'text.primary',
89
+ borderColor: (theme) => theme.palette.grey['300'],
90
+ '& .MuiButton-endIcon': { ml: 0, fontSize: '1.5em' },
91
+ }}
92
+ endIcon={<Iconify icon="eva:chevron-right-outline" />}
77
93
  >
78
94
  {t('form.labels.open_map')}
79
95
  </Button>
@@ -0,0 +1,188 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Skeleton, Stack, Typography, useTheme } from '@mui/material';
3
+ import { fCurrency } from '@utils/formatNumber';
4
+ import { IEvent } from '@utils/types/event.type';
5
+ import { IReleaseShort } from '@utils/types/release.type';
6
+ import { ITicketFormTicket } from '@utils/types/ticket.type';
7
+ import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
8
+ import useGlobal from '@hooks/useGlobal';
9
+ import ReleaseDescription from './ReleaseDescription';
10
+ import TicketQuantityControl from './TicketQuantityControl';
11
+
12
+ interface Props {
13
+ event: IEvent;
14
+ activeReleases?: IReleaseShort[];
15
+ showLoading: boolean;
16
+ tickets: ITicketFormTicket[];
17
+ isQuantityDisabled: (value: number, releaseId: number | '') => boolean;
18
+ setValue: (name: string, value: any) => void;
19
+ removeTicket: (indexToRemove: number) => void;
20
+ getExtraFields: (releaseId: number | '', index: number) => React.ReactNode;
21
+ }
22
+
23
+ const TicketSelectionMobile: React.FC<Props> = ({
24
+ event,
25
+ activeReleases,
26
+ showLoading,
27
+ tickets,
28
+ isQuantityDisabled,
29
+ setValue,
30
+ removeTicket,
31
+ getExtraFields,
32
+ }) => {
33
+ const { t, lang } = useGlobal();
34
+ const [expandedReleaseIds, setExpandedReleaseIds] = useState<Record<number, boolean>>({});
35
+ const theme = useTheme();
36
+ const isLight = theme.palette.mode === 'light';
37
+
38
+ const getReleaseTitle = (release: IReleaseShort) =>
39
+ release.releaseCategoryName || release.name || '';
40
+
41
+ const getTicketIndexByRelease = (releaseId: number) =>
42
+ tickets.findIndex((ticket) => ticket.releaseId === releaseId);
43
+
44
+ const getReleaseQuantity = (releaseId: number) => {
45
+ const ticket = tickets.find((t) => t.releaseId === releaseId);
46
+ return Number(ticket?.quantity || 0);
47
+ };
48
+
49
+ const isReleaseVisible = (release: IReleaseShort) =>
50
+ !release.locked || getReleaseQuantity(release.id) > 0;
51
+
52
+ const updateReleaseQuantity = (release: IReleaseShort, nextQuantity: number) => {
53
+ const maxAvailable = Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER);
54
+ const clampedQuantity = Math.max(0, Math.min(nextQuantity, maxAvailable));
55
+ const ticketIndex = getTicketIndexByRelease(release.id);
56
+
57
+ if (clampedQuantity <= 0) {
58
+ if (ticketIndex >= 0) removeTicket(ticketIndex);
59
+ return;
60
+ }
61
+
62
+ if (ticketIndex >= 0) {
63
+ setValue(`tickets.${event.id}.${ticketIndex}.quantity`, clampedQuantity);
64
+ return;
65
+ }
66
+
67
+ setValue(`tickets.${event.id}`, [
68
+ ...tickets,
69
+ {
70
+ releaseId: release.id,
71
+ quantity: clampedQuantity,
72
+ itemName: getReleaseTitle(release),
73
+ price: release.price || 0,
74
+ products: [],
75
+ extraFields: [],
76
+ },
77
+ ]);
78
+ };
79
+
80
+ const toggleReleaseDescription = (releaseId: number) =>
81
+ setExpandedReleaseIds((prev) => ({
82
+ ...prev,
83
+ [releaseId]: !prev[releaseId],
84
+ }));
85
+
86
+ if (showLoading) {
87
+ return (
88
+ <Stack spacing={2}>
89
+ {[...Array(2)].map((_, index) => (
90
+ <Skeleton
91
+ key={index}
92
+ variant="rounded"
93
+ sx={{
94
+ width: '100%',
95
+ height: (theme) => theme.spacing(12),
96
+ }}
97
+ />
98
+ ))}
99
+ </Stack>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <Stack spacing={2}>
105
+ {activeReleases
106
+ ?.filter((release) => isReleaseVisible(release))
107
+ .map((release) => {
108
+ const quantity = getReleaseQuantity(release.id);
109
+ const ticketIndex = getTicketIndexByRelease(release.id);
110
+ const maxAvailable = Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER);
111
+ const isLocked = release.locked;
112
+ const nextRelease = activeReleases?.find(
113
+ (item) =>
114
+ item.releaseCategoryName === release.releaseCategoryName &&
115
+ item.order === release.order + 1
116
+ );
117
+ const hasSelectedNextRelease = !!nextRelease && getReleaseQuantity(nextRelease.id) > 0;
118
+ const isDisabled = hasSelectedNextRelease || (isLocked && quantity === 0);
119
+ const canAddFirst = maxAvailable > 0 && !isQuantityDisabled(1, release.id);
120
+ const canAddMore =
121
+ quantity < maxAvailable && !isQuantityDisabled(quantity + 1, release.id);
122
+
123
+ return (
124
+ <Box
125
+ key={release.id}
126
+ sx={{
127
+ pt: 1,
128
+ pr: 0.5,
129
+ pb: 0.5,
130
+ pl: 2,
131
+ borderRadius: 1,
132
+ bgcolor: (theme) => (isLight ? theme.palette.grey[100] : theme.palette.grey[800]),
133
+ }}
134
+ >
135
+ <Stack spacing={0}>
136
+ <Box>
137
+ <Typography variant="subtitle2" fontWeight={700}>
138
+ {getReleaseTitle(release)}
139
+ </Typography>
140
+ </Box>
141
+
142
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
143
+ <Stack>
144
+ <Typography variant="body2">
145
+ {release.price === 0
146
+ ? t('free')
147
+ : fCurrency(release.price, lang, event.currency)}{' '}
148
+ - {release.name}
149
+ </Typography>
150
+
151
+ <ReleaseDescription
152
+ description={release.description}
153
+ isExpanded={Boolean(expandedReleaseIds[release.id])}
154
+ onToggle={() => toggleReleaseDescription(release.id)}
155
+ moreInfoLabel={t('more_info')}
156
+ />
157
+ </Stack>
158
+
159
+ <TicketQuantityControl
160
+ quantity={quantity}
161
+ isDisabled={isDisabled}
162
+ canAddFirst={canAddFirst}
163
+ canAddMore={canAddMore}
164
+ addLabel={t('add')}
165
+ onDecrement={() => updateReleaseQuantity(release, quantity - 1)}
166
+ onIncrement={() => updateReleaseQuantity(release, quantity + 1)}
167
+ onAddFirst={() => updateReleaseQuantity(release, 1)}
168
+ />
169
+ </Stack>
170
+
171
+ <ReleaseDescription
172
+ description={release.description}
173
+ isExpanded={Boolean(expandedReleaseIds[release.id])}
174
+ onToggle={() => toggleReleaseDescription(release.id)}
175
+ moreInfoLabel={t('more_info')}
176
+ showCollapse
177
+ />
178
+
179
+ {ticketIndex >= 0 && getExtraFields(release.id, ticketIndex)}
180
+ </Stack>
181
+ </Box>
182
+ );
183
+ })}
184
+ </Stack>
185
+ );
186
+ };
187
+
188
+ export default TicketSelectionMobile;