tf-checkout-react 1.7.2 → 1.7.4
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/dist/api/auth.d.ts +22 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/components/confirmationContainer/index.d.ts +5 -1
- package/dist/components/loginForm/index.d.ts +1 -0
- package/dist/components/loginModal/SignUpForm.d.ts +10 -0
- package/dist/components/loginModal/constants.d.ts +39 -0
- package/dist/components/loginModal/index.d.ts +1 -0
- package/dist/components/seatMapContainer/addToCart.d.ts +2 -2
- package/dist/components/ticketsContainer/TimeSlotTicketRow.d.ts +19 -0
- package/dist/tf-checkout-react.cjs.development.js +1654 -698
- package/dist/tf-checkout-react.cjs.development.js.map +1 -1
- package/dist/tf-checkout-react.cjs.production.min.js +1 -1
- package/dist/tf-checkout-react.cjs.production.min.js.map +1 -1
- package/dist/tf-checkout-react.esm.js +1655 -699
- package/dist/tf-checkout-react.esm.js.map +1 -1
- package/dist/tf-checkout-styles.css +1 -1
- package/dist/validators/index.d.ts +4 -0
- package/package.json +2 -2
- package/src/api/auth.ts +49 -0
- package/src/api/index.ts +1 -1
- package/src/api/publicRequest.ts +1 -0
- package/src/components/billing-info-container/index.tsx +128 -32
- package/src/components/billing-info-container/style.css +46 -2
- package/src/components/confirmationContainer/index.tsx +24 -3
- package/src/components/loginForm/index.tsx +19 -3
- package/src/components/loginModal/SignUpForm.tsx +329 -0
- package/src/components/loginModal/constants.ts +46 -0
- package/src/components/loginModal/index.tsx +86 -9
- package/src/components/loginModal/style.css +44 -2
- package/src/components/preRegistration/constants.tsx +6 -4
- package/src/components/preRegistration/index.tsx +3 -3
- package/src/components/preRegistration/utils.ts +9 -1
- package/src/components/ticketsContainer/TimeSlotTicketRow.tsx +224 -0
- package/src/components/ticketsContainer/TimeSlotsSection.tsx +98 -24
- package/src/components/ticketsContainer/index.tsx +79 -21
- package/src/types/api/common.d.ts +1 -0
- package/src/types/api/payment.d.ts +2 -0
- package/src/types/formFields.d.ts +1 -1
- package/src/validators/index.ts +22 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import './style.css'
|
|
3
|
+
|
|
4
|
+
import Box from '@mui/material/Box'
|
|
5
|
+
import FormControl from '@mui/material/FormControl'
|
|
6
|
+
import MenuItem from '@mui/material/MenuItem'
|
|
7
|
+
import Select from '@mui/material/Select'
|
|
8
|
+
import moment from 'moment-timezone'
|
|
9
|
+
import React, { useState } from 'react'
|
|
10
|
+
import { Tooltip } from 'react-tooltip'
|
|
11
|
+
|
|
12
|
+
import { FEES_STYLES } from '../../constants'
|
|
13
|
+
import { CONFIGS } from '../../utils'
|
|
14
|
+
import InfoIcon from './InfoIcon'
|
|
15
|
+
import { getTicketSelectOptions } from './utils'
|
|
16
|
+
|
|
17
|
+
function decodeHTML(html: string): string {
|
|
18
|
+
const textArea = document.createElement('textarea')
|
|
19
|
+
textArea.innerHTML = html
|
|
20
|
+
return textArea.value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TimeSlotOption {
|
|
24
|
+
timeKey: string;
|
|
25
|
+
ticketInstance: any;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ITimeSlotTicketRowProps {
|
|
29
|
+
ticketKey: string;
|
|
30
|
+
ticket: any;
|
|
31
|
+
availableTimeSlots: TimeSlotOption[];
|
|
32
|
+
selectedTickets: any;
|
|
33
|
+
selectedTimeSlots: any;
|
|
34
|
+
handleTicketSelect: (ticketKey: string, quantity: number, ticketInstance: any) => void;
|
|
35
|
+
handleTimeSlotSelect: (ticketKey: string, timeKey: string, ticketInstance: any) => void;
|
|
36
|
+
priceSymbol: string;
|
|
37
|
+
isSoldOut: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const TimeSlotTicketRow = ({
|
|
41
|
+
ticketKey,
|
|
42
|
+
ticket,
|
|
43
|
+
availableTimeSlots,
|
|
44
|
+
selectedTickets,
|
|
45
|
+
selectedTimeSlots,
|
|
46
|
+
handleTicketSelect,
|
|
47
|
+
handleTimeSlotSelect,
|
|
48
|
+
priceSymbol,
|
|
49
|
+
isSoldOut,
|
|
50
|
+
}: ITimeSlotTicketRowProps) => {
|
|
51
|
+
const [visibleDescription, setVisibleDescription] = useState<boolean>(false)
|
|
52
|
+
|
|
53
|
+
const currentSelectedTimeKey = selectedTimeSlots[ticketKey] || ''
|
|
54
|
+
const currentTicketInstance = availableTimeSlots.find(slot => slot.timeKey === currentSelectedTimeKey)?.ticketInstance
|
|
55
|
+
const currentSelectedQuantity = currentTicketInstance ? (selectedTickets[currentTicketInstance.id] || 0) : 0
|
|
56
|
+
|
|
57
|
+
const maxCount = ticket.maxQuantity
|
|
58
|
+
const minCount = ticket.minQuantity
|
|
59
|
+
const { multiplier } = ticket
|
|
60
|
+
const options = getTicketSelectOptions(maxCount, minCount, multiplier)
|
|
61
|
+
|
|
62
|
+
const ticketPriceWithoutFees = `${priceSymbol} ${(+ticket.cost).toFixed(2)}`
|
|
63
|
+
const ticketPriceWithFees = `${priceSymbol} ${(+ticket.basePrice).toFixed(2)}`
|
|
64
|
+
const ticketOldPriceWithFees = `${priceSymbol} ${(+ticket.oldBasePrice).toFixed(2)}`
|
|
65
|
+
const ticketOldPriceWithoutFees = `${priceSymbol} ${(+ticket.oldCost).toFixed(2)}`
|
|
66
|
+
|
|
67
|
+
let ticketIsDiscounted = false
|
|
68
|
+
if (ticket.oldPrice && !isSoldOut && ticket.oldPrice !== ticket.price) {
|
|
69
|
+
ticketIsDiscounted = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ticketIsFree = +ticket.price === 0
|
|
73
|
+
const discountTicketPriceElem =
|
|
74
|
+
CONFIGS.FEES_STYLE === FEES_STYLES.DISPLAY_BOTH || !ticket.feeIncluded
|
|
75
|
+
? ticketOldPriceWithoutFees
|
|
76
|
+
: ticketOldPriceWithFees
|
|
77
|
+
const ticketPriceElem = isSoldOut
|
|
78
|
+
? 'SOLD OUT'
|
|
79
|
+
: ticketIsFree
|
|
80
|
+
? 'FREE'
|
|
81
|
+
: CONFIGS.FEES_STYLE === FEES_STYLES.DISPLAY_BOTH || !ticket.feeIncluded
|
|
82
|
+
? ticketPriceWithoutFees
|
|
83
|
+
: ticketPriceWithFees
|
|
84
|
+
|
|
85
|
+
const handleTimeChange = (event: any) => {
|
|
86
|
+
const selectedTimeKey = event.target.value
|
|
87
|
+
const selectedOption = availableTimeSlots.find(slot => slot.timeKey === selectedTimeKey)
|
|
88
|
+
if (selectedOption) {
|
|
89
|
+
handleTimeSlotSelect(ticketKey, selectedOption.timeKey, selectedOption.ticketInstance)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleQuantityChange = (event: any) => {
|
|
94
|
+
const { value } = event.target
|
|
95
|
+
// Only allow quantity selection if a time slot is selected
|
|
96
|
+
if (!currentSelectedTimeKey && value > 0) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (currentTicketInstance) {
|
|
100
|
+
handleTicketSelect(ticketKey, value, currentTicketInstance)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleDescriptionToggle = () => {
|
|
105
|
+
setVisibleDescription(current => !current)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<div className={`event-detail__tier ${isSoldOut ? 'disabled' : ''}`} id={ticketKey}>
|
|
111
|
+
<div className="event-detail__tier-name">
|
|
112
|
+
{ticket.displayName || ticket.name}
|
|
113
|
+
{ticket.descriptionRich && (
|
|
114
|
+
<>
|
|
115
|
+
<span
|
|
116
|
+
aria-hidden
|
|
117
|
+
className="info-icon"
|
|
118
|
+
onClick={handleDescriptionToggle}
|
|
119
|
+
data-tooltip-id={`tooltip-${ticketKey}`}
|
|
120
|
+
data-tooltip-content="View ticket info"
|
|
121
|
+
style={{
|
|
122
|
+
marginLeft: 8,
|
|
123
|
+
cursor: 'pointer',
|
|
124
|
+
display: 'flex',
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<InfoIcon />
|
|
128
|
+
</span>
|
|
129
|
+
|
|
130
|
+
<Tooltip id={`tooltip-${ticketKey}`} place="top">
|
|
131
|
+
{ticket.description || 'No description available'}
|
|
132
|
+
</Tooltip>
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
<div className="event-tickets-container">
|
|
137
|
+
<div className="event-detail__tier-price">
|
|
138
|
+
{ticketIsDiscounted && <p className="old-price">{discountTicketPriceElem}</p>}
|
|
139
|
+
<p className={isSoldOut ? 'sold-out' : ''}>{ticketPriceElem}</p>
|
|
140
|
+
{!isSoldOut && !ticketIsFree && (
|
|
141
|
+
<p className="fees">
|
|
142
|
+
{CONFIGS.FEES_STYLE === FEES_STYLES.TRADITIONAL &&
|
|
143
|
+
(ticket.feeIncluded ? '(incl. Fees)' : '(excl. Fees)')}
|
|
144
|
+
{CONFIGS.FEES_STYLE === FEES_STYLES.DISPLAY_BOTH &&
|
|
145
|
+
`(${ticketPriceWithFees} with fees)`}
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
{!isSoldOut && (
|
|
150
|
+
<div className="event-detail__tier-state time-slot-selectors-container">
|
|
151
|
+
{/* Time Slot Selector */}
|
|
152
|
+
<Box
|
|
153
|
+
sx={{
|
|
154
|
+
display: 'flex',
|
|
155
|
+
flexDirection: 'row',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
justifyContent: 'space-between',
|
|
158
|
+
gap: 2,
|
|
159
|
+
marginTop: 2,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<FormControl>
|
|
163
|
+
<Select
|
|
164
|
+
sx={{ borderRadius: 0, minWidth: 120 }}
|
|
165
|
+
value={currentSelectedTimeKey}
|
|
166
|
+
onChange={handleTimeChange}
|
|
167
|
+
displayEmpty
|
|
168
|
+
inputProps={{ 'aria-label': 'Select time slot' }}
|
|
169
|
+
MenuProps={{
|
|
170
|
+
PaperProps: {
|
|
171
|
+
className: 'get-tickets-paper',
|
|
172
|
+
},
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<MenuItem value="" disabled>
|
|
176
|
+
Time
|
|
177
|
+
</MenuItem>
|
|
178
|
+
{availableTimeSlots.map((slot, index) => (
|
|
179
|
+
<MenuItem key={index} value={slot.timeKey}>
|
|
180
|
+
{moment(slot.timeKey).format('hh:mm A')}
|
|
181
|
+
</MenuItem>
|
|
182
|
+
))}
|
|
183
|
+
</Select>
|
|
184
|
+
</FormControl>
|
|
185
|
+
{/* Quantity Selector */}
|
|
186
|
+
<FormControl>
|
|
187
|
+
<Select
|
|
188
|
+
sx={{ borderRadius: 0, minWidth: 60 }}
|
|
189
|
+
value={currentSelectedQuantity}
|
|
190
|
+
onChange={handleQuantityChange}
|
|
191
|
+
displayEmpty
|
|
192
|
+
disabled={!currentSelectedTimeKey}
|
|
193
|
+
inputProps={{ 'aria-label': 'Select quantity' }}
|
|
194
|
+
MenuProps={{
|
|
195
|
+
PaperProps: {
|
|
196
|
+
sx: { maxHeight: 150 },
|
|
197
|
+
className: 'get-tickets-paper',
|
|
198
|
+
},
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{options.map((option, index) => (
|
|
202
|
+
<MenuItem key={index} value={option.value}>
|
|
203
|
+
{option.value}
|
|
204
|
+
</MenuItem>
|
|
205
|
+
))}
|
|
206
|
+
</Select>
|
|
207
|
+
</FormControl>
|
|
208
|
+
</Box>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
{visibleDescription && ticket.descriptionRich && (
|
|
213
|
+
<div className="ticket-description">
|
|
214
|
+
<div
|
|
215
|
+
dangerouslySetInnerHTML={{
|
|
216
|
+
__html: decodeHTML(ticket.descriptionRich),
|
|
217
|
+
}}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { Box, CircularProgress, TextField } from '@mui/material'
|
|
3
3
|
import { LocalizationProvider, StaticDatePicker as DatePicker } from '@mui/x-date-pickers'
|
|
4
4
|
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'
|
|
5
|
-
import
|
|
5
|
+
import _get from 'lodash/get'
|
|
6
6
|
import _map from 'lodash/map'
|
|
7
|
+
import _sortBy from 'lodash/sortBy'
|
|
7
8
|
import moment from 'moment-timezone'
|
|
8
|
-
import React, { ReactNode, useState } from 'react'
|
|
9
|
+
import React, { ReactNode, useMemo, useState } from 'react'
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { TimeSlotTicketRow } from './TimeSlotTicketRow'
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
event: any;
|
|
@@ -29,6 +30,16 @@ interface Props {
|
|
|
29
30
|
isSeatMapAllowed?: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
interface TimeSlotOption {
|
|
34
|
+
timeKey: string;
|
|
35
|
+
ticketInstance: any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface UniqueTicket {
|
|
39
|
+
ticket: any;
|
|
40
|
+
availableTimeSlots: TimeSlotOption[];
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
const TimeSlotsSection: React.FC<Props> = ({
|
|
33
44
|
event,
|
|
34
45
|
availableDates,
|
|
@@ -41,14 +52,14 @@ const TimeSlotsSection: React.FC<Props> = ({
|
|
|
41
52
|
sortBySoldOut,
|
|
42
53
|
hideTicketsHeader,
|
|
43
54
|
ticketsHeaderComponent,
|
|
44
|
-
showGroupNameBlock,
|
|
45
55
|
currencySymbol,
|
|
46
|
-
isSeatMapAllowed,
|
|
47
56
|
}) => {
|
|
48
57
|
const [loading, setLoading] = useState(false)
|
|
58
|
+
const [selectedTimeSlots, setSelectedTimeSlots] = useState<{ [key: string]: string }>({})
|
|
49
59
|
|
|
50
60
|
const handleDateChange = async (date: string | null) => {
|
|
51
61
|
setSelectedDate(date)
|
|
62
|
+
setSelectedTimeSlots({}) // Reset time slot selections when date changes
|
|
52
63
|
if (date) {
|
|
53
64
|
setLoading(true)
|
|
54
65
|
try {
|
|
@@ -66,6 +77,67 @@ const TimeSlotsSection: React.FC<Props> = ({
|
|
|
66
77
|
return !availableDates.includes(formattedDate)
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
// Group tickets by unique ticket type using displayName + price
|
|
81
|
+
const uniqueTickets = useMemo(() => {
|
|
82
|
+
const ticketMap: { [key: string]: UniqueTicket } = {}
|
|
83
|
+
|
|
84
|
+
// Iterate through all time slots
|
|
85
|
+
_map(timeSlotGroups, (tickets, timeKey) => {
|
|
86
|
+
tickets.forEach((ticket: any) => {
|
|
87
|
+
// Use displayName + price as the unique identifier for ticket types
|
|
88
|
+
// This handles cases where multiple ticket types share the same optionName
|
|
89
|
+
const ticketKey = `${ticket.displayName || ticket.name}_${ticket.price}`
|
|
90
|
+
|
|
91
|
+
if (!ticketMap[ticketKey]) {
|
|
92
|
+
ticketMap[ticketKey] = {
|
|
93
|
+
ticket: { ...ticket },
|
|
94
|
+
availableTimeSlots: [],
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Store both the time key and the ticket instance for this slot
|
|
99
|
+
if (!ticketMap[ticketKey].availableTimeSlots.find((slot: any) => slot.timeKey === timeKey)) {
|
|
100
|
+
ticketMap[ticketKey].availableTimeSlots.push({
|
|
101
|
+
timeKey,
|
|
102
|
+
ticketInstance: ticket,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Convert to array and sort
|
|
109
|
+
const ticketsArray = Object.values(ticketMap)
|
|
110
|
+
return sortBySoldOut
|
|
111
|
+
? _sortBy(_sortBy(ticketsArray, t => t.ticket.sortOrder), t => t.ticket.soldOut)
|
|
112
|
+
: _sortBy(ticketsArray, t => t.ticket.sortOrder)
|
|
113
|
+
}, [timeSlotGroups, sortBySoldOut])
|
|
114
|
+
|
|
115
|
+
const handleTimeSlotSelect = (ticketKey: string, timeKey: string, ticketInstance: any) => {
|
|
116
|
+
setSelectedTimeSlots(prev => ({
|
|
117
|
+
...prev,
|
|
118
|
+
[ticketKey]: timeKey,
|
|
119
|
+
}))
|
|
120
|
+
|
|
121
|
+
// Reset quantity when time slot changes
|
|
122
|
+
if (selectedTickets[ticketInstance.id]) {
|
|
123
|
+
handleTicketSelect(ticketInstance.id, 0)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const handleTicketSelectWithTimeSlot = (ticketKey: string, quantity: number, ticketInstance: any) => {
|
|
128
|
+
const timeSlot = selectedTimeSlots[ticketKey]
|
|
129
|
+
if (!timeSlot && quantity > 0) {
|
|
130
|
+
return // Don't allow quantity selection without time slot
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ticketInstance) {
|
|
134
|
+
handleTicketSelect(ticketInstance.id, quantity)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const symbol = _get(event, 'currency.symbol')
|
|
139
|
+
const priceSymbol = currencySymbol || symbol
|
|
140
|
+
|
|
69
141
|
return (
|
|
70
142
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
71
143
|
<LocalizationProvider dateAdapter={AdapterMoment}>
|
|
@@ -88,29 +160,31 @@ const TimeSlotsSection: React.FC<Props> = ({
|
|
|
88
160
|
width: '100%',
|
|
89
161
|
display: 'flex',
|
|
90
162
|
flexDirection: 'column',
|
|
91
|
-
gap: 2,
|
|
92
163
|
}}
|
|
93
164
|
>
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
165
|
+
{!hideTicketsHeader && ticketsHeaderComponent}
|
|
166
|
+
{uniqueTickets.map(({ ticket, availableTimeSlots }) => {
|
|
167
|
+
const ticketKey = `${ticket.displayName || ticket.name}_${ticket.price}`
|
|
168
|
+
const isSoldOut =
|
|
169
|
+
ticket.sold_out ||
|
|
170
|
+
!(ticket.displayTicket || ticket.slotGroupId) ||
|
|
171
|
+
ticket.soldOut
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<TimeSlotTicketRow
|
|
175
|
+
key={ticketKey}
|
|
176
|
+
ticketKey={ticketKey}
|
|
177
|
+
ticket={ticket}
|
|
178
|
+
availableTimeSlots={availableTimeSlots}
|
|
101
179
|
selectedTickets={selectedTickets}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
hideTicketsHeader={hideTicketsHeader || _isEmpty(timeSlotGroups[timeKey])}
|
|
108
|
-
showGroupNameBlock={showGroupNameBlock}
|
|
109
|
-
currencySymbol={currencySymbol}
|
|
110
|
-
isSeatMapAllowed={isSeatMapAllowed}
|
|
180
|
+
selectedTimeSlots={selectedTimeSlots}
|
|
181
|
+
handleTicketSelect={handleTicketSelectWithTimeSlot}
|
|
182
|
+
handleTimeSlotSelect={handleTimeSlotSelect}
|
|
183
|
+
priceSymbol={priceSymbol}
|
|
184
|
+
isSoldOut={isSoldOut}
|
|
111
185
|
/>
|
|
112
|
-
|
|
113
|
-
)
|
|
186
|
+
)
|
|
187
|
+
})}
|
|
114
188
|
</Box>
|
|
115
189
|
)}
|
|
116
190
|
</Box>
|
|
@@ -48,8 +48,8 @@ import ConfirmModal from '../confirmModal'
|
|
|
48
48
|
import Countdown from '../countdown'
|
|
49
49
|
import { VerificationPendingModal } from '../idVerificationContainer/VerificationPendingModal'
|
|
50
50
|
import { LoginModal } from '../loginModal'
|
|
51
|
-
import WaitingList from '../waitingList'
|
|
52
51
|
import { PreRegistration } from '../preRegistration'
|
|
52
|
+
import WaitingList from '../waitingList'
|
|
53
53
|
import { AccessCodeSection } from './AccessCodeSection'
|
|
54
54
|
import { PromoCodeSection } from './PromoCodeSection'
|
|
55
55
|
import { ReferralLogic } from './ReferralLogic'
|
|
@@ -417,6 +417,41 @@ export const TicketsContainer = ({
|
|
|
417
417
|
const handleTicketSelect = (key: string, value: number | string, isTable = false) => {
|
|
418
418
|
localStorage.setItem('selectedTicketsQuantity', value.toString())
|
|
419
419
|
setSelectedTickets(prevState => {
|
|
420
|
+
// Allow multiple ticket types to be selected simultaneously when flag is enabled
|
|
421
|
+
if (event?.allowMultipleTicketTypePurchases === true) {
|
|
422
|
+
// Check if we're switching between tables and regular tickets
|
|
423
|
+
const hasExistingSelection = Object.keys(prevState).some(k => k !== 'isTable')
|
|
424
|
+
const switchingTicketType = hasExistingSelection && prevState.isTable !== isTable
|
|
425
|
+
|
|
426
|
+
// If switching from tables to regular tickets or vice versa, clear all selections
|
|
427
|
+
if (switchingTicketType && Number(value) > 0) {
|
|
428
|
+
return {
|
|
429
|
+
[key]: value,
|
|
430
|
+
isTable,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// If value is 0, remove this ticket from selection
|
|
435
|
+
if (!value || Number(value) === 0) {
|
|
436
|
+
const newState = { ...prevState }
|
|
437
|
+
delete newState[key]
|
|
438
|
+
// If no ticket keys remain (only isTable left), return empty state
|
|
439
|
+
const ticketKeys = Object.keys(newState).filter(k => k !== 'isTable')
|
|
440
|
+
if (ticketKeys.length === 0) {
|
|
441
|
+
return { isTable: false } as ISelectedTickets
|
|
442
|
+
}
|
|
443
|
+
return { ...newState, isTable: prevState.isTable }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// If value > 0, add or update this ticket while keeping others of the same type selected
|
|
447
|
+
return {
|
|
448
|
+
...prevState,
|
|
449
|
+
[key]: value,
|
|
450
|
+
isTable,
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Default behavior: only one ticket type at a time
|
|
420
455
|
if (Object.keys(prevState)[0] !== key && !value) {
|
|
421
456
|
return prevState
|
|
422
457
|
}
|
|
@@ -441,32 +476,55 @@ export const TicketsContainer = ({
|
|
|
441
476
|
const timeSlotTickets = _flatten(_map(timeSlotGroups, slots => slots))
|
|
442
477
|
|
|
443
478
|
setHandleBookIsLoading(true)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
479
|
+
|
|
480
|
+
// Unified flow: works for both single and multiple ticket types
|
|
481
|
+
const ticketsList = event?.isTimeSlotEvent ? timeSlotTickets : tickets
|
|
482
|
+
|
|
483
|
+
// Get all selected ticket IDs with quantity > 0 (excluding 'isTable' key)
|
|
484
|
+
const selectedTicketIds = Object.keys(selectedTickets).filter(
|
|
485
|
+
key => key !== 'isTable' && Number(selectedTickets[key]) > 0
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
// Build ticket_types object with all selected tickets (works for 1 or N tickets)
|
|
489
|
+
const ticketTypesData: any = {}
|
|
490
|
+
let totalProductCartQuantity = 0
|
|
491
|
+
let firstTicket: ITicket | null = null
|
|
492
|
+
|
|
493
|
+
selectedTicketIds.forEach(ticketId => {
|
|
494
|
+
const ticket = _find(ticketsList || [], item => String(item.id) === ticketId) as ITicket
|
|
495
|
+
if (ticket) {
|
|
496
|
+
if (!firstTicket) firstTicket = ticket
|
|
497
|
+
const optionName = _get(ticket, 'optionName')
|
|
498
|
+
const quantity = +selectedTickets[ticketId]
|
|
499
|
+
totalProductCartQuantity += quantity
|
|
500
|
+
|
|
501
|
+
ticketTypesData[ticketId] = {
|
|
502
|
+
product_options: {
|
|
503
|
+
[optionName]: ticketId,
|
|
504
|
+
ticket_price: ticket.price,
|
|
505
|
+
},
|
|
506
|
+
quantity,
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
if (!firstTicket) {
|
|
512
|
+
setHandleBookIsLoading(false)
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const firstOptionName = _get(firstTicket, 'optionName')
|
|
517
|
+
const firstTicketId = _get(firstTicket, 'id')
|
|
452
518
|
|
|
453
519
|
const data: ICartRequestData = {
|
|
454
520
|
attributes: {
|
|
455
521
|
alternative_view_id: null,
|
|
456
|
-
product_cart_quantity:
|
|
522
|
+
product_cart_quantity: totalProductCartQuantity,
|
|
457
523
|
product_options: {
|
|
458
|
-
[
|
|
524
|
+
[firstOptionName]: firstTicketId,
|
|
459
525
|
},
|
|
460
526
|
product_id: eventId,
|
|
461
|
-
ticket_types:
|
|
462
|
-
[ticketId]: {
|
|
463
|
-
product_options: {
|
|
464
|
-
[optionName]: ticketId,
|
|
465
|
-
ticket_price: ticket.price,
|
|
466
|
-
},
|
|
467
|
-
quantity: ticketQuantity,
|
|
468
|
-
},
|
|
469
|
-
},
|
|
527
|
+
ticket_types: ticketTypesData,
|
|
470
528
|
},
|
|
471
529
|
}
|
|
472
530
|
|
|
@@ -502,7 +560,7 @@ export const TicketsContainer = ({
|
|
|
502
560
|
: {}
|
|
503
561
|
|
|
504
562
|
const checkoutBody = createCheckoutDataBodyWithDefaultHolder(
|
|
505
|
-
|
|
563
|
+
totalProductCartQuantity,
|
|
506
564
|
userData
|
|
507
565
|
)
|
|
508
566
|
|
package/src/validators/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
1
|
+
export const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
2
2
|
|
|
3
3
|
export const combineValidators = (...validators: any) => (...value: any) => {
|
|
4
4
|
for (let i = 0; i < validators.length; ++i) {
|
|
@@ -38,3 +38,24 @@ export const requiredValidator = (
|
|
|
38
38
|
|
|
39
39
|
export const emailValidator = (email: string) =>
|
|
40
40
|
!emailRegex.test(email) ? 'Please enter a valid email address' : ''
|
|
41
|
+
|
|
42
|
+
export const passwordValidator = (password: string): string => {
|
|
43
|
+
if (!password || password.length < 6) {
|
|
44
|
+
return 'The password must be at least 6 characters.'
|
|
45
|
+
}
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const confirmPasswordValidator = (confirmPassword: string, password: string): string => {
|
|
50
|
+
if (confirmPassword !== password) {
|
|
51
|
+
return 'Passwords do not match.'
|
|
52
|
+
}
|
|
53
|
+
return ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const confirmEmailValidator = (confirmEmail: string, email: string): string => {
|
|
57
|
+
if (confirmEmail !== email) {
|
|
58
|
+
return 'Emails do not match.'
|
|
59
|
+
}
|
|
60
|
+
return ''
|
|
61
|
+
}
|