@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
@@ -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, lang } = useGlobal();
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
- const selectedRelease = activeReleases?.find((item) => item.id === release.releaseId);
83
- const availableQuantity = selectedRelease ? selectedRelease.availableTickets : 0;
84
- return availableQuantity > 10 ? 10 : availableQuantity;
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 spacing={3} direction="column" divider={<Divider sx={{ borderStyle: 'dashed' }} />}>
192
- {tickets.map((item, index) => (
193
- <Box key={index}>
194
- <Grid container spacing={3}>
195
- <Grid size={{ xs: 12, md: 6 }}>
196
- {showLoading ? (
197
- <Skeleton
198
- variant="rounded"
199
- sx={{
200
- width: '100%',
201
- height: (theme) => theme.spacing(7.5),
202
- }}
203
- />
204
- ) : (
205
- <RHFSelect
206
- name={`tickets.${event.id}.${index}.releaseId`}
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="contained"
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;
@@ -1,14 +1,12 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
2
  import { useFormContext, useWatch } from 'react-hook-form';
3
- import { Box, Divider, Skeleton, Stack, Typography } from '@mui/material';
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 useResponsive from '@hooks/useResponsive';
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={3} direction="column" divider={<Divider sx={{ borderStyle: 'dashed' }} />}>
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
+ }
@@ -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
- 'Odesláním objednávky souhlasím s <0>Obchodními podmínkami</0> {{termsAndConditionsCompanies}} a beru na vědomí <1>Zásady soukromí</1>.',
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 akce',
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
  };
@@ -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
- 'By submitting the order, I agree to the <0>Terms and Conditions</0> of {{termsAndConditionsCompanies}} and acknowledge the <1>Privacy Policy</1>.',
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
  };