@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.
- package/.claude/settings.local.json +2 -1
- package/.env.example +1 -0
- package/README.md +18 -16
- package/dist/cjs/{index-DvUR1fp8.js → index-BAfaeq84.js} +3230 -554
- package/dist/cjs/index-BAfaeq84.js.map +1 -0
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/{index.umd-6SU6nkkJ.js → index.umd-Bpwd9vUs.js} +9 -19
- package/dist/cjs/index.umd-Bpwd9vUs.js.map +1 -0
- package/dist/esm/{index-BlTqx0jm.js → index-CJ_gPli9.js} +3217 -540
- package/dist/esm/index-CJ_gPli9.js.map +1 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/{index.umd-Dn0hjh7E.js → index.umd-ewNTELOK.js} +9 -19
- package/dist/esm/index.umd-ewNTELOK.js.map +1 -0
- package/dist/types/components/hook-form/FormProvider.d.ts +2 -1
- package/dist/types/form/PaymentOverviewBox.d.ts +2 -0
- package/dist/types/form/PaymentOverviewDrawer.d.ts +10 -0
- package/dist/types/form/TicketForm.d.ts +1 -0
- package/dist/types/form/index.d.ts +2 -1
- package/dist/types/form/merchandise/MerchandiseSelection.d.ts +9 -0
- package/dist/types/form/merchandise/MerchandiseSlider.d.ts +10 -0
- package/dist/types/form/payment/PaymentOverviewCheckbox.d.ts +0 -4
- package/dist/types/form/product/ProductVariantsDialog.d.ts +2 -1
- package/dist/types/form/services/index.d.ts +7 -0
- package/dist/types/form/style.d.ts +1 -0
- package/dist/types/form/tickets/ReleaseDescription.d.ts +10 -0
- package/dist/types/form/tickets/ReleaseWithMerchandise.d.ts +12 -0
- package/dist/types/form/tickets/TicketQuantityControl.d.ts +13 -0
- package/dist/types/form/tickets/TicketSelectionMobile.d.ts +17 -0
- package/dist/types/hooks/useScrollToFirstError.d.ts +4 -0
- package/dist/types/locales/cs.d.ts +22 -0
- package/dist/types/locales/en.d.ts +22 -0
- package/dist/types/locales/es.d.ts +22 -0
- package/dist/types/locales/pl.d.ts +22 -0
- package/dist/types/locales/sk.d.ts +22 -0
- package/dist/types/locales/uk.d.ts +22 -0
- package/dist/types/utils/data/global.d.ts +1 -0
- package/package.json +10 -4
- package/rollup.config.mjs +7 -12
- package/src/components/hook-form/FormProvider.tsx +5 -2
- package/src/form/ChildEventDialog.tsx +3 -3
- package/src/form/ContactPerson.tsx +1 -1
- package/src/form/PaymentOverviewBox.tsx +96 -123
- package/src/form/PaymentOverviewDrawer.tsx +446 -0
- package/src/form/PaymentPending.tsx +19 -4
- package/src/form/ReleaseWithMerchandise.tsx +4 -4
- package/src/form/Shipping.tsx +91 -74
- package/src/form/TicketForm.tsx +144 -41
- package/src/form/index.tsx +3 -1
- package/src/form/merchandise/MerchandiseSelection.tsx +24 -0
- package/src/form/merchandise/MerchandiseSlider.tsx +62 -0
- package/src/form/payment/FeeBox.tsx +4 -31
- package/src/form/payment/PaymentOverviewCheckbox.tsx +57 -56
- package/src/form/product/ProductCard.tsx +255 -59
- package/src/form/product/ProductVariantsDialog.tsx +271 -141
- package/src/form/services/index.tsx +263 -0
- package/src/form/style.ts +16 -4
- package/src/form/tickets/ReleaseDescription.tsx +46 -0
- package/src/form/tickets/ReleaseWithMerchandise.tsx +231 -0
- package/src/form/tickets/TicketQuantityControl.tsx +100 -0
- package/src/form/{TicketSelection.tsx → tickets/TicketSelection.tsx} +24 -128
- package/src/form/{TicketSelectionMap.tsx → tickets/TicketSelectionMap.tsx} +9 -1
- package/src/form/tickets/TicketSelectionMobile.tsx +177 -0
- package/src/form/{TicketWithMerchandiseSelection.tsx → tickets/TicketWithMerchandiseSelection.tsx} +3 -7
- package/src/hooks/useScrollToFirstError.ts +99 -0
- package/src/locales/cs.tsx +25 -3
- package/src/locales/en.tsx +23 -1
- package/src/locales/es.tsx +23 -1
- package/src/locales/pl.tsx +23 -1
- package/src/locales/sk.tsx +24 -2
- package/src/locales/uk.tsx +23 -1
- package/src/utils/data/global.ts +1 -0
- package/tsconfig.json +1 -1
- package/README +0 -1
- package/dist/cjs/index-DvUR1fp8.js.map +0 -1
- package/dist/cjs/index.umd-6SU6nkkJ.js.map +0 -1
- package/dist/esm/index-BlTqx0jm.js.map +0 -1
- package/dist/esm/index.umd-Dn0hjh7E.js.map +0 -1
- /package/dist/types/form/{TicketSelection.d.ts → tickets/TicketSelection.d.ts} +0 -0
- /package/dist/types/form/{TicketSelectionMap.d.ts → tickets/TicketSelectionMap.d.ts} +0 -0
- /package/dist/types/form/{TicketWithMerchandiseSelection.d.ts → tickets/TicketWithMerchandiseSelection.d.ts} +0 -0
|
@@ -1,36 +1,23 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
3
|
-
import {
|
|
4
|
-
Box,
|
|
5
|
-
Button,
|
|
6
|
-
Divider,
|
|
7
|
-
Grid,
|
|
8
|
-
IconButton,
|
|
9
|
-
MenuItem,
|
|
10
|
-
Skeleton,
|
|
11
|
-
Stack,
|
|
12
|
-
Typography,
|
|
13
|
-
} from '@mui/material';
|
|
14
|
-
import { RHFSelect } from '@components/hook-form';
|
|
3
|
+
import { Box, Divider, Stack, Typography } from '@mui/material';
|
|
15
4
|
import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
|
|
16
5
|
import useEventActiveReleases from '@hooks/data/useEventActiveReleases';
|
|
17
|
-
import { fCurrency } from '@utils/formatNumber';
|
|
18
|
-
import { Iconify } from '@components/iconify';
|
|
19
6
|
import { groupBy } from '@utils/global';
|
|
20
7
|
import { IReleaseShort } from '@utils/types/release.type';
|
|
21
|
-
import FeeBox from '@form/payment/FeeBox';
|
|
22
8
|
import { IEvent } from '@utils/types/event.type';
|
|
23
9
|
import useResponsive from '@hooks/useResponsive';
|
|
24
10
|
import ReleaseExtraFields from '@form/extra-field/ReleaseExtraFields';
|
|
25
11
|
import { EventType } from '@utils/data/event';
|
|
26
12
|
import useGlobal from '@hooks/useGlobal';
|
|
13
|
+
import TicketSelectionMobile from './TicketSelectionMobile';
|
|
27
14
|
|
|
28
15
|
interface Props {
|
|
29
16
|
event: IEvent;
|
|
30
17
|
}
|
|
31
18
|
|
|
32
19
|
const TicketSelection: React.FC<Props> = ({ event }) => {
|
|
33
|
-
const { t
|
|
20
|
+
const { t } = useGlobal();
|
|
34
21
|
const isMobile = useResponsive('down', 'md');
|
|
35
22
|
const { setValue, watch } = useFormContext<ITicketForm>();
|
|
36
23
|
const tickets = useWatch({
|
|
@@ -78,11 +65,11 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
|
|
|
78
65
|
);
|
|
79
66
|
};
|
|
80
67
|
|
|
81
|
-
const getAvailableTicketsForRelease = (release: ITicketFormTicket): number => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
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 > 10 ? 10 : availableQuantity;
|
|
72
|
+
// };
|
|
86
73
|
|
|
87
74
|
const countReleaseCategories = (): number => {
|
|
88
75
|
const grouped = groupBy(activeReleases || [], 'releaseCategoryName');
|
|
@@ -188,117 +175,26 @@ const TicketSelection: React.FC<Props> = ({ event }) => {
|
|
|
188
175
|
};
|
|
189
176
|
|
|
190
177
|
return (
|
|
191
|
-
<Stack
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
value={item.releaseId}
|
|
208
|
-
label={
|
|
209
|
-
index > 0
|
|
210
|
-
? t('form.labels.add_another_release')
|
|
211
|
-
: t('form.labels.release_category_price')
|
|
212
|
-
}
|
|
213
|
-
maxHeight="calc(100vh - 2rem)"
|
|
214
|
-
onChange={(e) => {
|
|
215
|
-
setValue(`tickets.${event.id}.${index}.releaseId`, Number(e.target.value));
|
|
216
|
-
setValue(`tickets.${event.id}.${index}.extraFields`, []);
|
|
217
|
-
}}
|
|
218
|
-
>
|
|
219
|
-
<MenuItem key={0} value="">
|
|
220
|
-
{t('choose')}
|
|
221
|
-
</MenuItem>
|
|
222
|
-
{activeReleases?.map((activeRelease) => (
|
|
223
|
-
<MenuItem
|
|
224
|
-
key={activeRelease.id}
|
|
225
|
-
value={activeRelease.id}
|
|
226
|
-
disabled={
|
|
227
|
-
isReleaseSelected(activeRelease.id) ||
|
|
228
|
-
(activeRelease.locked &&
|
|
229
|
-
!soldOutReleaseCategoryNames.includes(activeRelease.releaseCategoryName))
|
|
230
|
-
}
|
|
231
|
-
>
|
|
232
|
-
{activeRelease.releaseCategoryName} - {activeRelease.name}:{' '}
|
|
233
|
-
{activeRelease.price === 0
|
|
234
|
-
? t('free')
|
|
235
|
-
: fCurrency(activeRelease.price, lang, event.currency)}
|
|
236
|
-
</MenuItem>
|
|
237
|
-
))}
|
|
238
|
-
</RHFSelect>
|
|
239
|
-
)}
|
|
240
|
-
</Grid>
|
|
241
|
-
<Grid key={index} size={{ xs: 12, md: 6 }}>
|
|
242
|
-
{showLoading ? (
|
|
243
|
-
<Skeleton
|
|
244
|
-
variant="rounded"
|
|
245
|
-
sx={{
|
|
246
|
-
width: '100%',
|
|
247
|
-
height: (theme) => theme.spacing(7.5),
|
|
248
|
-
}}
|
|
249
|
-
/>
|
|
250
|
-
) : (
|
|
251
|
-
<Stack direction="row" alignItems="center" spacing={1}>
|
|
252
|
-
<RHFSelect
|
|
253
|
-
name={`tickets.${event.id}.${index}.quantity`}
|
|
254
|
-
value={item.quantity}
|
|
255
|
-
label={t('form.labels.quantity')}
|
|
256
|
-
>
|
|
257
|
-
{[...Array(getAvailableTicketsForRelease(item))].map((_, index2) => (
|
|
258
|
-
<MenuItem
|
|
259
|
-
key={index2}
|
|
260
|
-
value={index2 + 1}
|
|
261
|
-
disabled={isQuantityDisabled(index2 + 1, item.releaseId)}
|
|
262
|
-
>
|
|
263
|
-
{index2 + 1}
|
|
264
|
-
</MenuItem>
|
|
265
|
-
))}
|
|
266
|
-
{!item.releaseId && (
|
|
267
|
-
<MenuItem disabled sx={{ textTransform: 'unset!important' }}>
|
|
268
|
-
{t('event.tickets.stepper.1.quantity_select')}
|
|
269
|
-
</MenuItem>
|
|
270
|
-
)}
|
|
271
|
-
</RHFSelect>
|
|
272
|
-
{item.releaseId && item.quantity && (
|
|
273
|
-
<Box>
|
|
274
|
-
<IconButton color="primary" onClick={() => removeTicket(index)}>
|
|
275
|
-
<Iconify icon="carbon:trash-can" />
|
|
276
|
-
</IconButton>
|
|
277
|
-
</Box>
|
|
278
|
-
)}
|
|
279
|
-
</Stack>
|
|
280
|
-
)}
|
|
281
|
-
</Grid>
|
|
282
|
-
</Grid>
|
|
283
|
-
{activeReleases && item.releaseId && (
|
|
284
|
-
<Typography
|
|
285
|
-
variant="caption"
|
|
286
|
-
content="div"
|
|
287
|
-
mt={2}
|
|
288
|
-
mb={getRelease(item.releaseId)?.extraFields?.length ? 2 : 0}
|
|
289
|
-
display="block"
|
|
290
|
-
>
|
|
291
|
-
{getRelease(item.releaseId)?.description ?? ''}
|
|
292
|
-
</Typography>
|
|
293
|
-
)}
|
|
294
|
-
{getExtraFields(item.releaseId, index)}
|
|
295
|
-
</Box>
|
|
296
|
-
))}
|
|
178
|
+
<Stack
|
|
179
|
+
spacing={3}
|
|
180
|
+
direction="column"
|
|
181
|
+
divider={!isMobile ? <Divider sx={{ borderStyle: 'dashed' }} /> : undefined}
|
|
182
|
+
>
|
|
183
|
+
<TicketSelectionMobile
|
|
184
|
+
event={event}
|
|
185
|
+
activeReleases={activeReleases}
|
|
186
|
+
showLoading={showLoading}
|
|
187
|
+
soldOutReleaseCategoryNames={soldOutReleaseCategoryNames}
|
|
188
|
+
tickets={tickets}
|
|
189
|
+
isQuantityDisabled={isQuantityDisabled}
|
|
190
|
+
setValue={setValue as (name: string, value: any) => void}
|
|
191
|
+
removeTicket={removeTicket}
|
|
192
|
+
getExtraFields={getExtraFields}
|
|
193
|
+
/>
|
|
297
194
|
<Box>
|
|
298
195
|
<Typography variant="caption" component="div" fontStyle="italic" mb={2}>
|
|
299
196
|
*{t('event.tickets.stepper.1.max_ticket_quantity')}
|
|
300
197
|
</Typography>
|
|
301
|
-
{isMobile && <FeeBox event={event} align="right" />}
|
|
302
198
|
</Box>
|
|
303
199
|
</Stack>
|
|
304
200
|
);
|
|
@@ -5,6 +5,7 @@ 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 Iconify from '@components/iconify/Iconify';
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
event: IEvent;
|
|
@@ -65,7 +66,7 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
|
|
|
65
66
|
|
|
66
67
|
return (
|
|
67
68
|
<Button
|
|
68
|
-
variant="
|
|
69
|
+
variant="outlined"
|
|
69
70
|
onClick={() =>
|
|
70
71
|
iframe.openPicker({
|
|
71
72
|
eventId: String(event.id),
|
|
@@ -74,6 +75,13 @@ const TicketSelectionMap: React.FC<Props> = ({ event }) => {
|
|
|
74
75
|
clientId: uuid,
|
|
75
76
|
})
|
|
76
77
|
}
|
|
78
|
+
sx={{
|
|
79
|
+
width: { xs: '100%' },
|
|
80
|
+
color: 'text.primary',
|
|
81
|
+
borderColor: (theme) => theme.palette.grey['300'],
|
|
82
|
+
'& .MuiButton-endIcon': { ml: 0, fontSize: '1.5em' },
|
|
83
|
+
}}
|
|
84
|
+
endIcon={<Iconify icon="eva:chevron-right-outline" />}
|
|
77
85
|
>
|
|
78
86
|
{t('form.labels.open_map')}
|
|
79
87
|
</Button>
|
|
@@ -0,0 +1,177 @@
|
|
|
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 useGlobal from '@hooks/useGlobal';
|
|
8
|
+
import ReleaseDescription from './ReleaseDescription';
|
|
9
|
+
import TicketQuantityControl from './TicketQuantityControl';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
event: IEvent;
|
|
13
|
+
activeReleases?: IReleaseShort[];
|
|
14
|
+
showLoading: boolean;
|
|
15
|
+
soldOutReleaseCategoryNames: string[];
|
|
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
|
+
soldOutReleaseCategoryNames,
|
|
28
|
+
tickets,
|
|
29
|
+
isQuantityDisabled,
|
|
30
|
+
setValue,
|
|
31
|
+
removeTicket,
|
|
32
|
+
getExtraFields,
|
|
33
|
+
}) => {
|
|
34
|
+
const { t, lang } = useGlobal();
|
|
35
|
+
const [expandedReleaseIds, setExpandedReleaseIds] = useState<Record<number, boolean>>({});
|
|
36
|
+
const theme = useTheme();
|
|
37
|
+
const isLight = theme.palette.mode === 'light';
|
|
38
|
+
|
|
39
|
+
const getReleaseTitle = (release: IReleaseShort) =>
|
|
40
|
+
release.releaseCategoryName || release.name || '';
|
|
41
|
+
|
|
42
|
+
const getTicketIndexByRelease = (releaseId: number) =>
|
|
43
|
+
tickets.findIndex((ticket) => ticket.releaseId === releaseId);
|
|
44
|
+
|
|
45
|
+
const getReleaseQuantity = (releaseId: number) => {
|
|
46
|
+
const ticket = tickets.find((t) => t.releaseId === releaseId);
|
|
47
|
+
return Number(ticket?.quantity || 0);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const updateReleaseQuantity = (release: IReleaseShort, nextQuantity: number) => {
|
|
51
|
+
const maxAvailable = Math.min(release.availableTickets || 0, 10);
|
|
52
|
+
const clampedQuantity = Math.max(0, Math.min(nextQuantity, maxAvailable));
|
|
53
|
+
const ticketIndex = getTicketIndexByRelease(release.id);
|
|
54
|
+
|
|
55
|
+
if (clampedQuantity <= 0) {
|
|
56
|
+
if (ticketIndex >= 0) removeTicket(ticketIndex);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (ticketIndex >= 0) {
|
|
61
|
+
setValue(`tickets.${event.id}.${ticketIndex}.quantity`, clampedQuantity);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setValue(`tickets.${event.id}`, [
|
|
66
|
+
...tickets,
|
|
67
|
+
{
|
|
68
|
+
releaseId: release.id,
|
|
69
|
+
quantity: clampedQuantity,
|
|
70
|
+
itemName: getReleaseTitle(release),
|
|
71
|
+
price: release.price || 0,
|
|
72
|
+
products: [],
|
|
73
|
+
extraFields: [],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const toggleReleaseDescription = (releaseId: number) =>
|
|
79
|
+
setExpandedReleaseIds((prev) => ({
|
|
80
|
+
...prev,
|
|
81
|
+
[releaseId]: !prev[releaseId],
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
if (showLoading) {
|
|
85
|
+
return (
|
|
86
|
+
<Stack spacing={2}>
|
|
87
|
+
{[...Array(2)].map((_, index) => (
|
|
88
|
+
<Skeleton
|
|
89
|
+
key={index}
|
|
90
|
+
variant="rounded"
|
|
91
|
+
sx={{
|
|
92
|
+
width: '100%',
|
|
93
|
+
height: (theme) => theme.spacing(12),
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</Stack>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Stack spacing={2}>
|
|
103
|
+
{activeReleases?.map((release) => {
|
|
104
|
+
const quantity = getReleaseQuantity(release.id);
|
|
105
|
+
const ticketIndex = getTicketIndexByRelease(release.id);
|
|
106
|
+
const maxAvailable = Math.min(release.availableTickets || 0, 10);
|
|
107
|
+
const isLocked =
|
|
108
|
+
release.locked && !soldOutReleaseCategoryNames.includes(release.releaseCategoryName);
|
|
109
|
+
const isDisabled = isLocked && quantity === 0;
|
|
110
|
+
const canAddFirst = maxAvailable > 0 && !isQuantityDisabled(1, release.id);
|
|
111
|
+
const canAddMore = quantity < maxAvailable && !isQuantityDisabled(quantity + 1, release.id);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Box
|
|
115
|
+
key={release.id}
|
|
116
|
+
sx={{
|
|
117
|
+
pt: 1,
|
|
118
|
+
pr: 0.5,
|
|
119
|
+
pb: 0.5,
|
|
120
|
+
pl: 2,
|
|
121
|
+
borderRadius: 1,
|
|
122
|
+
bgcolor: (theme) => (isLight ? theme.palette.grey[100] : theme.palette.grey[800]),
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<Stack spacing={0}>
|
|
126
|
+
<Box>
|
|
127
|
+
<Typography variant="subtitle2" fontWeight={700}>
|
|
128
|
+
{getReleaseTitle(release)}
|
|
129
|
+
</Typography>
|
|
130
|
+
</Box>
|
|
131
|
+
|
|
132
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
|
133
|
+
<Stack>
|
|
134
|
+
<Typography variant="body2">
|
|
135
|
+
{release.price === 0
|
|
136
|
+
? t('free')
|
|
137
|
+
: fCurrency(release.price, lang, event.currency)}
|
|
138
|
+
</Typography>
|
|
139
|
+
|
|
140
|
+
<ReleaseDescription
|
|
141
|
+
description={release.description}
|
|
142
|
+
isExpanded={Boolean(expandedReleaseIds[release.id])}
|
|
143
|
+
onToggle={() => toggleReleaseDescription(release.id)}
|
|
144
|
+
moreInfoLabel={t('more_info')}
|
|
145
|
+
/>
|
|
146
|
+
</Stack>
|
|
147
|
+
|
|
148
|
+
<TicketQuantityControl
|
|
149
|
+
quantity={quantity}
|
|
150
|
+
isDisabled={isDisabled}
|
|
151
|
+
canAddFirst={canAddFirst}
|
|
152
|
+
canAddMore={canAddMore}
|
|
153
|
+
addLabel={t('add')}
|
|
154
|
+
onDecrement={() => updateReleaseQuantity(release, quantity - 1)}
|
|
155
|
+
onIncrement={() => updateReleaseQuantity(release, quantity + 1)}
|
|
156
|
+
onAddFirst={() => updateReleaseQuantity(release, 1)}
|
|
157
|
+
/>
|
|
158
|
+
</Stack>
|
|
159
|
+
|
|
160
|
+
<ReleaseDescription
|
|
161
|
+
description={release.description}
|
|
162
|
+
isExpanded={Boolean(expandedReleaseIds[release.id])}
|
|
163
|
+
onToggle={() => toggleReleaseDescription(release.id)}
|
|
164
|
+
moreInfoLabel={t('more_info')}
|
|
165
|
+
showCollapse
|
|
166
|
+
/>
|
|
167
|
+
|
|
168
|
+
{ticketIndex >= 0 && getExtraFields(release.id, ticketIndex)}
|
|
169
|
+
</Stack>
|
|
170
|
+
</Box>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
173
|
+
</Stack>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export default TicketSelectionMobile;
|
package/src/form/{TicketWithMerchandiseSelection.tsx → tickets/TicketWithMerchandiseSelection.tsx}
RENAMED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
3
|
-
import { Box,
|
|
3
|
+
import { Box, Skeleton, Stack, Typography } from '@mui/material';
|
|
4
4
|
import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
|
|
5
5
|
import useEventActiveReleases from '@hooks/data/useEventActiveReleases';
|
|
6
6
|
import { groupBy } from '@utils/global';
|
|
7
7
|
import { IReleaseShort } from '@utils/types/release.type';
|
|
8
|
-
import FeeBox from '@form/payment/FeeBox';
|
|
9
8
|
import { IEvent } from '@utils/types/event.type';
|
|
10
|
-
import
|
|
11
|
-
import ReleaseWithMerchandise from '@form/ReleaseWithMerchandise';
|
|
9
|
+
import ReleaseWithMerchandise from '@form/tickets/ReleaseWithMerchandise';
|
|
12
10
|
import { EventType } from '@utils/data/event';
|
|
13
11
|
import useGlobal from '@hooks/useGlobal';
|
|
14
12
|
|
|
@@ -18,7 +16,6 @@ interface Props {
|
|
|
18
16
|
|
|
19
17
|
const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
|
|
20
18
|
const { t } = useGlobal();
|
|
21
|
-
const isMobile = useResponsive('down', 'md');
|
|
22
19
|
const { setValue, watch } = useFormContext<ITicketForm>();
|
|
23
20
|
const tickets: ITicketFormTicket[] = useWatch({ name: `tickets.${event.id}`, defaultValue: [] });
|
|
24
21
|
const eventTimeslotId = watch('eventTimeslotId');
|
|
@@ -138,7 +135,7 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
|
|
|
138
135
|
};
|
|
139
136
|
|
|
140
137
|
return (
|
|
141
|
-
<Stack spacing={
|
|
138
|
+
<Stack spacing={1} direction="column">
|
|
142
139
|
{!activeReleases && event.type !== EventType.RECURRING ? (
|
|
143
140
|
<Skeleton
|
|
144
141
|
variant="rounded"
|
|
@@ -169,7 +166,6 @@ const TicketWithMerchandiseSelection: React.FC<Props> = ({ event }) => {
|
|
|
169
166
|
<Typography variant="caption" component="div" fontStyle="italic" mb={2}>
|
|
170
167
|
*{t('event.tickets.stepper.1.max_ticket_quantity')}
|
|
171
168
|
</Typography>
|
|
172
|
-
{isMobile && <FeeBox event={event} align="right" />}
|
|
173
169
|
</Box>
|
|
174
170
|
</Stack>
|
|
175
171
|
);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { FieldError, FieldErrors, FieldValues, Path, UseFormReturn } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
type FirstError<T extends FieldValues> = {
|
|
5
|
+
name: Path<T>;
|
|
6
|
+
ref?: HTMLElement | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isFieldError = (value: unknown): value is FieldError =>
|
|
10
|
+
!!value && typeof value === 'object' && ('type' in value || 'message' in value);
|
|
11
|
+
|
|
12
|
+
const getFirstError = <T extends FieldValues>(
|
|
13
|
+
errors: FieldErrors<T>,
|
|
14
|
+
parentPath = ''
|
|
15
|
+
): FirstError<T> | null => {
|
|
16
|
+
for (const [key, value] of Object.entries(errors)) {
|
|
17
|
+
if (!value) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fieldPath = parentPath ? `${parentPath}.${key}` : key;
|
|
22
|
+
|
|
23
|
+
if (isFieldError(value)) {
|
|
24
|
+
return {
|
|
25
|
+
name: fieldPath as Path<T>,
|
|
26
|
+
ref: value.ref instanceof HTMLElement ? value.ref : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
for (let index = 0; index < value.length; index++) {
|
|
32
|
+
const nestedError = value[index] as FieldErrors<T> | undefined;
|
|
33
|
+
if (!nestedError) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const firstNestedError = getFirstError(
|
|
38
|
+
nestedError,
|
|
39
|
+
parentPath ? `${parentPath}.${key}.${index}` : `${key}.${index}`
|
|
40
|
+
);
|
|
41
|
+
if (firstNestedError) {
|
|
42
|
+
return firstNestedError;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else if (typeof value === 'object') {
|
|
46
|
+
const firstNestedError = getFirstError(value as FieldErrors<T>, fieldPath);
|
|
47
|
+
if (firstNestedError) {
|
|
48
|
+
return firstNestedError;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const escapeSelectorValue = (value: string): string => {
|
|
57
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
58
|
+
return CSS.escape(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback: escape all CSS special characters manually
|
|
62
|
+
return value.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type ScrollToFirstErrorMethods<T extends FieldValues> = Pick<
|
|
66
|
+
UseFormReturn<T, object, undefined>,
|
|
67
|
+
'setFocus'
|
|
68
|
+
>;
|
|
69
|
+
|
|
70
|
+
export default function useScrollToFirstError<T extends FieldValues>({
|
|
71
|
+
setFocus,
|
|
72
|
+
}: ScrollToFirstErrorMethods<T>) {
|
|
73
|
+
return useCallback(
|
|
74
|
+
(errors: FieldErrors<T>) => {
|
|
75
|
+
const firstError = getFirstError(errors);
|
|
76
|
+
if (!firstError) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Re-query the DOM instead of relying on potentially stale refs
|
|
81
|
+
const fieldName = String(firstError.name);
|
|
82
|
+
const scrollTarget =
|
|
83
|
+
document.querySelector<HTMLElement>(`[name="${escapeSelectorValue(fieldName)}"]`) ||
|
|
84
|
+
document.getElementById(fieldName);
|
|
85
|
+
|
|
86
|
+
if (scrollTarget) {
|
|
87
|
+
scrollTarget.scrollIntoView({
|
|
88
|
+
behavior: 'smooth',
|
|
89
|
+
block: 'center',
|
|
90
|
+
});
|
|
91
|
+
scrollTarget.focus({ preventScroll: true });
|
|
92
|
+
} else {
|
|
93
|
+
// Fallback to RHF's setFocus if no DOM element found
|
|
94
|
+
setFocus(firstError.name);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[setFocus]
|
|
98
|
+
);
|
|
99
|
+
}
|
package/src/locales/cs.tsx
CHANGED
|
@@ -7,6 +7,8 @@ const cs = {
|
|
|
7
7
|
add: 'Přidat',
|
|
8
8
|
confirm: 'Potvrdit',
|
|
9
9
|
cancel: 'Zrušit',
|
|
10
|
+
close: 'Zavřít',
|
|
11
|
+
remove: 'Odstranit',
|
|
10
12
|
pay: 'Zaplatit',
|
|
11
13
|
change: 'Změnit',
|
|
12
14
|
free: 'Zdarma',
|
|
@@ -39,7 +41,7 @@ const cs = {
|
|
|
39
41
|
promo_code: 'Slevový kód',
|
|
40
42
|
start_date: 'Datum konání',
|
|
41
43
|
organizer: 'Pořadatel',
|
|
42
|
-
total: 'Celkem
|
|
44
|
+
total: 'Celkem',
|
|
43
45
|
with_fee: 'vč. servisního poplatku',
|
|
44
46
|
service_fee: 'Servisní poplatek',
|
|
45
47
|
shipping_fee: 'Doprava',
|
|
@@ -53,6 +55,10 @@ const cs = {
|
|
|
53
55
|
category: 'Kategorie',
|
|
54
56
|
sms_notification: 'SMS notifikace',
|
|
55
57
|
open_map: 'Otevřít mapu',
|
|
58
|
+
payment_overview_open: 'Otevřít přehled platby',
|
|
59
|
+
payment_overview_close: 'Zavřít přehled platby',
|
|
60
|
+
ticket_quantity_decrease: 'Snížit počet vstupenek',
|
|
61
|
+
ticket_quantity_increase: 'Zvýšit počet vstupenek',
|
|
56
62
|
},
|
|
57
63
|
validation: {
|
|
58
64
|
required: 'Toto pole je povinné.',
|
|
@@ -105,9 +111,13 @@ const cs = {
|
|
|
105
111
|
7: {
|
|
106
112
|
title: 'Přidružené eventy',
|
|
107
113
|
},
|
|
114
|
+
8: {
|
|
115
|
+
title: 'EventLook služby',
|
|
116
|
+
additional_info: 'Více informací ke službám',
|
|
117
|
+
},
|
|
108
118
|
},
|
|
109
119
|
terms_and_conditions:
|
|
110
|
-
'
|
|
120
|
+
'Souhlasím s <0>Obchodními podmínkami</0> {{termsAndConditionsCompanies}} a <1>Zásady soukromí</1>.',
|
|
111
121
|
insurance: {
|
|
112
122
|
label: 'Pojištění vstupenek',
|
|
113
123
|
per_ticket: 'ks',
|
|
@@ -120,13 +130,23 @@ const cs = {
|
|
|
120
130
|
},
|
|
121
131
|
},
|
|
122
132
|
sms_notification: {
|
|
123
|
-
label: 'SMS připomenutí termínu
|
|
133
|
+
label: 'SMS připomenutí termínu',
|
|
124
134
|
modal: {
|
|
125
135
|
description:
|
|
126
136
|
'Připomínka akce den předem: SMS vás upozorní na blížící se událost, abyste na ni nezapomněli a stihli se včas připravit.',
|
|
127
137
|
price: 'Cena SMS připomenutí termínu akce',
|
|
128
138
|
},
|
|
129
139
|
},
|
|
140
|
+
merchandise: {
|
|
141
|
+
show_sizes: 'Zobrazit velikosti',
|
|
142
|
+
show_added: 'Zobrazit přidané',
|
|
143
|
+
},
|
|
144
|
+
services: {
|
|
145
|
+
add_tickets_first: 'Nejdříve prosím přidejte vstupenky',
|
|
146
|
+
},
|
|
147
|
+
shipping: {
|
|
148
|
+
choose_address: 'Vybrat adresu',
|
|
149
|
+
},
|
|
130
150
|
order_success: {
|
|
131
151
|
title: 'Vstupenky byly úspěšně zarezervovány. Teď už zbývá jenom zaplatit.',
|
|
132
152
|
description:
|
|
@@ -161,6 +181,8 @@ const cs = {
|
|
|
161
181
|
components: {
|
|
162
182
|
product_variant_dialog: {
|
|
163
183
|
select_variant: 'Musíte vybrat variantu produktu.',
|
|
184
|
+
decrease_quantity: 'Snížit množství varianty produktu',
|
|
185
|
+
increase_quantity: 'Zvýšit množství varianty produktu',
|
|
164
186
|
},
|
|
165
187
|
},
|
|
166
188
|
};
|
package/src/locales/en.tsx
CHANGED
|
@@ -7,6 +7,8 @@ const en = {
|
|
|
7
7
|
add: 'Add',
|
|
8
8
|
confirm: 'Confirm',
|
|
9
9
|
cancel: 'Cancel',
|
|
10
|
+
close: 'Close',
|
|
11
|
+
remove: 'Remove',
|
|
10
12
|
pay: 'Pay',
|
|
11
13
|
change: 'Change',
|
|
12
14
|
free: 'Free',
|
|
@@ -53,6 +55,10 @@ const en = {
|
|
|
53
55
|
category: 'Category',
|
|
54
56
|
sms_notification: 'SMS notification',
|
|
55
57
|
open_map: 'Open map',
|
|
58
|
+
payment_overview_open: 'Open payment overview',
|
|
59
|
+
payment_overview_close: 'Close payment overview',
|
|
60
|
+
ticket_quantity_decrease: 'Decrease ticket quantity',
|
|
61
|
+
ticket_quantity_increase: 'Increase ticket quantity',
|
|
56
62
|
},
|
|
57
63
|
validation: {
|
|
58
64
|
required: 'This field is required.',
|
|
@@ -105,9 +111,13 @@ const en = {
|
|
|
105
111
|
7: {
|
|
106
112
|
title: 'Associated events',
|
|
107
113
|
},
|
|
114
|
+
8: {
|
|
115
|
+
title: 'EventLook services',
|
|
116
|
+
additional_info: 'More information about services',
|
|
117
|
+
},
|
|
108
118
|
},
|
|
109
119
|
terms_and_conditions:
|
|
110
|
-
'
|
|
120
|
+
'I agree with the <0>Terms and Conditions</0> {{termsAndConditionsCompanies}} and the <1>Privacy Policy</1>.',
|
|
111
121
|
insurance: {
|
|
112
122
|
label: 'Ticket insurance',
|
|
113
123
|
per_ticket: 'pc',
|
|
@@ -128,6 +138,16 @@ const en = {
|
|
|
128
138
|
price: 'SMS event reminder price',
|
|
129
139
|
},
|
|
130
140
|
},
|
|
141
|
+
merchandise: {
|
|
142
|
+
show_sizes: 'Show sizes',
|
|
143
|
+
show_added: 'Show added',
|
|
144
|
+
},
|
|
145
|
+
services: {
|
|
146
|
+
add_tickets_first: 'Please add tickets first',
|
|
147
|
+
},
|
|
148
|
+
shipping: {
|
|
149
|
+
choose_address: 'Choose address',
|
|
150
|
+
},
|
|
131
151
|
order_success: {
|
|
132
152
|
title: 'Tickets have been successfully reserved. Now you just need to pay.',
|
|
133
153
|
description:
|
|
@@ -162,6 +182,8 @@ const en = {
|
|
|
162
182
|
components: {
|
|
163
183
|
product_variant_dialog: {
|
|
164
184
|
select_variant: 'You must select a product variant',
|
|
185
|
+
decrease_quantity: 'Decrease product variant quantity',
|
|
186
|
+
increase_quantity: 'Increase product variant quantity',
|
|
165
187
|
},
|
|
166
188
|
},
|
|
167
189
|
};
|