@eventlook/sdk 1.7.0 → 1.7.2-beta.0
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 +10 -0
- package/.prettierignore +3 -0
- package/CLAUDE.md +33 -0
- package/dist/cjs/{index-0a8uUeDg.js → index-DmK9RPSa.js} +26553 -25757
- package/dist/cjs/index-DmK9RPSa.js.map +1 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/{index.umd-BvTBfvwB.js → index.umd-DUMMTVwU.js} +2 -2
- package/dist/cjs/{index.umd-BvTBfvwB.js.map → index.umd-DUMMTVwU.js.map} +1 -1
- package/dist/cjs/{mui-tel-input.es-Bjml407E.js → mui-tel-input.es-Dk9M_v4X.js} +6 -6
- package/dist/{esm/mui-tel-input.es-Bt2rE3An.js.map → cjs/mui-tel-input.es-Dk9M_v4X.js.map} +1 -1
- package/dist/esm/{index-ByLnhSXB.js → index-C0HcmMMr.js} +27111 -26333
- package/dist/esm/index-C0HcmMMr.js.map +1 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/{index.umd-DepuOxm3.js → index.umd-BqJOlKvJ.js} +4 -4
- package/dist/esm/{index.umd-DepuOxm3.js.map → index.umd-BqJOlKvJ.js.map} +1 -1
- package/dist/esm/{mui-tel-input.es-Bt2rE3An.js → mui-tel-input.es-Cb4Lpqx7.js} +21 -21
- package/dist/{cjs/mui-tel-input.es-Bjml407E.js.map → esm/mui-tel-input.es-Cb4Lpqx7.js.map} +1 -1
- package/dist/types/form/paydroid/PaydroidCashlessSection.d.ts +7 -0
- package/dist/types/form/paydroid/PaydroidError.d.ts +3 -0
- package/dist/types/form/paydroid/PaydroidErrorAccount.d.ts +3 -0
- package/dist/types/form/paydroid/PaydroidErrorTicket.d.ts +3 -0
- package/dist/types/form/paydroid/PaydroidPage.d.ts +6 -0
- package/dist/types/form/paydroid/PaydroidStatusCard.d.ts +10 -0
- package/dist/types/form/paydroid/PaydroidSuccess.d.ts +3 -0
- package/dist/types/form/paydroid/PaydroidSuccessTopup.d.ts +3 -0
- package/dist/types/form/tickets/PrimaryTicketDialog.d.ts +10 -0
- package/dist/types/locales/cs.d.ts +38 -0
- package/dist/types/locales/en.d.ts +40 -1
- package/dist/types/locales/es.d.ts +40 -1
- package/dist/types/locales/pl.d.ts +40 -1
- package/dist/types/locales/sk.d.ts +40 -1
- package/dist/types/locales/uk.d.ts +40 -1
- package/dist/types/modules/paydroid.d.ts +4 -0
- package/dist/types/modules/ticket.d.ts +1 -0
- package/dist/types/utils/data/page.d.ts +7 -0
- package/dist/types/utils/page.d.ts +1 -0
- package/dist/types/utils/paydroid.d.ts +6 -0
- package/dist/types/utils/types/global.type.d.ts +1 -0
- package/dist/types/utils/types/order.type.d.ts +2 -0
- package/dist/types/utils/types/paydroid.d.ts +23 -0
- package/dist/types/utils/types/release-category.type.d.ts +1 -0
- package/dist/types/utils/types/release.type.d.ts +2 -0
- package/dist/types/utils/types/ticket.type.d.ts +1 -0
- package/package.json +3 -1
- package/src/form/PaymentSuccess.tsx +6 -2
- package/src/form/TicketForm.tsx +28 -1
- package/src/form/paydroid/PaydroidCashlessSection.tsx +311 -0
- package/src/form/paydroid/PaydroidError.tsx +26 -0
- package/src/form/paydroid/PaydroidErrorAccount.tsx +26 -0
- package/src/form/paydroid/PaydroidErrorTicket.tsx +26 -0
- package/src/form/paydroid/PaydroidPage.tsx +22 -0
- package/src/form/paydroid/PaydroidStatusCard.tsx +91 -0
- package/src/form/paydroid/PaydroidSuccess.tsx +26 -0
- package/src/form/paydroid/PaydroidSuccessTopup.tsx +26 -0
- package/src/form/tickets/PrimaryTicketDialog.tsx +144 -0
- package/src/form/tickets/ReleaseWithMerchandise.tsx +89 -8
- package/src/form/tickets/TicketSelectionMobile.tsx +80 -12
- package/src/locales/cs.tsx +45 -0
- package/src/locales/en.tsx +47 -1
- package/src/locales/es.tsx +48 -1
- package/src/locales/pl.tsx +47 -1
- package/src/locales/sk.tsx +49 -1
- package/src/locales/uk.tsx +47 -2
- package/src/modules/paydroid.ts +33 -0
- package/src/modules/ticket.ts +13 -0
- package/src/utils/data/page.ts +7 -0
- package/src/utils/page.ts +4 -0
- package/src/utils/paydroid.ts +35 -0
- package/src/utils/types/global.type.ts +1 -0
- package/src/utils/types/order.type.ts +2 -0
- package/src/utils/types/paydroid.ts +26 -0
- package/src/utils/types/release-category.type.ts +1 -0
- package/src/utils/types/release.type.ts +2 -0
- package/src/utils/types/ticket.type.ts +1 -0
- package/dist/cjs/index-0a8uUeDg.js.map +0 -1
- package/dist/esm/index-ByLnhSXB.js.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogActions,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
Stack,
|
|
9
|
+
TextField,
|
|
10
|
+
Typography,
|
|
11
|
+
} from '@mui/material';
|
|
12
|
+
import useGlobal from '@hooks/useGlobal';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
open: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
onConfirm: (number: string) => void;
|
|
18
|
+
usedNumbers?: string[];
|
|
19
|
+
validateRemote?: (number: string) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PrimaryTicketDialog: React.FC<Props> = ({
|
|
23
|
+
open,
|
|
24
|
+
onClose,
|
|
25
|
+
onConfirm,
|
|
26
|
+
usedNumbers = [],
|
|
27
|
+
validateRemote,
|
|
28
|
+
}) => {
|
|
29
|
+
const { t, options } = useGlobal();
|
|
30
|
+
const [value, setValue] = useState('');
|
|
31
|
+
const [error, setError] = useState<string | null>(null);
|
|
32
|
+
const [isChecking, setIsChecking] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (open) {
|
|
36
|
+
setValue('');
|
|
37
|
+
setError(null);
|
|
38
|
+
setIsChecking(false);
|
|
39
|
+
const urlParams = new URLSearchParams(new URL(window.location.href).search);
|
|
40
|
+
const elementId = urlParams.get('elementId');
|
|
41
|
+
parent.postMessage({ type: 'eventlookFrameOpenDialog', name: elementId }, '*');
|
|
42
|
+
}
|
|
43
|
+
}, [open]);
|
|
44
|
+
|
|
45
|
+
const validateLocal = (next: string): string | null => {
|
|
46
|
+
if (!next) return t('form.validation.primary_ticket_required');
|
|
47
|
+
if (!/^\d+$/.test(next)) return t('form.validation.primary_ticket_numeric');
|
|
48
|
+
if (usedNumbers.includes(next)) return t('form.validation.primary_ticket_duplicate');
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
53
|
+
const next = e.target.value;
|
|
54
|
+
setValue(next);
|
|
55
|
+
if (error) setError(validateLocal(next));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleConfirm = async () => {
|
|
59
|
+
const message = validateLocal(value);
|
|
60
|
+
if (message) {
|
|
61
|
+
setError(message);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!validateRemote) {
|
|
65
|
+
onConfirm(value);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setIsChecking(true);
|
|
69
|
+
try {
|
|
70
|
+
await validateRemote(value);
|
|
71
|
+
onConfirm(value);
|
|
72
|
+
} catch (err: unknown) {
|
|
73
|
+
const remoteMessage =
|
|
74
|
+
(err as { message?: string } | null)?.message ?? t('event.tickets.error.order');
|
|
75
|
+
setError(remoteMessage);
|
|
76
|
+
} finally {
|
|
77
|
+
setIsChecking(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
82
|
+
if (e.key === 'Enter') {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
handleConfirm();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Dialog
|
|
90
|
+
open={open}
|
|
91
|
+
onClose={onClose}
|
|
92
|
+
fullWidth
|
|
93
|
+
slotProps={{ paper: { sx: { borderRadius: 2 } } }}
|
|
94
|
+
sx={{
|
|
95
|
+
mx: 'auto',
|
|
96
|
+
width: { xs: '100%', sm: 'calc(100vh - 300px)' },
|
|
97
|
+
minWidth: 300,
|
|
98
|
+
maxWidth: 444,
|
|
99
|
+
'& .MuiDialog-container': {
|
|
100
|
+
height: options?.isIframe ? 'auto' : '100%',
|
|
101
|
+
},
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<DialogTitle sx={{ textAlign: 'center', fontWeight: 700, fontSize: '1.25rem', pt: 3, pb: 1 }}>
|
|
105
|
+
{t('form.labels.primary_ticket_dialog_title')}
|
|
106
|
+
</DialogTitle>
|
|
107
|
+
<DialogContent sx={{ pb: 1 }}>
|
|
108
|
+
<Stack spacing={2.5}>
|
|
109
|
+
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center' }}>
|
|
110
|
+
{t('form.labels.primary_ticket_number_hint')}
|
|
111
|
+
</Typography>
|
|
112
|
+
<TextField
|
|
113
|
+
autoFocus
|
|
114
|
+
value={value}
|
|
115
|
+
onChange={handleChange}
|
|
116
|
+
onKeyDown={handleKeyDown}
|
|
117
|
+
label={t('form.labels.primary_ticket_dialog_input_label')}
|
|
118
|
+
fullWidth
|
|
119
|
+
inputMode="numeric"
|
|
120
|
+
disabled={isChecking}
|
|
121
|
+
error={!!error}
|
|
122
|
+
helperText={error || ' '}
|
|
123
|
+
/>
|
|
124
|
+
</Stack>
|
|
125
|
+
</DialogContent>
|
|
126
|
+
<DialogActions sx={{ px: 3, pb: 3, gap: 1 }}>
|
|
127
|
+
<Button
|
|
128
|
+
onClick={onClose}
|
|
129
|
+
disabled={isChecking}
|
|
130
|
+
variant="outlined"
|
|
131
|
+
color="inherit"
|
|
132
|
+
fullWidth
|
|
133
|
+
>
|
|
134
|
+
{t('cancel')}
|
|
135
|
+
</Button>
|
|
136
|
+
<Button variant="contained" onClick={handleConfirm} disabled={isChecking} fullWidth>
|
|
137
|
+
{t('confirm')}
|
|
138
|
+
</Button>
|
|
139
|
+
</DialogActions>
|
|
140
|
+
</Dialog>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default PrimaryTicketDialog;
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
|
|
|
2
2
|
import { Box, Stack, Typography } from '@mui/material';
|
|
3
3
|
import ProductVariantsDialog from '@form/product/ProductVariantsDialog';
|
|
4
4
|
import TicketQuantityControl from '@form/tickets/TicketQuantityControl';
|
|
5
|
+
import PrimaryTicketDialog from '@form/tickets/PrimaryTicketDialog';
|
|
5
6
|
import { IReleaseShort } from '@utils/types/release.type';
|
|
6
7
|
import { ITicketForm, ITicketFormTicket } from '@utils/types/ticket.type';
|
|
7
8
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
@@ -12,6 +13,7 @@ import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
|
|
|
12
13
|
import { getSelectedQuantityByVariant } from '@utils/product';
|
|
13
14
|
import ReleaseExtraFields from '@form/extra-field/ReleaseExtraFields';
|
|
14
15
|
import ReleaseDescription from '@form/tickets/ReleaseDescription';
|
|
16
|
+
import { validatePrimaryTicketNumber } from '@modules/ticket';
|
|
15
17
|
import useGlobal from '@hooks/useGlobal';
|
|
16
18
|
|
|
17
19
|
interface Props {
|
|
@@ -32,6 +34,8 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
32
34
|
const { t, lang } = useGlobal();
|
|
33
35
|
const [openVariantDialog, setOpenVariantDialog] = useState<'add' | 'increase' | null>(null);
|
|
34
36
|
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
|
37
|
+
const [primaryDialogMode, setPrimaryDialogMode] = useState<'add' | 'increase' | null>(null);
|
|
38
|
+
const [pendingPrimaryNumber, setPendingPrimaryNumber] = useState<string | null>(null);
|
|
35
39
|
const { setValue } = useFormContext<ITicketForm>();
|
|
36
40
|
const tickets: ITicketFormTicket[] = useWatch({ name: `tickets.${eventId}`, defaultValue: [] });
|
|
37
41
|
const products: IEventProductForm[] = useWatch({ name: `products.${eventId}`, defaultValue: [] });
|
|
@@ -65,7 +69,10 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
65
69
|
return getSelectedQuantity(releaseId) >= getAvailableTicketsForRelease(release);
|
|
66
70
|
};
|
|
67
71
|
|
|
68
|
-
const addRelease = (
|
|
72
|
+
const addRelease = (
|
|
73
|
+
productsToAdd?: IEventProductForm[] | IEventProductForm,
|
|
74
|
+
primaryTicketNumber?: string
|
|
75
|
+
) => {
|
|
69
76
|
const normalizedProducts = Array.isArray(productsToAdd)
|
|
70
77
|
? productsToAdd
|
|
71
78
|
: productsToAdd
|
|
@@ -100,12 +107,17 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
100
107
|
price: 0,
|
|
101
108
|
products: selectedProducts,
|
|
102
109
|
extraFields,
|
|
110
|
+
primaryTicketNumbers: release.requiresPrimaryTicket
|
|
111
|
+
? primaryTicketNumber !== undefined
|
|
112
|
+
? [primaryTicketNumber]
|
|
113
|
+
: []
|
|
114
|
+
: undefined,
|
|
103
115
|
},
|
|
104
116
|
]);
|
|
105
117
|
setOpenVariantDialog(null);
|
|
106
118
|
};
|
|
107
119
|
|
|
108
|
-
const increaseQuantity = (productsToAdd?: IEventProductForm[]) => {
|
|
120
|
+
const increaseQuantity = (productsToAdd?: IEventProductForm[], primaryTicketNumber?: string) => {
|
|
109
121
|
const normalizedProducts = productsToAdd ?? [];
|
|
110
122
|
const addedRelease = tickets.find((ticket) => ticket.releaseId === release.id);
|
|
111
123
|
if (addedRelease) {
|
|
@@ -140,6 +152,11 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
140
152
|
extraFields: release.extraFields?.length
|
|
141
153
|
? [...ticket.extraFields, ...extraFieldsToAdd]
|
|
142
154
|
: [],
|
|
155
|
+
primaryTicketNumbers: release.requiresPrimaryTicket
|
|
156
|
+
? primaryTicketNumber !== undefined
|
|
157
|
+
? [...(ticket.primaryTicketNumbers ?? []), primaryTicketNumber]
|
|
158
|
+
: ticket.primaryTicketNumbers
|
|
159
|
+
: ticket.primaryTicketNumbers,
|
|
143
160
|
}
|
|
144
161
|
: ticket
|
|
145
162
|
)
|
|
@@ -168,6 +185,7 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
168
185
|
quantity: newQuantity,
|
|
169
186
|
products: ticket?.products?.slice(0, -1), // non-mutating "pop"
|
|
170
187
|
extraFields: ticket?.extraFields?.slice(0, -1),
|
|
188
|
+
primaryTicketNumbers: ticket.primaryTicketNumbers?.slice(0, -1),
|
|
171
189
|
};
|
|
172
190
|
})
|
|
173
191
|
);
|
|
@@ -175,6 +193,7 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
175
193
|
}
|
|
176
194
|
}, [tickets, release.id, setValue]);
|
|
177
195
|
|
|
196
|
+
const ticketIndex = tickets.findIndex((t) => t.releaseId === release.id);
|
|
178
197
|
const remainingOrderCapacity = Math.max(0, MAX_TICKETS_PER_ORDER - countSelectedTickets());
|
|
179
198
|
const nextRelease = activeReleases?.find(
|
|
180
199
|
(item) =>
|
|
@@ -225,10 +244,28 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
225
244
|
}
|
|
226
245
|
addLabel={t('add')}
|
|
227
246
|
onDecrement={() => decreaseQuantity()}
|
|
228
|
-
onIncrement={() =>
|
|
229
|
-
release.
|
|
230
|
-
|
|
231
|
-
|
|
247
|
+
onIncrement={() => {
|
|
248
|
+
if (release.requiresPrimaryTicket) {
|
|
249
|
+
setPrimaryDialogMode('increase');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (release.product) {
|
|
253
|
+
setOpenVariantDialog('increase');
|
|
254
|
+
} else {
|
|
255
|
+
increaseQuantity();
|
|
256
|
+
}
|
|
257
|
+
}}
|
|
258
|
+
onAddFirst={() => {
|
|
259
|
+
if (release.requiresPrimaryTicket) {
|
|
260
|
+
setPrimaryDialogMode('add');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (release.product) {
|
|
264
|
+
setOpenVariantDialog('add');
|
|
265
|
+
} else {
|
|
266
|
+
addRelease();
|
|
267
|
+
}
|
|
268
|
+
}}
|
|
232
269
|
/>
|
|
233
270
|
</Stack>
|
|
234
271
|
|
|
@@ -247,12 +284,56 @@ const ReleaseWithMerchandise: React.FC<Props> = ({
|
|
|
247
284
|
quantity={countTickets}
|
|
248
285
|
/>
|
|
249
286
|
)}
|
|
287
|
+
{release.requiresPrimaryTicket && countTickets > 0 && ticketIndex >= 0 && (
|
|
288
|
+
<Stack spacing={0.5} mt={1.5}>
|
|
289
|
+
{(tickets[ticketIndex]?.primaryTicketNumbers ?? []).map((num, i) => (
|
|
290
|
+
<Typography key={i} variant="caption" color="text.secondary">
|
|
291
|
+
{t('form.labels.primary_ticket_number', { number: i + 1 })}: {num}
|
|
292
|
+
</Typography>
|
|
293
|
+
))}
|
|
294
|
+
</Stack>
|
|
295
|
+
)}
|
|
296
|
+
{release.requiresPrimaryTicket && (
|
|
297
|
+
<PrimaryTicketDialog
|
|
298
|
+
open={!!primaryDialogMode}
|
|
299
|
+
onClose={() => setPrimaryDialogMode(null)}
|
|
300
|
+
usedNumbers={tickets.flatMap((ticket) => ticket.primaryTicketNumbers ?? [])}
|
|
301
|
+
validateRemote={(number) =>
|
|
302
|
+
validatePrimaryTicketNumber(release.releaseCategoryId, number)
|
|
303
|
+
}
|
|
304
|
+
onConfirm={(number) => {
|
|
305
|
+
const mode = primaryDialogMode;
|
|
306
|
+
setPrimaryDialogMode(null);
|
|
307
|
+
if (release.product) {
|
|
308
|
+
setPendingPrimaryNumber(number);
|
|
309
|
+
setOpenVariantDialog(mode);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (mode === 'increase') {
|
|
313
|
+
increaseQuantity(undefined, number);
|
|
314
|
+
} else {
|
|
315
|
+
addRelease(undefined, number);
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
250
320
|
{release.product && (
|
|
251
321
|
<ProductVariantsDialog
|
|
252
322
|
eventProduct={release.product}
|
|
253
323
|
openDialog={!!openVariantDialog}
|
|
254
|
-
callback={
|
|
255
|
-
|
|
324
|
+
callback={(variants) => {
|
|
325
|
+
const number = pendingPrimaryNumber ?? undefined;
|
|
326
|
+
setPendingPrimaryNumber(null);
|
|
327
|
+
if (openVariantDialog === 'increase') {
|
|
328
|
+
increaseQuantity(variants, number);
|
|
329
|
+
} else {
|
|
330
|
+
addRelease(variants, number);
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
onClose={() => {
|
|
334
|
+
setPendingPrimaryNumber(null);
|
|
335
|
+
setOpenVariantDialog(null);
|
|
336
|
+
}}
|
|
256
337
|
selectedQuantityByVariant={getSelectedQuantityByVariant(products, tickets)}
|
|
257
338
|
eventId={eventId}
|
|
258
339
|
canAddOnlyOneAtATime
|
|
@@ -6,8 +6,10 @@ import { IReleaseShort } from '@utils/types/release.type';
|
|
|
6
6
|
import { ITicketFormTicket } from '@utils/types/ticket.type';
|
|
7
7
|
import { MAX_TICKETS_PER_ORDER } from '@utils/data/ticket';
|
|
8
8
|
import useGlobal from '@hooks/useGlobal';
|
|
9
|
+
import { validatePrimaryTicketNumber } from '@modules/ticket';
|
|
9
10
|
import ReleaseDescription from './ReleaseDescription';
|
|
10
11
|
import TicketQuantityControl from './TicketQuantityControl';
|
|
12
|
+
import PrimaryTicketDialog from './PrimaryTicketDialog';
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
event: IEvent;
|
|
@@ -32,6 +34,7 @@ const TicketSelectionMobile: React.FC<Props> = ({
|
|
|
32
34
|
}) => {
|
|
33
35
|
const { t, lang } = useGlobal();
|
|
34
36
|
const [expandedReleaseIds, setExpandedReleaseIds] = useState<Record<number, boolean>>({});
|
|
37
|
+
const [pendingPrimaryRelease, setPendingPrimaryRelease] = useState<IReleaseShort | null>(null);
|
|
35
38
|
const theme = useTheme();
|
|
36
39
|
const isLight = theme.palette.mode === 'light';
|
|
37
40
|
|
|
@@ -49,18 +52,22 @@ const TicketSelectionMobile: React.FC<Props> = ({
|
|
|
49
52
|
const isReleaseVisible = (release: IReleaseShort) =>
|
|
50
53
|
!release.locked || getReleaseQuantity(release.id) > 0;
|
|
51
54
|
|
|
52
|
-
const
|
|
55
|
+
const incrementRelease = (release: IReleaseShort, primaryTicketNumber?: string) => {
|
|
53
56
|
const maxAvailable = Math.min(release.availableTickets || 0, MAX_TICKETS_PER_ORDER);
|
|
54
|
-
const clampedQuantity = Math.max(0, Math.min(nextQuantity, maxAvailable));
|
|
55
57
|
const ticketIndex = getTicketIndexByRelease(release.id);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
58
|
+
const currentQuantity = ticketIndex >= 0 ? Number(tickets[ticketIndex]?.quantity || 0) : 0;
|
|
59
|
+
const nextQuantity = Math.min(currentQuantity + 1, maxAvailable);
|
|
60
|
+
if (nextQuantity === currentQuantity) return;
|
|
61
61
|
|
|
62
62
|
if (ticketIndex >= 0) {
|
|
63
|
-
setValue(`tickets.${event.id}.${ticketIndex}.quantity`,
|
|
63
|
+
setValue(`tickets.${event.id}.${ticketIndex}.quantity`, nextQuantity);
|
|
64
|
+
if (release.requiresPrimaryTicket && primaryTicketNumber !== undefined) {
|
|
65
|
+
const current = tickets[ticketIndex]?.primaryTicketNumbers ?? [];
|
|
66
|
+
setValue(`tickets.${event.id}.${ticketIndex}.primaryTicketNumbers`, [
|
|
67
|
+
...current,
|
|
68
|
+
primaryTicketNumber,
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
64
71
|
return;
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -68,15 +75,56 @@ const TicketSelectionMobile: React.FC<Props> = ({
|
|
|
68
75
|
...tickets,
|
|
69
76
|
{
|
|
70
77
|
releaseId: release.id,
|
|
71
|
-
quantity:
|
|
78
|
+
quantity: nextQuantity,
|
|
72
79
|
itemName: getReleaseTitle(release),
|
|
73
80
|
price: release.price || 0,
|
|
74
81
|
products: [],
|
|
75
82
|
extraFields: [],
|
|
83
|
+
primaryTicketNumbers: release.requiresPrimaryTicket
|
|
84
|
+
? primaryTicketNumber !== undefined
|
|
85
|
+
? [primaryTicketNumber]
|
|
86
|
+
: []
|
|
87
|
+
: undefined,
|
|
76
88
|
},
|
|
77
89
|
]);
|
|
78
90
|
};
|
|
79
91
|
|
|
92
|
+
const decrementRelease = (release: IReleaseShort) => {
|
|
93
|
+
const ticketIndex = getTicketIndexByRelease(release.id);
|
|
94
|
+
if (ticketIndex < 0) return;
|
|
95
|
+
const currentQuantity = Number(tickets[ticketIndex]?.quantity || 0);
|
|
96
|
+
const nextQuantity = currentQuantity - 1;
|
|
97
|
+
|
|
98
|
+
if (nextQuantity <= 0) {
|
|
99
|
+
removeTicket(ticketIndex);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setValue(`tickets.${event.id}.${ticketIndex}.quantity`, nextQuantity);
|
|
104
|
+
if (release.requiresPrimaryTicket) {
|
|
105
|
+
const current = tickets[ticketIndex]?.primaryTicketNumbers ?? [];
|
|
106
|
+
setValue(
|
|
107
|
+
`tickets.${event.id}.${ticketIndex}.primaryTicketNumbers`,
|
|
108
|
+
current.slice(0, nextQuantity)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleIncrementClick = (release: IReleaseShort) => {
|
|
114
|
+
if (release.requiresPrimaryTicket) {
|
|
115
|
+
setPendingPrimaryRelease(release);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
incrementRelease(release);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handlePrimaryTicketConfirm = (number: string) => {
|
|
122
|
+
if (pendingPrimaryRelease) {
|
|
123
|
+
incrementRelease(pendingPrimaryRelease, number);
|
|
124
|
+
}
|
|
125
|
+
setPendingPrimaryRelease(null);
|
|
126
|
+
};
|
|
127
|
+
|
|
80
128
|
const toggleReleaseDescription = (releaseId: number) =>
|
|
81
129
|
setExpandedReleaseIds((prev) => ({
|
|
82
130
|
...prev,
|
|
@@ -162,9 +210,9 @@ const TicketSelectionMobile: React.FC<Props> = ({
|
|
|
162
210
|
canAddFirst={canAddFirst}
|
|
163
211
|
canAddMore={canAddMore}
|
|
164
212
|
addLabel={t('add')}
|
|
165
|
-
onDecrement={() =>
|
|
166
|
-
onIncrement={() =>
|
|
167
|
-
onAddFirst={() =>
|
|
213
|
+
onDecrement={() => decrementRelease(release)}
|
|
214
|
+
onIncrement={() => handleIncrementClick(release)}
|
|
215
|
+
onAddFirst={() => handleIncrementClick(release)}
|
|
168
216
|
/>
|
|
169
217
|
</Stack>
|
|
170
218
|
|
|
@@ -177,10 +225,30 @@ const TicketSelectionMobile: React.FC<Props> = ({
|
|
|
177
225
|
/>
|
|
178
226
|
|
|
179
227
|
{ticketIndex >= 0 && getExtraFields(release.id, ticketIndex)}
|
|
228
|
+
{release.requiresPrimaryTicket && quantity > 0 && ticketIndex >= 0 && (
|
|
229
|
+
<Stack spacing={0.5} mt={1.5}>
|
|
230
|
+
{(tickets[ticketIndex]?.primaryTicketNumbers ?? []).map((num, i) => (
|
|
231
|
+
<Typography key={i} variant="caption" color="text.secondary">
|
|
232
|
+
{t('form.labels.primary_ticket_number', { number: i + 1 })}: {num}
|
|
233
|
+
</Typography>
|
|
234
|
+
))}
|
|
235
|
+
</Stack>
|
|
236
|
+
)}
|
|
180
237
|
</Stack>
|
|
181
238
|
</Box>
|
|
182
239
|
);
|
|
183
240
|
})}
|
|
241
|
+
<PrimaryTicketDialog
|
|
242
|
+
open={!!pendingPrimaryRelease}
|
|
243
|
+
onClose={() => setPendingPrimaryRelease(null)}
|
|
244
|
+
onConfirm={handlePrimaryTicketConfirm}
|
|
245
|
+
usedNumbers={tickets.flatMap((ticket) => ticket.primaryTicketNumbers ?? [])}
|
|
246
|
+
validateRemote={(number) =>
|
|
247
|
+
pendingPrimaryRelease
|
|
248
|
+
? validatePrimaryTicketNumber(pendingPrimaryRelease.releaseCategoryId, number)
|
|
249
|
+
: Promise.resolve()
|
|
250
|
+
}
|
|
251
|
+
/>
|
|
184
252
|
</Stack>
|
|
185
253
|
);
|
|
186
254
|
};
|
package/src/locales/cs.tsx
CHANGED
|
@@ -59,6 +59,12 @@ const cs = {
|
|
|
59
59
|
payment_overview_close: 'Zavřít přehled platby',
|
|
60
60
|
ticket_quantity_decrease: 'Snížit počet vstupenek',
|
|
61
61
|
ticket_quantity_increase: 'Zvýšit počet vstupenek',
|
|
62
|
+
primary_ticket_number: 'Číslo primární vstupenky #{{number}}',
|
|
63
|
+
primary_ticket_number_hint:
|
|
64
|
+
'Tato kategorie je doplňková — zadejte číslo již zakoupené vstupenky.',
|
|
65
|
+
primary_ticket_dialog_title: 'Zadejte číslo primární vstupenky',
|
|
66
|
+
primary_ticket_dialog_input_label: 'Číslo primární vstupenky',
|
|
67
|
+
amount: 'Částka',
|
|
62
68
|
},
|
|
63
69
|
validation: {
|
|
64
70
|
required: 'Toto pole je povinné.',
|
|
@@ -73,6 +79,9 @@ const cs = {
|
|
|
73
79
|
count_tickets_or_products: 'Musíte vybrat alespoň 1 vstupenku nebo produkt.',
|
|
74
80
|
insurance_terms_and_conditions:
|
|
75
81
|
'Musíte souhlasit s pojistnými podmínkami pojištění a informačním dokumentem o tomto pojistném produktu (IPID).',
|
|
82
|
+
primary_ticket_required: 'Zadejte číslo primární vstupenky.',
|
|
83
|
+
primary_ticket_numeric: 'Číslo vstupenky smí obsahovat pouze číslice.',
|
|
84
|
+
primary_ticket_duplicate: 'Toto číslo jste v této objednávce již použili.',
|
|
76
85
|
},
|
|
77
86
|
},
|
|
78
87
|
event: {
|
|
@@ -177,6 +186,42 @@ const cs = {
|
|
|
177
186
|
button: 'Stáhnout vstupenku',
|
|
178
187
|
products: 'Vaše produkty',
|
|
179
188
|
},
|
|
189
|
+
paydroid: {
|
|
190
|
+
label: 'Paydroid Cashless',
|
|
191
|
+
heading: 'Vyhněte se frontám!',
|
|
192
|
+
heading_cta: 'Přednabijte si',
|
|
193
|
+
subtitle:
|
|
194
|
+
'Na akci lze platit pouze bezhotovostně pomocí systému Paydroid Cashless. Přednabijte si částku teď a vyhněte se frontám!',
|
|
195
|
+
amount_label: 'Jakou částku chcete přednabít?',
|
|
196
|
+
amount_placeholder: 'Zadejte částku',
|
|
197
|
+
promo: 'Přednabijte si na akci částku vyšší než 1000 Kč a získejte pivo zdarma!',
|
|
198
|
+
terms_prefix: 'Souhlasím s',
|
|
199
|
+
terms_conditions: 'Obchodními podmínkami',
|
|
200
|
+
terms_middle: 'Paydroid Cashless a se',
|
|
201
|
+
terms_privacy: 'Zásadami zpracování osobních údajů',
|
|
202
|
+
terms: 'Souhlas VOP a GDPR Paydroid Cashless',
|
|
203
|
+
skip: 'chci pouze vstupenku v aplikaci',
|
|
204
|
+
submit: 'přednabít',
|
|
205
|
+
success_title: 'Vstupenku jsme přidali k vašemu účtu',
|
|
206
|
+
success_subtitle:
|
|
207
|
+
'Teď už se stačí jen přihlásit do aplikace Paydroid Cashless. A pokud se budete chtít vyhnout frontám, přednabijte si v aplikaci!',
|
|
208
|
+
topup_success_title: 'Děkujeme za přednabitî!',
|
|
209
|
+
topup_success_subtitle:
|
|
210
|
+
'Právě jsme připsali kredit na Váš účet Paydroid Cashless, nyní už se stačí pouze přihlásit v aplikaci.',
|
|
211
|
+
error_title: 'Přednabití se nezdařilo',
|
|
212
|
+
error_subtitle:
|
|
213
|
+
'Nebyli jste nijak zpoplatněni, v případě jakýchkoliv problémů nás prosím kontaktujte.',
|
|
214
|
+
error_account_title: 'Nepodařilo se vytvořit účet',
|
|
215
|
+
error_account_subtitle:
|
|
216
|
+
'Pravděpodobně jste zadali údaje, které se liší se záznamem v naší databázi.',
|
|
217
|
+
error_account_button: 'přihlásit se do aplikace',
|
|
218
|
+
error_ticket_title: 'Přidání vstupenky se nezdařilo',
|
|
219
|
+
error_ticket_subtitle:
|
|
220
|
+
'Omlouváme se, vyzkoušejte to prosím později. V případě jakýchkoliv problémů nás prosím kontaktujte.',
|
|
221
|
+
faq_button: 'nejčastější dotazy',
|
|
222
|
+
retry_button: 'vyzkoušet znovu',
|
|
223
|
+
missing_api_key: 'Paydroid API klíč chybí',
|
|
224
|
+
},
|
|
180
225
|
unpaid: 'Platba neproběhla',
|
|
181
226
|
unpaid_description: 'Zkuste to prosím znovu kliknutím na tlačítko Zaplatit.',
|
|
182
227
|
},
|
package/src/locales/en.tsx
CHANGED
|
@@ -59,6 +59,12 @@ const en = {
|
|
|
59
59
|
payment_overview_close: 'Close payment overview',
|
|
60
60
|
ticket_quantity_decrease: 'Decrease ticket quantity',
|
|
61
61
|
ticket_quantity_increase: 'Increase ticket quantity',
|
|
62
|
+
primary_ticket_number: 'Primary ticket number #{{number}}',
|
|
63
|
+
primary_ticket_number_hint:
|
|
64
|
+
'This is an addon category — enter the number of an already-purchased ticket.',
|
|
65
|
+
primary_ticket_dialog_title: 'Enter primary ticket number',
|
|
66
|
+
primary_ticket_dialog_input_label: 'Primary ticket number',
|
|
67
|
+
amount: 'Amount',
|
|
62
68
|
},
|
|
63
69
|
validation: {
|
|
64
70
|
required: 'This field is required.',
|
|
@@ -71,6 +77,11 @@ const en = {
|
|
|
71
77
|
promo_code_cant_combine: 'This promo code cannot be combined with other codes.',
|
|
72
78
|
min_purchase_not_met: 'Minimum purchase amount not met: ',
|
|
73
79
|
count_tickets_or_products: 'You must select at least 1 ticket or product.',
|
|
80
|
+
insurance_terms_and_conditions:
|
|
81
|
+
'You must agree to the insurance terms and conditions and the insurance product information document (IPID).',
|
|
82
|
+
primary_ticket_required: 'Enter the primary ticket number.',
|
|
83
|
+
primary_ticket_numeric: 'Ticket number must contain digits only.',
|
|
84
|
+
primary_ticket_duplicate: 'You have already entered this number in this order.',
|
|
74
85
|
},
|
|
75
86
|
},
|
|
76
87
|
event: {
|
|
@@ -120,9 +131,10 @@ const en = {
|
|
|
120
131
|
'I agree with the <0>Terms and Conditions</0> {{termsAndConditionsCompanies}} and the <1>Privacy Policy</1>.',
|
|
121
132
|
insurance: {
|
|
122
133
|
label: 'Ticket insurance',
|
|
134
|
+
checkbox:
|
|
135
|
+
'I confirm that I have read the <0>insurance terms and conditions</0> and the <1>insurance product information document (IPID)</1>',
|
|
123
136
|
per_ticket: 'pc',
|
|
124
137
|
modal: {
|
|
125
|
-
title: 'Ticket Insurance',
|
|
126
138
|
description: [
|
|
127
139
|
'Cancellation fee insurance provides you with protection in case of unexpected events that prevent you from attending the event. With insurance, you can avoid financial losses. You will receive a refund for the ticket if you cannot attend the event, especially due to:',
|
|
128
140
|
'- Your injury or illness',
|
|
@@ -174,6 +186,40 @@ const en = {
|
|
|
174
186
|
button: 'Download ticket',
|
|
175
187
|
products: 'Your products',
|
|
176
188
|
},
|
|
189
|
+
paydroid: {
|
|
190
|
+
label: 'Paydroid Cashless',
|
|
191
|
+
heading: 'Skip the queues!',
|
|
192
|
+
heading_cta: 'Top up now',
|
|
193
|
+
subtitle:
|
|
194
|
+
'The event accepts only cashless payments via Paydroid Cashless. Top up your balance now and skip the queues!',
|
|
195
|
+
amount_label: 'How much would you like to top up?',
|
|
196
|
+
amount_placeholder: 'Enter amount',
|
|
197
|
+
promo: 'Top up more than 1000 CZK and get a free beer!',
|
|
198
|
+
terms_prefix: 'I agree to the',
|
|
199
|
+
terms_conditions: 'Terms and Conditions',
|
|
200
|
+
terms_middle: 'of Paydroid Cashless and the',
|
|
201
|
+
terms_privacy: 'Privacy Policy',
|
|
202
|
+
terms: 'I agree to the T&C and GDPR of Paydroid Cashless',
|
|
203
|
+
skip: 'I just want the ticket in the app',
|
|
204
|
+
submit: 'top up',
|
|
205
|
+
success_title: 'We added the ticket to your account',
|
|
206
|
+
success_subtitle:
|
|
207
|
+
'Just log in to the Paydroid Cashless app. And if you want to skip the queues, top up in the app!',
|
|
208
|
+
topup_success_title: 'Thank you for topping up!',
|
|
209
|
+
topup_success_subtitle:
|
|
210
|
+
'We have credited your Paydroid Cashless account. Just log in to the app.',
|
|
211
|
+
error_title: 'Top-up failed',
|
|
212
|
+
error_subtitle: 'You have not been charged. Please contact us if you have any issues.',
|
|
213
|
+
error_account_title: 'Failed to create account',
|
|
214
|
+
error_account_subtitle: 'You may have entered details that differ from our records.',
|
|
215
|
+
error_account_button: 'log in to the app',
|
|
216
|
+
error_ticket_title: 'Failed to add ticket',
|
|
217
|
+
error_ticket_subtitle:
|
|
218
|
+
'We are sorry, please try again later. Contact us if the issue persists.',
|
|
219
|
+
faq_button: 'frequently asked questions',
|
|
220
|
+
retry_button: 'try again',
|
|
221
|
+
missing_api_key: 'Paydroid API key is missing',
|
|
222
|
+
},
|
|
177
223
|
unpaid: 'The payment was not successful.',
|
|
178
224
|
unpaid_description: 'Please try again by clicking the Pay button.',
|
|
179
225
|
},
|