bookito-widget 0.0.1
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/.babelrc +12 -0
- package/.eslintrc.json +18 -0
- package/README.md +0 -0
- package/dist/3rdpartylicenses.txt +1700 -0
- package/dist/Carlito.eot +0 -0
- package/dist/Carlito.svg +294 -0
- package/dist/Carlito.ttf +0 -0
- package/dist/Carlito.woff +0 -0
- package/dist/Carlito.woff2 +0 -0
- package/dist/bookito-widget.umd.js +1444 -0
- package/dist/bookito-widget.umd.js.LICENSE.txt +92 -0
- package/dist/bookito-widget.umd.js.map +1 -0
- package/dist/layers-2x.png +0 -0
- package/dist/layers.png +0 -0
- package/dist/marker-icon.png +0 -0
- package/jest.config.ts +9 -0
- package/package.json +8 -0
- package/postcss.config.js +3 -0
- package/project.json +63 -0
- package/src/Card/EmptyCard.tsx +30 -0
- package/src/Card/index.tsx +44 -0
- package/src/CardHolder/index.tsx +25 -0
- package/src/Modal/BookingForm.tsx +286 -0
- package/src/Modal/FormSubmitButton/AnimatedSuccessCheckmark/index.module.css +45 -0
- package/src/Modal/FormSubmitButton/AnimatedSuccessCheckmark/index.tsx +21 -0
- package/src/Modal/FormSubmitButton/index.tsx +58 -0
- package/src/Modal/PartialForm.tsx +308 -0
- package/src/Modal/helper.tsx +93 -0
- package/src/Modal/index.tsx +50 -0
- package/src/index.module.css +35 -0
- package/src/index.tsx +144 -0
- package/src/utils/index.tsx +54 -0
- package/tailwind.config.js +13 -0
- package/tsconfig.json +26 -0
- package/tsconfig.lib.json +23 -0
- package/tsconfig.spec.json +20 -0
|
Binary file
|
package/dist/layers.png
ADDED
|
Binary file
|
|
Binary file
|
package/jest.config.ts
ADDED
package/package.json
ADDED
package/project.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bookito-widget",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/apps/bookito-widget/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"lint": {
|
|
8
|
+
"executor": "@nrwl/linter:eslint",
|
|
9
|
+
"outputs": ["{options.outputFile}"],
|
|
10
|
+
"options": {
|
|
11
|
+
"lintFilePatterns": [
|
|
12
|
+
"libs/apps/bookito-widget/**/*.ts",
|
|
13
|
+
"libs/apps/bookito-widget/**/*.tsx"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"test": {
|
|
18
|
+
"executor": "@nrwl/jest:jest",
|
|
19
|
+
"outputs": ["{workspaceRoot}/coverage/libs/apps/bookito-widget"],
|
|
20
|
+
"options": {
|
|
21
|
+
"jestConfig": "libs/apps/bookito-widget/jest.config.ts",
|
|
22
|
+
"passWithNoTests": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"bundle": {
|
|
26
|
+
"executor": "@nx/webpack:webpack",
|
|
27
|
+
"outputs": ["{options.outputPath}"],
|
|
28
|
+
"options": {
|
|
29
|
+
"libraryName": "bookito-widget",
|
|
30
|
+
"libraryTargets": ["umd"],
|
|
31
|
+
"index": "",
|
|
32
|
+
"tsConfig": "libs/apps/bookito-widget/tsconfig.json",
|
|
33
|
+
"main": "libs/apps/bookito-widget/src/index.tsx",
|
|
34
|
+
"outputPath": "libs/apps/bookito-widget/dist",
|
|
35
|
+
"compiler": "babel",
|
|
36
|
+
"optimization": true,
|
|
37
|
+
"extractLicenses": true,
|
|
38
|
+
"runtimeChunk": false,
|
|
39
|
+
"vendorChunk": false,
|
|
40
|
+
"generateIndexHtml": false,
|
|
41
|
+
"commonChunk": false,
|
|
42
|
+
"namedChunks": false,
|
|
43
|
+
"webpackConfig": "custom-config/webpack-lib.config.js",
|
|
44
|
+
"postcssConfig": "libs/apps/bookito-widget/postcss.config.js",
|
|
45
|
+
"isolatedConfig": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"version": {
|
|
49
|
+
"executor": "@jscutlery/semver:version",
|
|
50
|
+
"options": {
|
|
51
|
+
"postTargets": ["bookito-widget:publish-to-npm"]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"publish-to-npm": {
|
|
55
|
+
"executor": "nx:run-commands",
|
|
56
|
+
"options": {
|
|
57
|
+
"command": "npm publish",
|
|
58
|
+
"cwd": "libs/apps/bookito-widget"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"tags": []
|
|
63
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const EmptyCard = () => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="mr-5 animate-pulse min-w-min min-w-lg bg-white border border-gray-200 rounded-3xl shadow dark:bg-gray-800 dark:border-gray-700 text-left">
|
|
4
|
+
<div className="p-5 w-[80vw] md:w-80 lg:w-96">
|
|
5
|
+
<h5 className="mb-3 rounded-md max-w-fit px-2 bg-white text-md font-black tracking-tight text-gray-900 dark:text-black">
|
|
6
|
+
<div className="h-2 bg-slate-200 rounded"></div>
|
|
7
|
+
</h5>
|
|
8
|
+
|
|
9
|
+
<h5 className="mb-10 text-2xl font-bold tracking-tight text-gray-50 dark:text-white">
|
|
10
|
+
<div className="grid grid-cols-3 gap-4 mt-1">
|
|
11
|
+
<div className="h-4 bg-slate-200 rounded col-span-2"></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div className="grid grid-cols-3 gap-4 mt-2">
|
|
14
|
+
<div className="h-4 bg-slate-200 rounded col-span-1"></div>
|
|
15
|
+
</div>
|
|
16
|
+
</h5>
|
|
17
|
+
|
|
18
|
+
<div className="mb-2 dark:text-white text-gray-50 mt-6">
|
|
19
|
+
{new Array(4).fill(undefined).map((_, key) => (
|
|
20
|
+
<div className="grid grid-cols-3 gap-4 mt-2" key={key}>
|
|
21
|
+
<div className="h-2 bg-slate-200 rounded col-span-3"></div>
|
|
22
|
+
</div>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default EmptyCard;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LocalizedEventData } from '@carlito/bookito-client';
|
|
2
|
+
import formatPrice from '../utils';
|
|
3
|
+
|
|
4
|
+
type CardProps = { event: LocalizedEventData; openModal: () => void };
|
|
5
|
+
|
|
6
|
+
const ExperienceCard = (props: CardProps) => {
|
|
7
|
+
const { event, openModal } = props;
|
|
8
|
+
const { title, basePrice, description, bannerImg } = event;
|
|
9
|
+
|
|
10
|
+
const appenedDescription =
|
|
11
|
+
description && description.length > 200 ? description.substring(0, 200) + '...' : description;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
onClick={() => {
|
|
16
|
+
openModal();
|
|
17
|
+
}}
|
|
18
|
+
role="button"
|
|
19
|
+
style={{ backgroundImage: `url(${bannerImg?.url})` }}
|
|
20
|
+
className="relative mr-5 min-w-lg bg-no-repeat bg-cover bg-white border border-gray-200 rounded-3xl shadow dark:bg-gray-800 dark:border-gray-700 text-left overflow-hidden"
|
|
21
|
+
>
|
|
22
|
+
<div className="absolute w-full h-full top-0 left-0 bg-gradient-to-t from-neutral-900"></div>
|
|
23
|
+
<div className="p-5 w-[80vw] md:w-80 lg:w-96 relative z-1">
|
|
24
|
+
{title && (
|
|
25
|
+
<h5 className="mb-3 rounded-md max-w-fit px-2 bg-white text-md font-black tracking-tight text-gray-900 dark:text-black">
|
|
26
|
+
{title}
|
|
27
|
+
</h5>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
{basePrice?.amount && (
|
|
31
|
+
<h5 className="mb-10 text-2xl font-bold tracking-tight text-gray-50 dark:text-white">
|
|
32
|
+
{formatPrice(basePrice?.amount)}
|
|
33
|
+
</h5>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{appenedDescription && (
|
|
37
|
+
<p className="mb-2 dark:text-white text-gray-50 text-xs">{appenedDescription}</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default ExperienceCard;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LocalizedEventData } from '@carlito/bookito-client';
|
|
2
|
+
import ExperienceCard from '../Card';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
events: LocalizedEventData[];
|
|
6
|
+
openModal(event: LocalizedEventData): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const ExperienceCardHolder = ({ events, openModal }: Props) => {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
style={{
|
|
13
|
+
display: 'flex',
|
|
14
|
+
maxHeight: '200px',
|
|
15
|
+
flexFlow: 'inherit',
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
{events.map((event) => (
|
|
19
|
+
<ExperienceCard key={event.id} event={event} openModal={() => openModal(event)} />
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default ExperienceCardHolder;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { formatPrice, useActiveLanguage } from '@carlito/api';
|
|
2
|
+
import {
|
|
3
|
+
ListBookingsResponse,
|
|
4
|
+
LocalizedEventData,
|
|
5
|
+
findOccurrenceForDate,
|
|
6
|
+
useCreateBooking,
|
|
7
|
+
} from '@carlito/bookito-client';
|
|
8
|
+
import { applyTimeOfDate } from '@carlito/date-utils';
|
|
9
|
+
import { SupportedLanguages } from '@carlito/intl';
|
|
10
|
+
import { Formik, useFormikContext } from 'formik';
|
|
11
|
+
import humanizeDuration from 'humanize-duration';
|
|
12
|
+
import { useCallback } from 'react';
|
|
13
|
+
import { Trans } from 'react-i18next';
|
|
14
|
+
import FormSubmitButton from './FormSubmitButton';
|
|
15
|
+
import BookitoBookingRequestForm, { BookingParams } from './PartialForm';
|
|
16
|
+
import { isEventAllDay, useBookableTimeRange } from './helper';
|
|
17
|
+
type InitialBookingData = ListBookingsResponse['bookings'][0];
|
|
18
|
+
type FormValues = Partial<InitialBookingData>;
|
|
19
|
+
type EventInfoProps = {
|
|
20
|
+
price?: number | null;
|
|
21
|
+
overallDuration?: number | null;
|
|
22
|
+
stringifiedDuration: string;
|
|
23
|
+
activeLanguage: SupportedLanguages;
|
|
24
|
+
isAllDay?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type BookingFormProps = {
|
|
28
|
+
event: LocalizedEventData;
|
|
29
|
+
date: Date;
|
|
30
|
+
authId: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const supportedLanguages: Record<SupportedLanguages, string> = {
|
|
34
|
+
en: 'English',
|
|
35
|
+
de: 'Deutsch',
|
|
36
|
+
it: 'Italiano',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ChooseLanguage = () => {
|
|
40
|
+
const { getFieldProps } = useFormikContext();
|
|
41
|
+
|
|
42
|
+
const fieldName = `bookedBy.communicationLanguage`;
|
|
43
|
+
|
|
44
|
+
const { value: language, onBlur, onChange } = getFieldProps<SupportedLanguages>(fieldName);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<select
|
|
48
|
+
className="text-sm mb-3 rounded-lg outline-none border border-gray-300 focus:border-primary-default focus:ring-2 focus:ring-primary-default focus:ring-offset-1"
|
|
49
|
+
value={language}
|
|
50
|
+
onChange={onChange}
|
|
51
|
+
onBlur={onBlur}
|
|
52
|
+
name={fieldName}
|
|
53
|
+
>
|
|
54
|
+
{Object.entries(supportedLanguages).map(([key, value]) => (
|
|
55
|
+
<option key={key} value={key}>
|
|
56
|
+
{value}
|
|
57
|
+
</option>
|
|
58
|
+
))}
|
|
59
|
+
</select>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const EventInfo = ({
|
|
64
|
+
price,
|
|
65
|
+
overallDuration,
|
|
66
|
+
stringifiedDuration,
|
|
67
|
+
activeLanguage,
|
|
68
|
+
isAllDay,
|
|
69
|
+
}: EventInfoProps) => {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex justify-between items-start">
|
|
72
|
+
{price && price !== null ? (
|
|
73
|
+
<div>
|
|
74
|
+
<p className="font-bold">
|
|
75
|
+
<Trans>Price</Trans>
|
|
76
|
+
</p>
|
|
77
|
+
<p className="text-3xl font-bold">{formatPrice(price, activeLanguage)}</p>
|
|
78
|
+
</div>
|
|
79
|
+
) : (
|
|
80
|
+
<div />
|
|
81
|
+
)}
|
|
82
|
+
{overallDuration && (
|
|
83
|
+
<div className="text-right">
|
|
84
|
+
<p className="font-bold">
|
|
85
|
+
<Trans>Duration</Trans>
|
|
86
|
+
</p>
|
|
87
|
+
{isAllDay ? (
|
|
88
|
+
<p className="text-xl">
|
|
89
|
+
<Trans>All Day</Trans>
|
|
90
|
+
</p>
|
|
91
|
+
) : (
|
|
92
|
+
<p className="text-3xl">
|
|
93
|
+
<Trans>{stringifiedDuration}</Trans>
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const BookingForm = ({ event: eventParams, date: onDate, authId }: BookingFormProps) => {
|
|
103
|
+
const { activeLanguage } = useActiveLanguage();
|
|
104
|
+
const { mutate: createBooking, isLoading, isSuccess, reset: resetMutation } = useCreateBooking();
|
|
105
|
+
|
|
106
|
+
const onSuccessAnimationEnd = useCallback(() => {
|
|
107
|
+
resetMutation();
|
|
108
|
+
}, [resetMutation]);
|
|
109
|
+
|
|
110
|
+
const initialValues: FormValues = {
|
|
111
|
+
bookedBy: {
|
|
112
|
+
name: '',
|
|
113
|
+
email: '',
|
|
114
|
+
roomNr: '',
|
|
115
|
+
communicationLanguage: activeLanguage,
|
|
116
|
+
},
|
|
117
|
+
chosenTime: eventParams.timeBookabilitySettings ? [] : undefined,
|
|
118
|
+
comment: '',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const occurrence = findOccurrenceForDate(onDate, eventParams) ?? eventParams?.occurrences[0];
|
|
122
|
+
|
|
123
|
+
const overallDuration =
|
|
124
|
+
(eventParams?.timeBookability === 'custom'
|
|
125
|
+
? eventParams?.timeBookabilitySettings?.timeBlockDurationMs
|
|
126
|
+
: occurrence?.durationMs) ?? occurrence.durationMs;
|
|
127
|
+
|
|
128
|
+
const stringifiedDuration = humanizeDuration(overallDuration ?? 0, {
|
|
129
|
+
language: 'shortDe',
|
|
130
|
+
// fallbacks: ['en'],
|
|
131
|
+
languages: {
|
|
132
|
+
shortDe: {
|
|
133
|
+
y: () => 'y',
|
|
134
|
+
mo: () => 'mo',
|
|
135
|
+
w: () => 'w',
|
|
136
|
+
d: () => 'd',
|
|
137
|
+
h: () => 'h',
|
|
138
|
+
m: () => 'm',
|
|
139
|
+
s: () => 's',
|
|
140
|
+
ms: () => 'ms',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
units: ['h', 'm'],
|
|
144
|
+
maxDecimalPoints: 0,
|
|
145
|
+
delimiter: ' ',
|
|
146
|
+
} as any);
|
|
147
|
+
|
|
148
|
+
const startDateTime = applyTimeOfDate(onDate, new Date(occurrence.startDateTime));
|
|
149
|
+
const isAllDay = eventParams ? isEventAllDay(startDateTime, eventParams) : undefined;
|
|
150
|
+
|
|
151
|
+
function onSubmit(values: FormValues) {
|
|
152
|
+
createBooking({
|
|
153
|
+
bookedForDate: onDate.toISOString(),
|
|
154
|
+
eventId: eventParams.id,
|
|
155
|
+
bookedBy: {
|
|
156
|
+
name: values.bookedBy?.name ?? '',
|
|
157
|
+
email: values.bookedBy?.email ?? '',
|
|
158
|
+
communicationLanguage: activeLanguage,
|
|
159
|
+
roomNr: values.bookedBy?.roomNr,
|
|
160
|
+
},
|
|
161
|
+
comment: values?.comment,
|
|
162
|
+
priceVariants: values.priceVariants,
|
|
163
|
+
numOfAdditionalParticipants: values.numOfAdditionalParticipants,
|
|
164
|
+
chosenTime: values.chosenTime?.map((time) => ({
|
|
165
|
+
dateTime: time.dateTime.toString(),
|
|
166
|
+
})),
|
|
167
|
+
ownerAuthId: authId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const bookableTimeRange = useBookableTimeRange(eventParams, onDate);
|
|
172
|
+
|
|
173
|
+
const priceVariants = eventParams.priceVariants?.map((priceVariant) => {
|
|
174
|
+
return {
|
|
175
|
+
id: priceVariant.id,
|
|
176
|
+
displayName: priceVariant.description ?? '',
|
|
177
|
+
price: priceVariant.priceVariation.amount,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const canChooseTime = eventParams.timeBookability === 'custom';
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
|
185
|
+
{(props) => {
|
|
186
|
+
const { values, isSubmitting, handleChange, handleBlur, handleSubmit, setValues, touched } =
|
|
187
|
+
props;
|
|
188
|
+
|
|
189
|
+
const isTouched = Object.values(touched).some((t) => t);
|
|
190
|
+
|
|
191
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
192
|
+
const onBookingParamsChange = useCallback(
|
|
193
|
+
(requestParams: BookingParams) => {
|
|
194
|
+
setValues({
|
|
195
|
+
...values,
|
|
196
|
+
numOfAdditionalParticipants:
|
|
197
|
+
(requestParams.numParticipants ?? 0) > 1
|
|
198
|
+
? (requestParams.numParticipants ?? 0) - 1
|
|
199
|
+
: undefined,
|
|
200
|
+
priceVariants: requestParams.priceVariants,
|
|
201
|
+
chosenTime: requestParams.chosenTimes?.map((time) => ({
|
|
202
|
+
dateTime: time.toISOString(),
|
|
203
|
+
})),
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
[setValues, values]
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<form className="flex mt-10 flex-col" onSubmit={handleSubmit}>
|
|
211
|
+
<EventInfo
|
|
212
|
+
price={eventParams.basePrice?.amount || 0}
|
|
213
|
+
activeLanguage={activeLanguage}
|
|
214
|
+
isAllDay={isAllDay}
|
|
215
|
+
stringifiedDuration={stringifiedDuration}
|
|
216
|
+
overallDuration={overallDuration}
|
|
217
|
+
/>
|
|
218
|
+
<BookitoBookingRequestForm
|
|
219
|
+
onChange={onBookingParamsChange}
|
|
220
|
+
canChooseTime={canChooseTime}
|
|
221
|
+
eventPriceVariants={priceVariants}
|
|
222
|
+
bookingParams={{
|
|
223
|
+
numParticipants: (values.numOfAdditionalParticipants ?? 0) + 1,
|
|
224
|
+
chosenTimes: values.chosenTime?.map((time) => new Date(time.dateTime)),
|
|
225
|
+
priceVariants: values.priceVariants,
|
|
226
|
+
}}
|
|
227
|
+
bookableTimeRange={bookableTimeRange}
|
|
228
|
+
/>
|
|
229
|
+
|
|
230
|
+
<div className="mt-3 flex flex-col">
|
|
231
|
+
<label htmlFor="name">Name</label>
|
|
232
|
+
<input
|
|
233
|
+
id="name"
|
|
234
|
+
name="bookedBy.name"
|
|
235
|
+
type="text"
|
|
236
|
+
onChange={handleChange}
|
|
237
|
+
value={values.bookedBy?.name}
|
|
238
|
+
className="mb-3 rounded-lg"
|
|
239
|
+
/>
|
|
240
|
+
<label htmlFor="roomNr">Room Nr / Booking Reference</label>
|
|
241
|
+
<input
|
|
242
|
+
id="roomNr"
|
|
243
|
+
name="bookedBy.roomNr"
|
|
244
|
+
type="text"
|
|
245
|
+
onChange={handleChange}
|
|
246
|
+
value={values.bookedBy?.roomNr}
|
|
247
|
+
className="mb-3 rounded-lg"
|
|
248
|
+
/>
|
|
249
|
+
<label htmlFor="email" style={{ display: 'block' }}>
|
|
250
|
+
Email
|
|
251
|
+
</label>
|
|
252
|
+
<input
|
|
253
|
+
id="email"
|
|
254
|
+
placeholder="Enter your email"
|
|
255
|
+
name="bookedBy.email"
|
|
256
|
+
type="email"
|
|
257
|
+
value={values.bookedBy?.email}
|
|
258
|
+
onChange={handleChange}
|
|
259
|
+
onBlur={handleBlur}
|
|
260
|
+
className="mb-3 rounded-lg"
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
<label htmlFor="comments" style={{ display: 'block' }}>
|
|
264
|
+
Comments
|
|
265
|
+
</label>
|
|
266
|
+
<textarea name="comment" placeholder="Comments if any" className="mb-3 rounded-lg" />
|
|
267
|
+
|
|
268
|
+
<ChooseLanguage />
|
|
269
|
+
|
|
270
|
+
<div className="flex-1">
|
|
271
|
+
<FormSubmitButton
|
|
272
|
+
onSuccessAnimationEnd={onSuccessAnimationEnd}
|
|
273
|
+
success={isSuccess}
|
|
274
|
+
loading={isLoading}
|
|
275
|
+
disabled={!isTouched || isSubmitting}
|
|
276
|
+
>
|
|
277
|
+
Submit
|
|
278
|
+
</FormSubmitButton>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</form>
|
|
282
|
+
);
|
|
283
|
+
}}
|
|
284
|
+
</Formik>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.checkmark__circle {
|
|
2
|
+
stroke-dasharray: 166;
|
|
3
|
+
stroke-dashoffset: 166;
|
|
4
|
+
stroke-width: 2;
|
|
5
|
+
stroke-miterlimit: 10;
|
|
6
|
+
stroke: #7ac142;
|
|
7
|
+
fill: none;
|
|
8
|
+
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
|
9
|
+
}
|
|
10
|
+
.checkmark {
|
|
11
|
+
width: 30px;
|
|
12
|
+
height: 30px;
|
|
13
|
+
border-radius: 50%;
|
|
14
|
+
display: block;
|
|
15
|
+
stroke-width: 2;
|
|
16
|
+
stroke: #fff;
|
|
17
|
+
stroke-miterlimit: 10;
|
|
18
|
+
box-shadow: inset 0px 0px 0px #7ac142;
|
|
19
|
+
animation: fill 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both;
|
|
20
|
+
}
|
|
21
|
+
.checkmark__check {
|
|
22
|
+
transform-origin: 50% 50%;
|
|
23
|
+
stroke-dasharray: 48;
|
|
24
|
+
stroke-dashoffset: 48;
|
|
25
|
+
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
|
|
26
|
+
}
|
|
27
|
+
@keyframes stroke {
|
|
28
|
+
100% {
|
|
29
|
+
stroke-dashoffset: 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@keyframes scale {
|
|
33
|
+
0%,
|
|
34
|
+
100% {
|
|
35
|
+
transform: none;
|
|
36
|
+
}
|
|
37
|
+
50% {
|
|
38
|
+
transform: scale3d(1.1, 1.1, 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
@keyframes fill {
|
|
42
|
+
100% {
|
|
43
|
+
box-shadow: inset 0px 0px 0px 30px #7ac142;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import styles from './index.module.css';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const AnimatedSuccessCheckmark = ({ className }: Props) => {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
className={classNames(className, styles['checkmark'])}
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
viewBox="0 0 52 52"
|
|
14
|
+
>
|
|
15
|
+
<circle className={styles['checkmark__circle']} cx="26" cy="26" r="25" fill="none" />{' '}
|
|
16
|
+
<path className={styles['checkmark__check']} fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default AnimatedSuccessCheckmark;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { LoadingSpinner } from '@carlito/dashboard-ui';
|
|
2
|
+
import classnames from 'classnames';
|
|
3
|
+
import { ComponentProps, useEffect } from 'react';
|
|
4
|
+
import AnimatedSuccessCheckmark from './AnimatedSuccessCheckmark';
|
|
5
|
+
|
|
6
|
+
interface Props extends ComponentProps<'button'> {
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
success?: boolean;
|
|
9
|
+
onSuccessAnimationEnd?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FormSubmitButton = ({
|
|
13
|
+
className,
|
|
14
|
+
success,
|
|
15
|
+
loading,
|
|
16
|
+
disabled,
|
|
17
|
+
children,
|
|
18
|
+
onSuccessAnimationEnd,
|
|
19
|
+
...btnProps
|
|
20
|
+
}: Props) => {
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (success) {
|
|
23
|
+
const timeout = setTimeout(() => {
|
|
24
|
+
onSuccessAnimationEnd?.();
|
|
25
|
+
}, 2000);
|
|
26
|
+
return () => clearTimeout(timeout);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined;
|
|
30
|
+
}, [success, onSuccessAnimationEnd]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="relative">
|
|
34
|
+
{success && <AnimatedSuccessCheckmark className="absolute left-2 top-2" />}
|
|
35
|
+
<button
|
|
36
|
+
type="submit"
|
|
37
|
+
id="product-tour-save"
|
|
38
|
+
disabled={loading || disabled}
|
|
39
|
+
className={classnames(
|
|
40
|
+
className,
|
|
41
|
+
{ 'bg-gray-300': disabled },
|
|
42
|
+
{
|
|
43
|
+
'bg-primary-default hover:bg-primary-tint focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-default':
|
|
44
|
+
!disabled,
|
|
45
|
+
},
|
|
46
|
+
{ 'cursor-not-allowed': loading },
|
|
47
|
+
'w-full flex justify-center items-center py-2 px-4 border border-transparent rounded-md shadow-sm text-lg font-extrabold text-primary-contrast'
|
|
48
|
+
)}
|
|
49
|
+
{...btnProps}
|
|
50
|
+
>
|
|
51
|
+
<div className="w-0">{loading && <LoadingSpinner className="h-5 w-5" />}</div>
|
|
52
|
+
<div className="w-full pl-3">{children}</div>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default FormSubmitButton;
|