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

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 (80) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.env.example +1 -0
  3. package/README.md +18 -16
  4. package/dist/cjs/{index-DvUR1fp8.js → index-BAfaeq84.js} +3230 -554
  5. package/dist/cjs/index-BAfaeq84.js.map +1 -0
  6. package/dist/cjs/index.js +2 -2
  7. package/dist/cjs/{index.umd-6SU6nkkJ.js → index.umd-Bpwd9vUs.js} +9 -19
  8. package/dist/cjs/index.umd-Bpwd9vUs.js.map +1 -0
  9. package/dist/esm/{index-BlTqx0jm.js → index-CJ_gPli9.js} +3217 -540
  10. package/dist/esm/index-CJ_gPli9.js.map +1 -0
  11. package/dist/esm/index.js +2 -2
  12. package/dist/esm/{index.umd-Dn0hjh7E.js → index.umd-ewNTELOK.js} +9 -19
  13. package/dist/esm/index.umd-ewNTELOK.js.map +1 -0
  14. package/dist/types/components/hook-form/FormProvider.d.ts +2 -1
  15. package/dist/types/form/PaymentOverviewBox.d.ts +2 -0
  16. package/dist/types/form/PaymentOverviewDrawer.d.ts +10 -0
  17. package/dist/types/form/TicketForm.d.ts +1 -0
  18. package/dist/types/form/index.d.ts +2 -1
  19. package/dist/types/form/merchandise/MerchandiseSelection.d.ts +9 -0
  20. package/dist/types/form/merchandise/MerchandiseSlider.d.ts +10 -0
  21. package/dist/types/form/payment/PaymentOverviewCheckbox.d.ts +0 -4
  22. package/dist/types/form/product/ProductVariantsDialog.d.ts +2 -1
  23. package/dist/types/form/services/index.d.ts +7 -0
  24. package/dist/types/form/style.d.ts +1 -0
  25. package/dist/types/form/tickets/ReleaseDescription.d.ts +10 -0
  26. package/dist/types/form/tickets/ReleaseWithMerchandise.d.ts +12 -0
  27. package/dist/types/form/tickets/TicketQuantityControl.d.ts +13 -0
  28. package/dist/types/form/tickets/TicketSelectionMobile.d.ts +17 -0
  29. package/dist/types/hooks/useScrollToFirstError.d.ts +4 -0
  30. package/dist/types/locales/cs.d.ts +22 -0
  31. package/dist/types/locales/en.d.ts +22 -0
  32. package/dist/types/locales/es.d.ts +22 -0
  33. package/dist/types/locales/pl.d.ts +22 -0
  34. package/dist/types/locales/sk.d.ts +22 -0
  35. package/dist/types/locales/uk.d.ts +22 -0
  36. package/dist/types/utils/data/global.d.ts +1 -0
  37. package/package.json +10 -4
  38. package/rollup.config.mjs +7 -12
  39. package/src/components/hook-form/FormProvider.tsx +5 -2
  40. package/src/form/ChildEventDialog.tsx +3 -3
  41. package/src/form/ContactPerson.tsx +1 -1
  42. package/src/form/PaymentOverviewBox.tsx +96 -123
  43. package/src/form/PaymentOverviewDrawer.tsx +446 -0
  44. package/src/form/PaymentPending.tsx +19 -4
  45. package/src/form/ReleaseWithMerchandise.tsx +4 -4
  46. package/src/form/Shipping.tsx +91 -74
  47. package/src/form/TicketForm.tsx +144 -41
  48. package/src/form/index.tsx +3 -1
  49. package/src/form/merchandise/MerchandiseSelection.tsx +24 -0
  50. package/src/form/merchandise/MerchandiseSlider.tsx +62 -0
  51. package/src/form/payment/FeeBox.tsx +4 -31
  52. package/src/form/payment/PaymentOverviewCheckbox.tsx +57 -56
  53. package/src/form/product/ProductCard.tsx +255 -59
  54. package/src/form/product/ProductVariantsDialog.tsx +271 -141
  55. package/src/form/services/index.tsx +263 -0
  56. package/src/form/style.ts +16 -4
  57. package/src/form/tickets/ReleaseDescription.tsx +46 -0
  58. package/src/form/tickets/ReleaseWithMerchandise.tsx +231 -0
  59. package/src/form/tickets/TicketQuantityControl.tsx +100 -0
  60. package/src/form/{TicketSelection.tsx → tickets/TicketSelection.tsx} +24 -128
  61. package/src/form/{TicketSelectionMap.tsx → tickets/TicketSelectionMap.tsx} +9 -1
  62. package/src/form/tickets/TicketSelectionMobile.tsx +177 -0
  63. package/src/form/{TicketWithMerchandiseSelection.tsx → tickets/TicketWithMerchandiseSelection.tsx} +3 -7
  64. package/src/hooks/useScrollToFirstError.ts +99 -0
  65. package/src/locales/cs.tsx +25 -3
  66. package/src/locales/en.tsx +23 -1
  67. package/src/locales/es.tsx +23 -1
  68. package/src/locales/pl.tsx +23 -1
  69. package/src/locales/sk.tsx +24 -2
  70. package/src/locales/uk.tsx +23 -1
  71. package/src/utils/data/global.ts +1 -0
  72. package/tsconfig.json +1 -1
  73. package/README +0 -1
  74. package/dist/cjs/index-DvUR1fp8.js.map +0 -1
  75. package/dist/cjs/index.umd-6SU6nkkJ.js.map +0 -1
  76. package/dist/esm/index-BlTqx0jm.js.map +0 -1
  77. package/dist/esm/index.umd-Dn0hjh7E.js.map +0 -1
  78. /package/dist/types/form/{TicketSelection.d.ts → tickets/TicketSelection.d.ts} +0 -0
  79. /package/dist/types/form/{TicketSelectionMap.d.ts → tickets/TicketSelectionMap.d.ts} +0 -0
  80. /package/dist/types/form/{TicketWithMerchandiseSelection.d.ts → tickets/TicketWithMerchandiseSelection.d.ts} +0 -0
@@ -0,0 +1,263 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ Box,
4
+ Button,
5
+ Checkbox,
6
+ Dialog,
7
+ DialogContent,
8
+ DialogTitle,
9
+ Divider,
10
+ FormControlLabel,
11
+ IconButton,
12
+ Link,
13
+ Stack,
14
+ Typography,
15
+ } from '@mui/material';
16
+ import { useWatch, Controller } from 'react-hook-form';
17
+ import { fCurrency } from '@utils/formatNumber';
18
+ import useGlobal from '@hooks/useGlobal';
19
+ import { Iconify } from '@components';
20
+ import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
21
+ import { IEvent } from '@utils/types/event.type';
22
+ import { useFormContext } from 'react-hook-form';
23
+
24
+ interface Props {
25
+ event: IEvent;
26
+ }
27
+
28
+ interface BorderedCheckboxProps {
29
+ name: string;
30
+ disabled: boolean;
31
+ title: React.ReactNode;
32
+ price: React.ReactNode;
33
+ onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
34
+ }
35
+
36
+ const BorderedCheckbox: React.FC<BorderedCheckboxProps> = ({
37
+ name,
38
+ disabled,
39
+ title,
40
+ price,
41
+ onChange,
42
+ }) => {
43
+ const { control } = useFormContext<ITicketForm>();
44
+
45
+ return (
46
+ <Controller
47
+ name={name as any}
48
+ control={control}
49
+ render={({ field }) => (
50
+ <FormControlLabel
51
+ control={
52
+ <Checkbox
53
+ {...field}
54
+ checked={field.value || false}
55
+ onChange={(e) => {
56
+ onChange?.(e);
57
+ if (!e.defaultPrevented) {
58
+ field.onChange(e);
59
+ }
60
+ }}
61
+ disabled={disabled}
62
+ />
63
+ }
64
+ label={
65
+ <Stack
66
+ direction="row"
67
+ justifyContent="space-between"
68
+ alignItems="center"
69
+ sx={{ width: '100%', pr: 1 }}
70
+ >
71
+ <Typography variant="body2" fontWeight={400}>
72
+ {title}
73
+ </Typography>
74
+ <Typography variant="body2" fontWeight={700} sx={{ whiteSpace: 'nowrap' }}>
75
+ {price}
76
+ </Typography>
77
+ </Stack>
78
+ }
79
+ sx={{
80
+ m: 0,
81
+ px: 1,
82
+ pr: 0.5,
83
+ pl: 0,
84
+ borderRadius: 1,
85
+ width: '100%',
86
+ border: '1px solid',
87
+ borderColor: 'primary.main',
88
+ '& .MuiFormControlLabel-label': {
89
+ width: '100%',
90
+ },
91
+ }}
92
+ />
93
+ )}
94
+ />
95
+ );
96
+ };
97
+
98
+ const Services: React.FC<Props> = ({ event }) => {
99
+ const { t, lang, showSnackbar } = useGlobal();
100
+ const [open, setOpen] = useState(false);
101
+ const ticketInsurancePricePerUnit = Number(
102
+ useWatch<ITicketForm>({
103
+ name: 'ticketInsurancePricePerUnit',
104
+ defaultValue: 0,
105
+ }) || 0
106
+ );
107
+ const smsNotificationPrice = Number(
108
+ useWatch<ITicketForm>({
109
+ name: 'smsNotificationPrice',
110
+ defaultValue: 0,
111
+ }) || 0
112
+ );
113
+ const tickets = useWatch<ITicketForm, 'tickets'>({
114
+ name: 'tickets',
115
+ defaultValue: {},
116
+ });
117
+
118
+ const totalTickets = useMemo(() => {
119
+ const items: ITicketFormTicket[] = Object.values(tickets || {}).flat();
120
+ return items.reduce((sum, ticket) => sum + (Number(ticket.quantity) || 0), 0);
121
+ }, [tickets]);
122
+
123
+ const handleOpen = (e: React.MouseEvent<HTMLAnchorElement>) => {
124
+ e.preventDefault();
125
+ setOpen(true);
126
+ };
127
+
128
+ const handleClose = () => setOpen(false);
129
+
130
+ const handleServiceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
131
+ if (totalTickets <= 0) {
132
+ e.preventDefault();
133
+ showSnackbar(t('event.tickets.services.add_tickets_first'), {
134
+ variant: 'error',
135
+ });
136
+ }
137
+ };
138
+
139
+ return (
140
+ <Stack spacing={1}>
141
+ {/*<BorderedCheckbox*/}
142
+ {/* name="ticketInsurance"*/}
143
+ {/* disabled={false}*/}
144
+ {/* title={t('event.tickets.insurance.label')}*/}
145
+ {/* price={*/}
146
+ {/* <>*/}
147
+ {/* + {fCurrency(ticketInsurancePricePerUnit, lang, event.currency)} /{' '}*/}
148
+ {/* {t('event.tickets.insurance.per_ticket')}*/}
149
+ {/* </>*/}
150
+ {/* }*/}
151
+ {/* onChange={handleServiceChange}*/}
152
+ {/*/>*/}
153
+
154
+ <BorderedCheckbox
155
+ name="smsNotification"
156
+ disabled={false}
157
+ title={t('event.tickets.sms_notification.label')}
158
+ price={<>+ {fCurrency(smsNotificationPrice, lang, event.currency)}</>}
159
+ onChange={handleServiceChange}
160
+ />
161
+
162
+ <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
163
+ <Link variant="body2" underline="always" onClick={handleOpen} color={'text.primary'}>
164
+ {t('event.tickets.stepper.8.additional_info')}
165
+ </Link>
166
+ </Box>
167
+
168
+ <Dialog
169
+ open={open}
170
+ onClose={handleClose}
171
+ maxWidth="sm"
172
+ fullWidth
173
+ PaperProps={{
174
+ sx: {
175
+ borderRadius: 4,
176
+ overflow: 'hidden',
177
+ },
178
+ }}
179
+ >
180
+ <DialogTitle
181
+ sx={{
182
+ position: 'relative',
183
+ textAlign: 'center',
184
+ fontWeight: 700,
185
+ fontSize: { xs: '1.5rem', sm: '1.75rem' },
186
+ pt: { xs: 3.5, sm: 4 },
187
+ pb: { xs: 2, sm: 2.5 },
188
+ px: { xs: 3, sm: 4 },
189
+ }}
190
+ >
191
+ {t('event.tickets.stepper.8.title')}:
192
+ <IconButton
193
+ onClick={handleClose}
194
+ size="small"
195
+ sx={{ position: 'absolute', right: 16, top: 16 }}
196
+ >
197
+ <Iconify icon="carbon:close" />
198
+ </IconButton>
199
+ </DialogTitle>
200
+ <DialogContent sx={{ px: { xs: 3, sm: 4 }, pb: { xs: 4, sm: 5 } }}>
201
+ <Stack spacing={3}>
202
+ {/*<Box>*/}
203
+ {/* <Typography variant="subtitle2" fontWeight={700} gutterBottom>*/}
204
+ {/* {t('event.tickets.insurance.label')}*/}
205
+ {/* </Typography>*/}
206
+ {/* <Typography variant="body2" paragraph>*/}
207
+ {/* {t('event.tickets.insurance.modal.description')}*/}
208
+ {/* </Typography>*/}
209
+ {/* <Typography variant="body2" paragraph>*/}
210
+ {/* {t('event.tickets.insurance.modal.coverage')}*/}
211
+ {/* </Typography>*/}
212
+ {/* <BorderedCheckbox*/}
213
+ {/* name="ticketInsurance"*/}
214
+ {/* disabled={false}*/}
215
+ {/* title={t('event.tickets.insurance.label')}*/}
216
+ {/* price={*/}
217
+ {/* <>*/}
218
+ {/* + {fCurrency(ticketInsurancePricePerUnit, lang, event.currency)} /{' '}*/}
219
+ {/* {t('event.tickets.insurance.per_ticket')}*/}
220
+ {/* </>*/}
221
+ {/* }*/}
222
+ {/* onChange={handleServiceChange}*/}
223
+ {/* />*/}
224
+ {/*</Box>*/}
225
+ {/*<Divider />*/}
226
+ <Box>
227
+ <Typography variant="subtitle2" fontWeight={700} gutterBottom>
228
+ {t('event.tickets.sms_notification.label')}
229
+ </Typography>
230
+ <Typography variant="body2" paragraph>
231
+ {t('event.tickets.sms_notification.modal.description')}
232
+ </Typography>
233
+ <BorderedCheckbox
234
+ name="smsNotification"
235
+ disabled={false}
236
+ title={t('event.tickets.sms_notification.label')}
237
+ price={<>+ {fCurrency(smsNotificationPrice, lang, event.currency)}</>}
238
+ onChange={handleServiceChange}
239
+ />
240
+ </Box>
241
+ <Button
242
+ variant="outlined"
243
+ onClick={handleClose}
244
+ sx={{
245
+ mt: 1,
246
+ py: 1.25,
247
+ borderRadius: 1,
248
+ height: 36,
249
+ fontWeight: 700,
250
+ borderColor: 'grey.300',
251
+ color: 'text.primary',
252
+ }}
253
+ >
254
+ {t('close')}
255
+ </Button>
256
+ </Stack>
257
+ </DialogContent>
258
+ </Dialog>
259
+ </Stack>
260
+ );
261
+ };
262
+
263
+ export default Services;
package/src/form/style.ts CHANGED
@@ -5,17 +5,29 @@ export const OverviewCard = styled(Card)<{ stickyHeaderTop: number }>(
5
5
  ({ theme, stickyHeaderTop }) => ({
6
6
  position: 'sticky',
7
7
  top: stickyHeaderTop,
8
+ borderRadius: theme.spacing(2),
9
+
10
+ [theme.breakpoints.down('sm')]: {
11
+ borderRadius: theme.spacing(1),
12
+ boxShadow: 'none',
13
+ },
8
14
  })
9
15
  );
10
16
 
11
17
  export const ShippingMethodItem = styled(Box, {
12
- shouldForwardProp: (prop) => prop !== 'active',
13
- })<{ active: boolean }>(({ theme, active }) => ({
18
+ shouldForwardProp: (prop) => prop !== 'active' && prop !== 'hasError',
19
+ })<{ active: boolean; hasError?: boolean }>(({ theme, active, hasError }) => ({
14
20
  borderRadius: theme.spacing(1),
15
- border: `1px solid ${active ? theme.palette.primary.main : theme.palette.grey.A200}`,
21
+ border: `1px solid ${
22
+ hasError
23
+ ? theme.palette.error.main
24
+ : active
25
+ ? theme.palette.primary.main
26
+ : theme.palette.grey.A200
27
+ }`,
16
28
  marginTop: theme.spacing(1),
17
29
  width: '100%',
18
- padding: `0 ${theme.spacing(2)}`,
30
+ padding: `0 0 0 ${theme.spacing(2)}`,
19
31
  transitionDuration: '.3s',
20
32
 
21
33
  '& > .MuiFormControlLabel-root': {
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { Box, Collapse, Link, Stack, Typography } from '@mui/material';
3
+ import { Iconify } from '@components/iconify';
4
+
5
+ interface ReleaseDescriptionProps {
6
+ description?: string | null;
7
+ isExpanded: boolean;
8
+ onToggle: () => void;
9
+ moreInfoLabel: string;
10
+ showCollapse?: boolean;
11
+ }
12
+
13
+ const ReleaseDescription: React.FC<ReleaseDescriptionProps> = ({
14
+ description,
15
+ isExpanded,
16
+ onToggle,
17
+ moreInfoLabel,
18
+ showCollapse = false,
19
+ }) => {
20
+ if (!description) return null;
21
+
22
+ if (showCollapse) {
23
+ return (
24
+ <Collapse in={isExpanded}>
25
+ <Typography variant="body2" color="text.secondary">
26
+ {description}
27
+ </Typography>
28
+ </Collapse>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <Link onClick={onToggle} color="inherit" underline="always" fontSize={12}>
34
+ <Stack direction="row" alignItems="center" spacing={0}>
35
+ <Box>{moreInfoLabel}</Box>
36
+
37
+ <Iconify
38
+ sx={{ width: 24, height: 24 }}
39
+ icon={isExpanded ? 'eva:chevron-up-fill' : 'eva:chevron-down-fill'}
40
+ />
41
+ </Stack>
42
+ </Link>
43
+ );
44
+ };
45
+
46
+ export default ReleaseDescription;
@@ -0,0 +1,231 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Box, Stack, Typography } from '@mui/material';
3
+ import ProductVariantsDialog from '@form/product/ProductVariantsDialog';
4
+ import TicketQuantityControl from '@form/tickets/TicketQuantityControl';
5
+ import { IReleaseShort } from '@utils/types/release.type';
6
+ import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
7
+ import { useFormContext, useWatch } from 'react-hook-form';
8
+ import { IEventProductForm } from '@utils/types/product.type';
9
+ import { fCurrency } from '@utils/formatNumber';
10
+ import { Currencies } from '@utils/data/currency';
11
+ import { getSelectedQuantityByVariant } from '@utils/product';
12
+ import ReleaseExtraFields from '@form/extra-field/ReleaseExtraFields';
13
+ import ReleaseDescription from '@form/tickets/ReleaseDescription';
14
+ import useGlobal from '@hooks/useGlobal';
15
+
16
+ interface Props {
17
+ eventId: number;
18
+ release: IReleaseShort;
19
+ activeReleases: IReleaseShort[];
20
+ currency: Currencies;
21
+ index: number;
22
+ }
23
+
24
+ const ReleaseWithMerchandise: React.FC<Props> = ({
25
+ eventId,
26
+ release,
27
+ activeReleases,
28
+ currency,
29
+ index,
30
+ }) => {
31
+ const { t, lang } = useGlobal();
32
+ const [openVariantDialog, setOpenVariantDialog] = useState<'add' | 'increase' | null>(null);
33
+ const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
34
+ const { setValue } = useFormContext<ITicketForm>();
35
+ const tickets: ITicketFormTicket[] = useWatch({ name: `tickets.${eventId}`, defaultValue: [] });
36
+ const products: IEventProductForm[] = useWatch({ name: `products.${eventId}`, defaultValue: [] });
37
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
38
+ const countTickets = addedRelease?.quantity || 0;
39
+
40
+ const getReleaseTitle = (release: IReleaseShort) =>
41
+ release.releaseCategoryName || release.name || '';
42
+
43
+ const getSelectedQuantity = (id: number) =>
44
+ tickets.find((ticket) => ticket.releaseId === id)?.quantity || 0;
45
+
46
+ const getAvailableTicketsForRelease = (release: ITicketFormTicket): number => {
47
+ const selectedRelease = activeReleases?.find((item) => item.id === release.releaseId);
48
+ const availableQuantity = selectedRelease ? selectedRelease.availableTickets : 0;
49
+ return availableQuantity > 10 ? 10 : availableQuantity;
50
+ };
51
+
52
+ const isMaxQuantity = (releaseId: number) => {
53
+ const release = tickets.find((ticket) => ticket.releaseId === releaseId);
54
+ if (!release) return false;
55
+ return getSelectedQuantity(releaseId) >= getAvailableTicketsForRelease(release);
56
+ };
57
+
58
+ const addRelease = (productsToAdd?: IEventProductForm[]) => {
59
+ const normalizedProducts = productsToAdd ?? [];
60
+ const quantity = normalizedProducts.length ? normalizedProducts.length : 1;
61
+ const extraFields = release.extraFields?.length
62
+ ? Array.from({ length: quantity }, () =>
63
+ release.extraFields!.map((field) => ({
64
+ eventExtraFieldId: field.id,
65
+ value: '',
66
+ }))
67
+ )
68
+ : [];
69
+
70
+ setValue(`tickets.${eventId}`, [
71
+ ...tickets,
72
+ {
73
+ releaseId: release.id,
74
+ quantity,
75
+ itemName: '',
76
+ price: 0,
77
+ products: normalizedProducts,
78
+ extraFields,
79
+ },
80
+ ]);
81
+ setOpenVariantDialog(null);
82
+ };
83
+
84
+ const increaseQuantity = (productsToAdd?: IEventProductForm[]) => {
85
+ const normalizedProducts = productsToAdd ?? [];
86
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
87
+ if (addedRelease) {
88
+ const increment = normalizedProducts.length ? normalizedProducts.length : 1;
89
+ const maxQuantity = getAvailableTicketsForRelease(addedRelease);
90
+ const availableIncrement = Math.max(
91
+ 0,
92
+ Math.min(increment, maxQuantity - Number(addedRelease.quantity))
93
+ );
94
+ if (availableIncrement === 0) return;
95
+
96
+ const newQuantity = Number(addedRelease.quantity) + availableIncrement;
97
+ const productsSlice = normalizedProducts.slice(0, availableIncrement);
98
+ const extraFieldsToAdd = release.extraFields?.length
99
+ ? Array.from({ length: availableIncrement }, () =>
100
+ release.extraFields!.map((field) => ({
101
+ eventExtraFieldId: field.id,
102
+ value: '',
103
+ }))
104
+ )
105
+ : [];
106
+
107
+ setValue(
108
+ `tickets.${eventId}`,
109
+ tickets.map((ticket) =>
110
+ ticket.releaseId === release.id
111
+ ? {
112
+ ...ticket,
113
+ quantity: newQuantity > maxQuantity ? maxQuantity : newQuantity,
114
+ products: [...ticket.products, ...productsSlice],
115
+ extraFields: release.extraFields?.length
116
+ ? [...ticket.extraFields, ...extraFieldsToAdd]
117
+ : [],
118
+ }
119
+ : ticket
120
+ )
121
+ );
122
+ setOpenVariantDialog(null);
123
+ }
124
+ };
125
+
126
+ const decreaseQuantity = useCallback(() => {
127
+ const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
128
+ if (addedRelease) {
129
+ const newQuantity = Number(addedRelease.quantity) - 1;
130
+ if (newQuantity < 1) {
131
+ setValue(
132
+ `tickets.${eventId}`,
133
+ tickets.filter((ticket) => ticket.releaseId !== release.id)
134
+ );
135
+ } else {
136
+ setValue(
137
+ `tickets.${eventId}`,
138
+ tickets.map((ticket) => {
139
+ if (ticket.releaseId !== release.id) return ticket;
140
+
141
+ return {
142
+ ...ticket,
143
+ quantity: newQuantity,
144
+ products: ticket?.products?.slice(0, -1), // non-mutating "pop"
145
+ extraFields: ticket?.extraFields?.slice(0, -1),
146
+ };
147
+ })
148
+ );
149
+ }
150
+ }
151
+ }, [tickets, release.id, setValue]);
152
+
153
+ return (
154
+ <Box
155
+ sx={{
156
+ pt: 1,
157
+ pr: 0.5,
158
+ pb: 0.5,
159
+ pl: 2,
160
+ borderRadius: 1,
161
+ bgcolor: (theme) =>
162
+ theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[800],
163
+ }}
164
+ >
165
+ <Stack spacing={0}>
166
+ <Box>
167
+ <Typography variant="subtitle2" fontWeight={700}>
168
+ {getReleaseTitle(release)}
169
+ </Typography>
170
+ </Box>
171
+
172
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
173
+ <Stack>
174
+ <Typography variant="body2">
175
+ {release.price === 0 ? t('free') : fCurrency(release.price, lang, currency)}
176
+ </Typography>
177
+
178
+ <ReleaseDescription
179
+ description={release.description}
180
+ isExpanded={isDescriptionExpanded}
181
+ onToggle={() => setIsDescriptionExpanded((prev) => !prev)}
182
+ moreInfoLabel={t('more_info')}
183
+ />
184
+ </Stack>
185
+
186
+ <TicketQuantityControl
187
+ quantity={getSelectedQuantity(release.id)}
188
+ isDisabled={release.locked}
189
+ canAddFirst={!release.locked}
190
+ canAddMore={!isMaxQuantity(release.id)}
191
+ addLabel={t('add')}
192
+ onDecrement={() => decreaseQuantity()}
193
+ onIncrement={() =>
194
+ release.product ? setOpenVariantDialog('increase') : increaseQuantity()
195
+ }
196
+ onAddFirst={() => (release.product ? setOpenVariantDialog('add') : addRelease())}
197
+ />
198
+ </Stack>
199
+
200
+ <ReleaseDescription
201
+ description={release.description}
202
+ isExpanded={isDescriptionExpanded}
203
+ onToggle={() => setIsDescriptionExpanded((prev) => !prev)}
204
+ moreInfoLabel={t('more_info')}
205
+ showCollapse
206
+ />
207
+ {release.extraFields && release.extraFields.length > 0 && (
208
+ <ReleaseExtraFields
209
+ release={release}
210
+ eventId={eventId}
211
+ releaseIndex={index}
212
+ quantity={countTickets}
213
+ />
214
+ )}
215
+ {release.product && (
216
+ <ProductVariantsDialog
217
+ eventProduct={release.product}
218
+ openDialog={!!openVariantDialog}
219
+ callback={openVariantDialog === 'increase' ? increaseQuantity : addRelease}
220
+ onClose={() => setOpenVariantDialog(null)}
221
+ selectedQuantityByVariant={getSelectedQuantityByVariant(products, tickets)}
222
+ eventId={eventId}
223
+ canAddOnlyOneAtATime
224
+ />
225
+ )}
226
+ </Stack>
227
+ </Box>
228
+ );
229
+ };
230
+
231
+ export default ReleaseWithMerchandise;
@@ -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;