Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc997739b4 | ||
|
|
542d9aa3c6 | ||
|
|
8f3e088db7 | ||
|
|
699313ac70 | ||
|
|
fca7087e7d | ||
|
|
a6b16490c0 | ||
|
|
5da735c418 | ||
|
|
f9ef883f3d | ||
|
|
9ddbaf0966 | ||
|
|
0695ce7edb | ||
|
|
74f0d39997 | ||
|
|
85ab3fb164 | ||
|
|
ab417ac267 | ||
|
|
24635af730 | ||
|
|
682f0c66ff | ||
|
|
ace27ca84e | ||
|
|
ce41397517 | ||
|
|
81aba55710 | ||
|
|
c5fc3a7e8e | ||
|
|
f628ac4366 | ||
|
|
6e2e252c49 | ||
|
|
10d3a126aa | ||
|
|
341c39cb6b | ||
|
|
91909281bc | ||
|
|
f14b13a1ba | ||
|
|
ca859c871c | ||
|
|
6e49a3e1c3 | ||
|
|
830f42bf9a | ||
|
|
0830523b84 | ||
|
|
612b293a70 | ||
|
|
3ecfa75f86 | ||
|
|
631e974769 | ||
|
|
42c128b790 | ||
|
|
7d1065d228 | ||
|
|
e2747f89d0 | ||
|
|
a1d4589e66 | ||
|
|
7d2f87a13e | ||
|
|
4e7200a44b | ||
|
|
52959fe257 | ||
|
|
61b010a60a | ||
|
|
4e6e484d8b | ||
|
|
0a2de203a6 | ||
|
|
48ee7a956c | ||
|
|
ae46d615ef | ||
|
|
9b6d5d2ff8 | ||
|
|
ec0352da20 | ||
|
|
7e65d5d61d | ||
|
|
f855f24d5c | ||
|
|
c2057c4cc2 | ||
|
|
1640264e40 | ||
|
|
579fd399c3 | ||
|
|
1de00d46bf | ||
|
|
01db3ad14e | ||
|
|
c1cb02a9d1 | ||
|
|
344710d1f9 | ||
|
|
6f0b4f94e6 | ||
|
|
c6ad2de2f9 | ||
|
|
9ae3a0eaba | ||
|
|
2f3d7318a4 | ||
|
|
6e71de7ca3 | ||
|
|
7d277ad89f | ||
|
|
ac276a9654 | ||
|
|
588748d78e | ||
|
|
25711ef18e | ||
|
|
440f53714b | ||
|
|
22f6d8ab9b | ||
|
|
57f0d115cf | ||
|
|
23bc6aa600 | ||
|
|
abdb196f56 | ||
|
|
8c0e2b5129 | ||
|
|
cf32287109 | ||
|
|
2ef8e230c5 | ||
|
|
546d0d50c4 | ||
|
|
33e8198779 | ||
|
|
e7dd0ff3e9 | ||
|
|
1b2707d4f0 | ||
|
|
00071bcdf0 | ||
|
|
fc5cc5b8bc | ||
|
|
1e79a55825 | ||
|
|
4645bbcb75 | ||
|
|
95b903110f | ||
|
|
2917455324 | ||
|
|
d71765848c | ||
|
|
cf7b7d7d3d | ||
|
|
96aaa36751 | ||
|
|
52e74245a4 | ||
|
|
cc03b132e6 | ||
|
|
b787d98502 | ||
|
+4 |
d55ffe8d9d | ||
|
|
1a776a7320 | ||
|
|
9253257699 | ||
|
|
fc74b686b6 | ||
|
|
400a80f8e2 | ||
|
|
00c9584ee3 | ||
|
|
26e07bc2d8 | ||
|
|
8da31cb293 | ||
|
|
a9b41766f2 | ||
|
|
859bf60ee5 | ||
|
|
96451c5eb9 | ||
|
|
4b0f7019a3 | ||
|
|
7393ba1d17 | ||
|
+15 |
0c78dbea27 | ||
|
|
7a95e714c2 | ||
|
|
0d9d182b56 | ||
|
|
d3b9ebbd34 | ||
|
|
d35ce9c95e | ||
|
|
909b37ed44 | ||
|
|
33e8b5d227 | ||
|
|
a3462657db | ||
|
|
35c2f9046a | ||
|
|
2281c7cf9f | ||
|
|
e29d9cd545 | ||
|
|
f09af2d393 | ||
|
|
34c8affacd | ||
|
|
d302dc164d | ||
|
|
34d417515c | ||
|
|
b1a1525471 | ||
|
|
93b59d60c3 | ||
|
|
c3cb839027 | ||
|
|
91c39a0306 | ||
|
|
f1c15f4e8a | ||
|
|
3f29df16ad | ||
|
|
3a06947c8a | ||
|
|
f486b0242f | ||
|
|
9bc2676822 | ||
|
|
5c72aa3497 | ||
|
|
09da911bf9 | ||
|
|
2fbaec41c9 | ||
|
|
3bada22900 | ||
|
|
45059554b6 | ||
|
|
8b69359ccb | ||
|
|
b457125915 | ||
|
|
01e0fed3c5 | ||
|
|
c09107cec8 | ||
|
|
2f9905e548 | ||
|
|
ab4d910564 | ||
|
|
34bca4984f | ||
|
|
0907d154c7 | ||
|
|
6f807930f6 | ||
|
|
e34405046f | ||
|
|
cefb46dbdd | ||
|
|
b22786d7df | ||
|
|
735254d48e | ||
|
|
404168fe7d | ||
|
|
6c4a372121 | ||
|
|
3c60b4f569 | ||
|
|
2292343db6 | ||
|
|
5db525a62d | ||
|
|
b2d3432438 | ||
|
|
140bb3836a | ||
|
|
6b2d675043 | ||
|
|
7e917cdcbb | ||
|
|
50f63ef000 | ||
|
|
1deca816bd | ||
|
|
7e52e3d295 | ||
|
+7 |
ded1fce6fa | ||
|
|
27f58c70d3 | ||
|
|
869ebb092b | ||
|
|
5f3e9efa6d | ||
|
|
078ca46a28 | ||
|
|
ef7863ad0b | ||
|
|
9557c1818b | ||
|
|
20fd45a3fd | ||
|
|
e6849174c1 | ||
|
|
a9b79eafb6 | ||
|
|
e50bc0f198 | ||
|
|
63aae60af4 | ||
|
|
c215f7de03 | ||
|
|
3ce687c0a4 | ||
|
|
20e9e9763a | ||
|
|
5cd2ce9246 | ||
|
|
770c35039d | ||
|
|
0cab371db4 | ||
|
|
15a0216cc5 | ||
|
|
a385a4bb38 | ||
|
|
5965129851 | ||
|
|
3e92571078 | ||
|
|
83f0ba7ba4 | ||
|
|
b0de653166 | ||
|
|
1c46370572 | ||
|
|
99f11dcc5c | ||
|
|
d7e523d365 | ||
|
|
342c281f3c | ||
|
|
d0225824fb | ||
|
|
79fd1bd76f | ||
|
|
293c2ffb86 | ||
|
|
4162d403e1 | ||
|
|
bb24539731 | ||
|
|
0b8eb3b8ec | ||
|
|
38ccd7b159 | ||
|
|
4aa9c830c1 | ||
|
|
0c2a25fb78 | ||
|
|
dce7973825 | ||
|
|
4771a1a64a | ||
|
|
bb7087640d | ||
|
|
d7ec335a34 | ||
|
|
54f1eef308 | ||
|
|
f1b95cd28d | ||
|
|
6ee878aab0 | ||
|
|
1bb987556d | ||
|
|
306a1212cf | ||
|
|
f7119849db | ||
|
|
a8c1851c04 | ||
|
|
3c8cc4e624 | ||
|
|
6958c710cf | ||
|
|
38bd111260 | ||
|
|
290fb8db55 | ||
|
|
3e0f14ea29 | ||
|
|
984281963e | ||
|
|
ec8be2994c | ||
|
|
8996a2aa8e | ||
|
|
4856ed9977 | ||
|
|
d36d925788 | ||
|
|
de54d75cd3 | ||
|
|
70ac949bea | ||
|
|
34408c5593 | ||
|
|
7da272f67a | ||
|
|
46e46bc1de | ||
|
|
7b36df9eb3 | ||
|
|
b4d1cb0988 | ||
|
|
e52af8d387 | ||
|
|
e9b6e5144a | ||
|
|
0c6c23e1b0 | ||
|
|
458406feaf | ||
|
|
b9fc3ac076 | ||
|
|
00f5ba2ac5 | ||
|
|
f1436da96c | ||
|
|
30f8d12563 | ||
|
|
1561224dbb | ||
|
|
f3bedabf92 | ||
|
|
58b3067b2b | ||
|
|
6a6adf1c3c | ||
|
|
2027447eab | ||
|
|
13c2dc24dc | ||
|
|
7f2db86a83 | ||
|
|
91bc6ae820 | ||
|
|
008706e5ac | ||
|
|
dad2fd098e | ||
|
|
c0b83c1df6 | ||
|
|
851fb25430 | ||
|
|
00d50f0285 | ||
|
|
efc0bf6a5b | ||
|
|
dd9d7ab485 | ||
|
+4 |
3a35eb413f | ||
|
|
62ab3056a5 | ||
|
|
f9d639b157 | ||
|
|
b30ab45612 | ||
|
|
143279699a | ||
|
|
bbf22a29b0 | ||
|
|
5ca2c0561c | ||
|
|
addd58cca6 | ||
|
|
dc6f8d1f63 | ||
|
|
dc68b608a0 | ||
|
|
6fdbadc94b | ||
|
|
046dadbae2 | ||
|
|
d0c66a0a64 | ||
|
|
0ea42faa57 | ||
|
|
35459a046b | ||
|
|
18d697436c | ||
|
|
faf62ac8e7 | ||
|
|
8653e8eea5 | ||
|
|
49a379ec6c | ||
|
|
52b2ce8774 | ||
|
|
10815c9541 | ||
|
|
ebd4ce0340 | ||
|
|
0e63996ee2 | ||
|
|
898c83d1ad | ||
|
|
d0aaa4d83b | ||
|
|
c610c73378 | ||
|
|
8602a0a6e1 | ||
|
|
7b0d4f0f78 | ||
|
|
b0a1902ca3 | ||
|
|
ae5030198c | ||
|
|
8a2005ccf1 | ||
|
|
1c90dcaa62 | ||
|
|
4ea29ed2ac | ||
|
|
45bdfcfb7d | ||
|
|
93adf57d00 | ||
|
|
5e77b95e48 | ||
|
|
dbca3ee780 | ||
|
|
c9d384c397 | ||
|
|
a213fa95f8 | ||
|
|
1948455be0 | ||
|
|
015ed6d4f9 | ||
|
|
265af73ad0 | ||
|
|
46f2142e5f | ||
|
|
bcb7861a04 | ||
|
|
52289a5cb5 | ||
|
|
f353da625e | ||
|
|
26b771064d | ||
|
|
f3be103456 | ||
|
|
0e54f755e3 | ||
|
|
9b9a16b847 | ||
|
|
31abf01724 | ||
|
|
11b52c839c | ||
|
|
7d7e4464d2 | ||
|
|
03a9996c84 | ||
|
|
d27b7ab2c4 | ||
|
|
b09d727d3f | ||
|
|
0db1b3f023 | ||
|
|
a97bfbb1ce | ||
|
|
d87ddef135 | ||
|
|
c455639ff4 | ||
|
|
d5b7195d67 | ||
|
|
fe67b99096 | ||
|
|
84cff3613a | ||
|
|
4308138170 | ||
|
|
d876688325 | ||
|
|
f4fe91396f | ||
|
|
58c4c894fd | ||
|
+2 |
eeabb53c82 | ||
|
|
6c3dcc2fbf | ||
|
|
397d47b4b5 | ||
|
|
e020bb9758 | ||
|
|
dbbf77f21a | ||
|
|
19ae065f68 | ||
|
|
bb0ff5503a | ||
|
|
e17c463890 | ||
|
|
f5e394bb57 | ||
|
|
2414cacabb | ||
|
|
dd24ef4946 | ||
|
|
a3eeb9e3a3 | ||
|
|
b151ccd4c6 | ||
|
|
0c8847fb87 | ||
|
|
b8b1b9a6d0 | ||
|
|
f5c1c76f0a | ||
|
|
e66050a414 | ||
|
|
1a81459e28 | ||
|
|
ad8d0a4e77 | ||
|
|
fe12a83898 | ||
|
|
7d611c5527 | ||
|
|
0bd8c23be7 | ||
|
|
fdc8a839b4 | ||
|
|
3da3e6ec38 | ||
|
|
fb3c481235 | ||
|
|
3527c5126a | ||
|
|
d75a8910f6 | ||
|
|
037410aea7 | ||
|
|
54bd1436b1 | ||
|
|
a262d261a7 | ||
|
|
e4e4ec5ffc | ||
|
|
00c00a9aee | ||
|
|
85cc62d0cb | ||
|
|
ac50492fbc | ||
|
|
4b771a5bde | ||
|
|
944f4dc43b | ||
|
|
cfe7c7a5b3 | ||
|
|
d1c5268ef0 | ||
|
|
02ac593ebf | ||
|
|
1d927a8b33 | ||
|
|
090a977f72 | ||
|
|
2d3a6fb4ca | ||
|
|
974ff03127 | ||
|
|
4ef666a610 | ||
|
|
e0744a4857 | ||
|
|
fac99c3e9b | ||
|
|
006badc3b5 | ||
|
|
19e9e0083e | ||
|
|
19fe931f4b | ||
|
|
0856339e3a | ||
|
|
cf321496d4 | ||
|
|
c6e54d9d9d | ||
|
|
b00402f429 | ||
|
|
6c86317081 | ||
|
|
87cce8eab6 | ||
|
|
11dc9c668e | ||
|
|
8f6ba440b7 | ||
|
|
18c949207e | ||
|
|
b84c8d9e82 | ||
|
|
4feda21d22 | ||
|
|
45f16333a8 | ||
|
|
9c3cea512f | ||
|
|
7f94ce0605 | ||
|
|
1b541ff214 | ||
|
|
5950b62b02 | ||
|
|
436f88a48e | ||
|
|
c17e949cd6 | ||
|
|
b88f0617d8 | ||
|
|
6413bb399e | ||
|
|
bfe780e0af | ||
|
|
4e73c4f7fb | ||
|
|
f4ea385c7f | ||
|
|
cb1d881cc0 | ||
|
+1 |
27c5bf9dbc | ||
|
|
ef7dd5102b | ||
|
|
0c3e309596 | ||
|
|
1b942c4003 | ||
|
|
b328527434 | ||
|
|
662b437a18 | ||
|
|
e7f1413ed8 | ||
|
|
071b20b6ec | ||
|
|
bf6dd6cb32 | ||
|
|
c44e444271 | ||
|
|
a06c564517 | ||
|
|
02b5a2bc1f | ||
|
|
6e161da35b | ||
|
|
b257cdbf1e | ||
|
|
9de723d557 | ||
|
|
081d5ae9c8 | ||
|
|
cddca2b10d | ||
|
|
6ed0e5e281 | ||
|
|
1fd8d50e52 | ||
|
|
5206a37fe4 | ||
|
|
025019f789 | ||
|
|
d58bd02cab | ||
|
|
b8acf1e180 | ||
|
|
d8fab68edd | ||
|
|
592d46f758 | ||
|
|
a331d02668 | ||
|
|
73e38384e2 | ||
|
|
f5b5150cf3 | ||
|
|
7e0dbee2ca | ||
|
|
46715a6cd4 | ||
|
|
1e5cb0bbd0 | ||
|
|
3a54ca2c95 | ||
|
|
79d37676fc | ||
|
|
4f76654813 | ||
|
|
6dcb0c43ab | ||
|
|
cf9116b5bb | ||
|
|
4df54ef3d1 | ||
|
|
82c98a8b7b | ||
|
|
0d7be1886e | ||
|
|
bc6c9e1b0a | ||
|
|
732acb5b25 | ||
|
|
5b6818a3cc | ||
|
|
497d4902af | ||
|
|
8c7579fede | ||
|
|
4ce6b91a79 | ||
|
|
8d0caa522e | ||
|
|
89510e6af6 | ||
|
|
297a965508 | ||
|
|
c644a10688 | ||
|
|
8ff762de3e | ||
|
|
fb2ed0b5ad | ||
|
|
358d733b5f | ||
|
|
22a4a1b2ab | ||
|
|
bcd80eab34 | ||
|
|
c3fbf8224b | ||
|
|
4cb5a56d41 | ||
|
|
5e013d5864 | ||
|
|
d90a431dcc | ||
|
|
ee02112a7c | ||
|
|
870f70fc38 | ||
|
|
cc48b87fa3 | ||
|
|
a839cf7f59 | ||
|
|
935491c70f | ||
|
|
62c168176d | ||
|
|
50a20e2c3b | ||
|
|
f28a1312a6 | ||
|
|
4c89a64f0e | ||
|
|
f57726dc95 | ||
|
|
a2a5d48037 | ||
|
|
870cb1f872 | ||
|
|
accf845f0e | ||
|
|
6d79f80928 | ||
|
|
14f0c30584 | ||
|
|
4bfa63e804 | ||
|
|
d363f6e2f6 | ||
|
|
e8d6d50bc2 | ||
|
|
9221042db4 | ||
|
|
b936b7c256 | ||
|
|
57869f0b88 | ||
|
|
7a4795bdbf | ||
|
|
773bb5424b | ||
|
|
2c600cff43 | ||
|
|
0eeee42811 | ||
|
|
8eb45ee1eb | ||
|
|
87d4afea13 | ||
|
|
7776bbbf52 | ||
|
|
88384192f8 | ||
|
|
f036f0fb64 | ||
|
|
d0f34aa699 | ||
|
+2 |
507f5d028e | ||
|
|
477c411611 | ||
|
|
3652ffa40a | ||
|
|
1332b76441 | ||
|
|
1a3aaa134e | ||
|
|
af06ded584 | ||
|
|
2f9862bb47 | ||
|
|
22d3fb7e92 | ||
|
|
3ed206df23 | ||
|
|
ff3bd5fc61 | ||
|
|
be16b72400 | ||
|
|
775fe92960 | ||
|
|
70d6c8d8e7 | ||
|
|
f589d06ed4 | ||
|
|
2b7b03fffd | ||
|
|
a53ca3389c | ||
|
|
85764b6aa6 | ||
|
|
ee767a3e41 | ||
|
|
414233961c | ||
|
|
7747b3d9a0 | ||
|
|
da638c4b1b | ||
|
|
218c3d7b50 | ||
|
|
37309e8435 | ||
|
|
2aee450c0b | ||
|
|
ca7a78dcda | ||
|
|
fc4b88e222 | ||
|
|
affedd1014 | ||
|
|
4240235cb8 | ||
|
|
2a73fd8188 | ||
|
|
e878e1a665 | ||
|
|
298a6d5e15 | ||
|
|
532723fb41 | ||
|
|
b3b82fa1cf | ||
|
|
48a1f22ca5 | ||
|
|
77bf1d0712 | ||
|
|
23c79fff75 | ||
|
|
abea19ad39 | ||
|
|
5ca2d7cae2 | ||
|
|
3ac140fad5 | ||
|
|
5358e5848d | ||
|
|
6301ea16f8 | ||
|
|
e767d233ad | ||
|
|
7480a73905 | ||
|
|
fe50b8de18 | ||
|
|
fd8b323e47 | ||
|
|
760026b2e6 | ||
|
|
53de4ddaaf | ||
|
|
e7418f68a4 | ||
|
|
3ae4655449 | ||
|
|
299f69aa7b | ||
|
|
c1896fc833 | ||
|
|
54acf6d28f | ||
|
|
7e8f13148e | ||
|
|
b42f0ffadd | ||
|
|
9346ae644f | ||
|
|
d6fd9b5df3 | ||
|
|
529adc4137 | ||
|
|
c9d7412a43 | ||
|
|
dba48ab852 | ||
|
|
0bca141b74 | ||
|
|
a4e91a0666 | ||
|
|
8efb98698d | ||
|
|
1403a567ec | ||
|
|
bd6a84e986 | ||
|
|
bd41d9d5f0 | ||
|
|
7e7938a4d9 | ||
|
|
2f03f18447 | ||
|
|
2b054862df | ||
|
|
382f11f740 | ||
|
|
920050022a | ||
|
|
95a0496f32 | ||
|
|
1ac36882ac | ||
|
|
a4b8cc8391 | ||
|
|
e7a91d4594 | ||
|
|
54c8c294fc | ||
|
|
49f584a956 | ||
|
|
67f72223bf | ||
|
|
534efe53fe | ||
|
|
5437005c7e | ||
|
|
b931cf2d7c | ||
|
|
464343f5ab | ||
|
|
84b4bb397f | ||
|
|
b90cf96ab9 | ||
|
|
5d61d2d79b | ||
|
|
1be5510c5e | ||
|
|
e871781079 | ||
|
|
bfa70dcc83 | ||
|
|
b3bd8c1a58 | ||
|
|
2c23949cea | ||
|
|
6e3825d95a | ||
|
|
525ce0d075 | ||
|
|
7537ea5b91 | ||
|
|
47468798fd | ||
|
|
587526186f | ||
|
|
7582d93d6a | ||
|
|
b9645b4de5 |
No files matched your search
@@ -87,4 +87,17 @@ VITAL_REGION="us"
|
|||||||
# Used for the Zapier integration
|
# Used for the Zapier integration
|
||||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
||||||
ZAPIER_INVITE_LINK=""
|
ZAPIER_INVITE_LINK=""
|
||||||
|
|
||||||
|
# - LARK
|
||||||
|
# Needed to enable Lark Calendar integration and Login with Lark
|
||||||
|
# @see <https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g>
|
||||||
|
LARK_OPEN_APP_ID=""
|
||||||
|
LARK_OPEN_APP_SECRET=""
|
||||||
|
LARK_OPEN_VERIFICATION_TOKEN=""
|
||||||
|
|
||||||
|
# - WEB3
|
||||||
|
# Used for the Web3 plugin
|
||||||
|
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/web3/README.md
|
||||||
|
ALCHEMY_API_KEY=""
|
||||||
|
INFURA_API_KEY=""
|
||||||
# *********************************************************************************************************
|
# *********************************************************************************************************
|
||||||
+11
-4
@@ -21,7 +21,7 @@ CALCOM_LICENSE_KEY=
|
|||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
# ⚠️ ⚠️ ⚠️ DATABASE_URL got moved to `packages/prisma/.env.example` ⚠️ ⚠️ ⚠️
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - SHARED **************************************************************************************************
|
# - SHARED **************************************************************************************************
|
||||||
@@ -46,13 +46,12 @@ PGSSLMODE=
|
|||||||
# @see: https://github.com/calendso/calendso/issues/263
|
# @see: https://github.com/calendso/calendso/issues/263
|
||||||
# @see: https://next-auth.js.org/configuration/options#nextauth_url
|
# @see: https://next-auth.js.org/configuration/options#nextauth_url
|
||||||
# Required for Vercel hosting - set NEXTAUTH_URL to equal your NEXT_PUBLIC_WEBAPP_URL
|
# Required for Vercel hosting - set NEXTAUTH_URL to equal your NEXT_PUBLIC_WEBAPP_URL
|
||||||
# NEXTAUTH_URL='http://localhost:3000'
|
NEXTAUTH_URL='http://localhost:3000'
|
||||||
NEXTAUTH_URL=
|
|
||||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||||
# You can use: `openssl rand -base64 32` to generate one
|
# You can use: `openssl rand -base64 32` to generate one
|
||||||
NEXTAUTH_SECRET=
|
NEXTAUTH_SECRET=
|
||||||
# Used for cross-domain cookie authentication
|
# Used for cross-domain cookie authentication
|
||||||
NEXTAUTH_COOKIE_DOMAIN=.example.com
|
NEXTAUTH_COOKIE_DOMAIN=
|
||||||
|
|
||||||
# Set this to '1' if you don't want Cal to collect anonymous usage
|
# Set this to '1' if you don't want Cal to collect anonymous usage
|
||||||
CALCOM_TELEMETRY_DISABLED=
|
CALCOM_TELEMETRY_DISABLED=
|
||||||
@@ -95,6 +94,8 @@ NEXT_PUBLIC_IS_E2E=
|
|||||||
# Used for internal billing system
|
# Used for internal billing system
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE=
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
STRIPE_PRO_PLAN_PRODUCT_ID=
|
STRIPE_PRO_PLAN_PRODUCT_ID=
|
||||||
@@ -130,3 +131,9 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
|
|||||||
|
|
||||||
# Set the following value to true if you wish to enable Team Impersonation
|
# Set the following value to true if you wish to enable Team Impersonation
|
||||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||||
|
|
||||||
|
# Close.com internal CRM
|
||||||
|
CLOSECOM_API_KEY=
|
||||||
|
|
||||||
|
# Sendgrid internal email sender
|
||||||
|
SENDGRID_API_KEY=
|
||||||
@@ -15,6 +15,7 @@ Fixes # (issue)
|
|||||||
<!-- Please delete options that are not relevant. -->
|
<!-- Please delete options that are not relevant. -->
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- [ ] This change requires a documentation update
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Add PRs to project
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-PR-to-project:
|
||||||
|
name: Add PR to project
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/add-to-project@v0.1.0
|
||||||
|
with:
|
||||||
|
project-url: https://github.com/orgs/calcom/projects/1
|
||||||
|
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
@@ -12,41 +12,20 @@ on:
|
|||||||
- apps/storybook/**
|
- apps/storybook/**
|
||||||
- packages/ui/**
|
- packages/ui/**
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
# List of jobs
|
# List of jobs
|
||||||
jobs:
|
jobs:
|
||||||
check-if-ui-has-changed:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Declare outputs for next jobs
|
|
||||||
outputs:
|
|
||||||
docs_changed: ${{ steps.check_file_changed.outputs.docs_changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
# Checkout as many commits as needed for the diff
|
|
||||||
fetch-depth: 2
|
|
||||||
- shell: pwsh
|
|
||||||
id: check_file_changed
|
|
||||||
run: |
|
|
||||||
# Diff HEAD with the previous commit
|
|
||||||
$diff = git diff --name-only HEAD^ HEAD
|
|
||||||
|
|
||||||
# Check if a file under /packages/ui or apps/storybook has been modified since the previous commit
|
|
||||||
$SourceDiff = $diff | Where-Object { $_ -match '^packages/ui/' -or $_ -match '^apps/storybook/' }
|
|
||||||
$HasDiff = $SourceDiff.Length -gt 0
|
|
||||||
|
|
||||||
# Set the output named "hasUiChanges"
|
|
||||||
Write-Host "::set-output name=hasUiChanges::$HasDiff"
|
|
||||||
chromatic-deployment:
|
chromatic-deployment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ check-if-ui-has-changed ]
|
|
||||||
if: needs.checkIfUiHasChanged.outputs.hasUiChanges== 'True'
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
fetch-depth: 2
|
fetch-depth: 0
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
# 👇 Adds Chromatic as a step in the workflow
|
# 👇 Adds Chromatic as a step in the workflow
|
||||||
@@ -56,3 +35,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
|
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
workingDir: "apps/storybook"
|
||||||
|
buildScriptName: "build"
|
||||||
@@ -5,6 +5,11 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "**.tsx?"
|
- "**.tsx?"
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
jobs:
|
jobs:
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -17,8 +22,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Use Node 16.x
|
- name: Use Node 16.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Show info
|
||||||
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn type-check
|
- run: yarn type-check
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Cleanup Preview Databases
|
||||||
|
|
||||||
|
env:
|
||||||
|
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
|
||||||
|
SNAPLET_PROJECT_ID: cl4u26bwz7962859ply7ibuo43t
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: snaplet/action@v1
|
||||||
|
if: ${{ env.SNAPLET_ACCESS_TOKEN && env.SNAPLET_PROJECT_ID }}
|
||||||
|
with:
|
||||||
|
delete: true
|
||||||
@@ -4,22 +4,24 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Checkout
|
- name: crowdin action
|
||||||
uses: actions/checkout@v2
|
uses: crowdin/github-action@1.4.9
|
||||||
|
with:
|
||||||
- name: crowdin action
|
upload_translations: true
|
||||||
uses: crowdin/github-action@1.4.9
|
download_translations: true
|
||||||
with:
|
env:
|
||||||
upload_translations: true
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
download_translations: true
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
env:
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
name: E2E App-Store Apps
|
name: E2E App-Store Apps
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ feature/event-routing ]
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
pull_request_target: # So we can test on forks
|
pull_request_target: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -12,6 +12,9 @@ on:
|
|||||||
- apps/swagger/**
|
- apps/swagger/**
|
||||||
- apps/website/**
|
- apps/website/**
|
||||||
- apps/web/public/**
|
- apps/web/public/**
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -86,14 +89,14 @@ jobs:
|
|||||||
restore-keys: cache-playwright-
|
restore-keys: cache-playwright-
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Install playwright deps
|
- name: Install playwright deps
|
||||||
# if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn app-e2e-quick
|
run: yarn test-e2e:app-store
|
||||||
|
|
||||||
- name: Upload Test Results
|
- name: Upload Test Results
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: test-results-core
|
name: test-results
|
||||||
path: packages/app-store/**/playwright/results
|
path: test-results
|
||||||
@@ -7,6 +7,9 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- apps/web/**
|
- apps/web/**
|
||||||
- packages/embeds/**
|
- packages/embeds/**
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
e2e-embed:
|
e2e-embed:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -25,6 +28,8 @@ jobs:
|
|||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
@@ -79,7 +84,7 @@ jobs:
|
|||||||
restore-keys: cache-playwright-
|
restore-keys: cache-playwright-
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Install playwright deps
|
- name: Install playwright deps
|
||||||
# if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
name: E2E test
|
name: E2E test
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
pull_request_target: # So we can test on forks
|
pull_request_target: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- apps/web/**
|
- apps/web/**
|
||||||
- packages/**
|
- packages/**
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@@ -24,6 +29,8 @@ jobs:
|
|||||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
||||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
name: "Pull Request Labeler"
|
name: "Pull Request Labeler"
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
- pull_request_target
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "**.tsx?"
|
- "**.tsx?"
|
||||||
- "**.jsx?"
|
- "**.jsx?"
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
name: "Meta Workflow: Require Conditional Status Checks"
|
name: "Meta Workflow: Require Conditional Status Checks"
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
required-checks:
|
required-checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -34,3 +39,7 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- /apps/web/**
|
- /apps/web/**
|
||||||
- /packages/embeds/**
|
- /packages/embeds/**
|
||||||
|
- job: test
|
||||||
|
paths:
|
||||||
|
- /apps/web/**
|
||||||
|
- /packages/**
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: Unit tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
|
pull_request_target: # So we can test on forks
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- apps/web/**
|
||||||
|
- packages/**
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 20
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
|
fetch-depth: 2
|
||||||
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
|
- name: Use Node 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
cache: "yarn"
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
- run: yarn test
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# .env file
|
# .env file
|
||||||
.env
|
.env
|
||||||
|
!packages/prisma/.env
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
|||||||
|
|
||||||
Portions of this software are licensed as follows:
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
* All content that resides under "apps/web/ee/" (https://github.com/calcom/cal.com/tree/main/apps/web/ee) directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
|
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
|
||||||
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
||||||
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||||
|
<a href="https://status.cal.com"><img src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||||
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
|
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||||
@@ -36,6 +37,8 @@
|
|||||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
||||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||||
|
<a href="https://github.com/orgs/calcom/projects/1/views/25"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||||
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -55,7 +58,7 @@ Calendly and other scheduling tools are awesome. It made our lives massively eas
|
|||||||
|
|
||||||
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
|
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
|
||||||
|
|
||||||
## Product of the Month: April
|
## Product of the Month: April 2021
|
||||||
|
|
||||||
#### Support us on [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
#### Support us on [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
||||||
|
|
||||||
@@ -84,7 +87,7 @@ To get a local copy up and running, please follow these simple steps.
|
|||||||
|
|
||||||
Here is what you need to be able to run Cal.
|
Here is what you need to be able to run Cal.
|
||||||
|
|
||||||
- Node.js (Version: >=14.x <17)
|
- Node.js (Version: >=15.x <17)
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
- Yarn _(recommended)_
|
- Yarn _(recommended)_
|
||||||
|
|
||||||
@@ -112,7 +115,10 @@ Here is what you need to be able to run Cal.
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
|
1. Set up your .env file
|
||||||
|
- Duplicate `.env.example` to `.env`
|
||||||
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
|
||||||
|
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the .env file.
|
||||||
|
|
||||||
#### Quick start with `yarn dx`
|
#### Quick start with `yarn dx`
|
||||||
|
|
||||||
@@ -133,7 +139,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
#### Manual setup
|
#### Manual setup
|
||||||
|
|
||||||
1. Configure environment variables in the `packages/prisma/.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||||
|
|
||||||
```
|
```
|
||||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||||
@@ -201,7 +207,7 @@ Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If
|
|||||||
yarn test-e2e
|
yarn test-e2e
|
||||||
|
|
||||||
# To open last HTML report run:
|
# To open last HTML report run:
|
||||||
yarn workspace @calcom/web playwright-report
|
yarn playwright show-report test-results/reports/playwright-html-report
|
||||||
```
|
```
|
||||||
|
|
||||||
### Upgrading from earlier versions
|
### Upgrading from earlier versions
|
||||||
@@ -260,12 +266,16 @@ yarn workspace @calcom/web playwright-report
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
The Docker configuration for Cal is an effort powered by people within the community. Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
|
The Docker configuration for Cal is an effort powered by people within the community.
|
||||||
|
|
||||||
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
||||||
|
|
||||||
The Docker configuration can be found [in our docker repository](https://github.com/calcom/docker).
|
The Docker configuration can be found [in our docker repository](https://github.com/calcom/docker).
|
||||||
|
|
||||||
|
Issues with Docker? Find your answer or open a new discussion [here](https://github.com/calcom/docker/discussions) to ask the community.
|
||||||
|
|
||||||
|
Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
|
||||||
|
|
||||||
### Heroku
|
### Heroku
|
||||||
|
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
||||||
@@ -274,13 +284,15 @@ The Docker configuration can be found [in our docker repository](https://github.
|
|||||||
|
|
||||||
### Railway
|
### Railway
|
||||||
|
|
||||||
[](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fcalendso%2Fcalendso&plugins=postgresql&envs=GOOGLE_API_CREDENTIALS%2CBASE_URL%2CNEXTAUTH_URL%2CPORT&BASE_URLDefault=http%3A%2F%2Flocalhost%3A3000&NEXTAUTH_URLDefault=http%3A%2F%2Flocalhost%3A3000&PORTDefault=3000)
|
[](https://railway.app/new/template/cal)
|
||||||
|
|
||||||
You can deploy Cal on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal on their platform.
|
You can deploy Cal on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal on their platform.
|
||||||
|
|
||||||
### Vercel
|
### Vercel
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_SECRET,NEXTAUTH_URL,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT,CALCOM_LICENSE_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com)
|
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
|
|
||||||
@@ -298,7 +310,7 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
### Good First Issues
|
### Good First Issues
|
||||||
|
|
||||||
We have a list of [good first issues](https://github.com/calcom/cal.com/labels/✅%20good%20first%20issue) that contain bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/25) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
@@ -316,6 +328,23 @@ We have a list of [good first issues](https://github.com/calcom/cal.com/labels/
|
|||||||
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
||||||
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
||||||
|
|
||||||
|
#### _Adding google calendar to Cal.com App Store_
|
||||||
|
|
||||||
|
After adding Google credentials, you can now Google Calendar App to the app store.
|
||||||
|
You can repopulate the App store by running
|
||||||
|
|
||||||
|
```
|
||||||
|
cd packages/prisma
|
||||||
|
yarn seed-app-store
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to complete a few more steps to activate Google Calendar App.
|
||||||
|
Make sure to complete section "Obtaining the Google API Credentials". After the do the
|
||||||
|
following
|
||||||
|
|
||||||
|
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
||||||
|
1. Under 'OAuth concent screen', click "PUBLISH APP"
|
||||||
|
|
||||||
### Obtaining Microsoft Graph Client ID and Secret
|
### Obtaining Microsoft Graph Client ID and Secret
|
||||||
|
|
||||||
1. Open [Azure App Registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and select New registration
|
1. Open [Azure App Registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and select New registration
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* This is a Playwright script Checkly generated for you based on your Vercel project.
|
||||||
|
* To learn more about Browser checks and Playwright visit: https://www.checklyhq.com/docs/browser-checks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Create a Chromium browser
|
||||||
|
const { chromium } = require("playwright");
|
||||||
|
|
||||||
|
// Checkly supports top level await, but we wrap your code in an async function so you can run it locally too.
|
||||||
|
async function run() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// If available, we set the target URL to a preview deployment URL provided by the ENVIRONMENT_URL created by Vercel.
|
||||||
|
// Otherwise, we use the Production URL.
|
||||||
|
const targetUrl = process.env.ENVIRONMENT_URL || "https://app.cal.com";
|
||||||
|
|
||||||
|
// We visit the page. This waits for the "load" event by default.
|
||||||
|
const response = await page.goto(targetUrl);
|
||||||
|
|
||||||
|
// If the page doesn't return a successful response code, we fail the check
|
||||||
|
if (response.status() > 399) {
|
||||||
|
throw new Error(`Failed with response code ${response.status()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We snap a screenshot.
|
||||||
|
await page.screenshot({ path: "screenshot.jpg" });
|
||||||
|
|
||||||
|
// We close the page and browser. This is needed for collecting accurate web vitals.
|
||||||
|
await page.close();
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -10,18 +10,73 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"BASE_URL": {
|
"PGSSLMODE": {
|
||||||
|
"description": "If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then put 'no-verify' here.",
|
||||||
|
"value": "no-verify"
|
||||||
|
},
|
||||||
|
"NEXT_PUBLIC_WEBAPP_URL": {
|
||||||
"description": "Replace HEROKU_APP_NAME with the name given to your app",
|
"description": "Replace HEROKU_APP_NAME with the name given to your app",
|
||||||
"value": "https://HEROKU_APP_NAME.herokuapp.com"
|
"value": "https://HEROKU_APP_NAME.herokuapp.com",
|
||||||
|
"required": "true"
|
||||||
},
|
},
|
||||||
"CALENDSO_ENCRYPTION_KEY": {
|
"CALENDSO_ENCRYPTION_KEY": {
|
||||||
"description": "Application Key for symmetric encryption and decryption. Must be 32 bytes for AES256 encryption algorithm.",
|
"description": "Application Key for symmetric encryption and decryption. Must be 32 bytes for AES256 encryption algorithm.",
|
||||||
"value": "secret"
|
"value": "secret",
|
||||||
|
"required": "true"
|
||||||
},
|
},
|
||||||
"NEXTAUTH_SECRET": "secret",
|
|
||||||
"NEXTAUTH_URL": {
|
"NEXTAUTH_URL": {
|
||||||
"description": "Replace HEROKU_APP_NAME with the name given to your app",
|
"description": "Replace HEROKU_APP_NAME with the name given to your app",
|
||||||
"value": "https://HEROKU_APP_NAME.herokuapp.com"
|
"value": "https://HEROKU_APP_NAME.herokuapp.com"
|
||||||
|
},
|
||||||
|
"NEXTAUTH_SECRET": {
|
||||||
|
"description": "Cookie encryption key",
|
||||||
|
"generator": "secret",
|
||||||
|
"required": "true"
|
||||||
|
},
|
||||||
|
"NEXT_PUBLIC_LICENSE_CONSENT": {
|
||||||
|
"description": "License consent",
|
||||||
|
"value": "true",
|
||||||
|
"required": "true"
|
||||||
|
},
|
||||||
|
"CRON_API_KEY": {
|
||||||
|
"description": "ApiKey for cronjobs",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"SEND_FEEDBACK_EMAIL": {
|
||||||
|
"description": "Send feedback email",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"SENDGRID_API_KEY": {
|
||||||
|
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"SENDGRID_EMAIL": {
|
||||||
|
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"EMAIL_FROM": {
|
||||||
|
"description": "Configures the global From: header whilst sending emails",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"EMAIL_SERVER_HOST": {
|
||||||
|
"description": "Configures the global SMTP server host",
|
||||||
|
"value": "smtp.gmail.com"
|
||||||
|
},
|
||||||
|
"EMAIL_SERVER_PORT": {
|
||||||
|
"description": "Configures the global SMTP server port",
|
||||||
|
"value": "465"
|
||||||
|
},
|
||||||
|
"EMAIL_SERVER_USER": {
|
||||||
|
"description": "Configures the global SMTP server user",
|
||||||
|
"value": "<gmail_emailAddress>"
|
||||||
|
},
|
||||||
|
"EMAIL_SERVER_PASSWORD": {
|
||||||
|
"description": "Configures the global SMTP server password",
|
||||||
|
"value": "<gmail_app_password>"
|
||||||
|
},
|
||||||
|
"NEXT_PUBLIC_TEAM_IMPERSONATION": {
|
||||||
|
"description": "Set the following value to true if you wish to enable Team Impersonation",
|
||||||
|
"value": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
Submodule apps/api updated: aba7b1ec1c...a36b6fa923.
+1
-1
Submodule apps/console updated: ac2567263d...995771ebbd.
@@ -7,7 +7,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||||
"dev": "PORT=4000 next",
|
"dev": "PORT=4000 next",
|
||||||
"lint": "next lint",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||||
"start": "PORT=4000 next start",
|
"start": "PORT=4000 next start",
|
||||||
@@ -17,14 +17,14 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iframe-resizer-react": "^1.1.0",
|
"iframe-resizer-react": "^1.1.0",
|
||||||
"next": "^12.2.0",
|
"next": "^12.2.5",
|
||||||
"nextra": "^1.1.0",
|
"nextra": "^1.1.0",
|
||||||
"nextra-theme-docs": "^1.2.2",
|
"nextra-theme-docs": "^1.2.2",
|
||||||
"react": "^18.1.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.1.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "*",
|
||||||
"eslint": "^8.15.0"
|
"eslint": "^8.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,10 @@ We use a number of style guides written by other amazing companies, simply becau
|
|||||||
We don't expect you to study every single rule of each style guide, but these are great reference points as to how your code should be styled. We may reject a pull request if your code style significantly differs from these style guides however.
|
We don't expect you to study every single rule of each style guide, but these are great reference points as to how your code should be styled. We may reject a pull request if your code style significantly differs from these style guides however.
|
||||||
|
|
||||||
## ESLint & Prettier
|
## ESLint & Prettier
|
||||||
Calendso uses the ESLint and Prettier formatting tools, and the repository comes with defined rules for each tool. We recommend setting up both tools and using these to help automatically style your code to our guidelines.
|
Cal.com uses the ESLint and Prettier formatting tools, and the repository comes with defined rules for each tool. We recommend setting up both tools and using these to help automatically style your code to our guidelines.
|
||||||
|
|
||||||
## JavaScript/TypeScript
|
## JavaScript/TypeScript
|
||||||
We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for all JavaScript and Typescript code.
|
We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for all JavaScript and TypeScript code.
|
||||||
|
|
||||||
## HTML & CSS
|
## HTML & CSS
|
||||||
We use the [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) for any HTML and CSS markup. However, exceptions to the HTML guide apply where JSX differentiates from standard HTML.
|
We use the [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) for any HTML and CSS markup. However, exceptions to the HTML guide apply where JSX differentiates from standard HTML.
|
||||||
@@ -34,7 +34,6 @@ Here is what you need to be able to run Cal.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp apps/web/.env.example apps/web/.env
|
cp apps/web/.env.example apps/web/.env
|
||||||
cp packages/prisma/.env.example packages/prisma/.env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Install packages with yarn
|
1. Install packages with yarn
|
||||||
@@ -54,7 +53,7 @@ yarn dx
|
|||||||
|
|
||||||
### Manual setup
|
### Manual setup
|
||||||
|
|
||||||
1. Configure database in the `packages/prisma/.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
1. Configure database in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||||
|
|
||||||
```text
|
```text
|
||||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ title: Vercel
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You need a PostgresDB database hosted somewhere. [Heroku](https://www.heroku.com) and [Supabase](https://supabase.com/) offer great free options.
|
You need a PostgresDB database hosted somewhere. [Supabase](https://supabase.com/) offers a great free option.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ module.exports = {
|
|||||||
* @see https://github.com/storybookjs/storybook/issues/12844#issuecomment-867544160
|
* @see https://github.com/storybookjs/storybook/issues/12844#issuecomment-867544160
|
||||||
*/
|
*/
|
||||||
config.resolve.roots = [path.resolve(__dirname, "../public"), "node_modules"];
|
config.resolve.roots = [path.resolve(__dirname, "../public"), "node_modules"];
|
||||||
|
config.resolve.alias = {
|
||||||
|
...config.resolve.alias,
|
||||||
|
"@/interfaces": path.resolve(__dirname, "../interfaces"),
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Why webpack5... Just why?
|
* Why webpack5... Just why?
|
||||||
|
|||||||
@@ -22,3 +22,13 @@ export const parameters = {
|
|||||||
Provider: RouterContext.Provider,
|
Provider: RouterContext.Provider,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.getEmbedNamespace = () => {
|
||||||
|
const url = new URL(document.URL);
|
||||||
|
const namespace = url.searchParams.get("embed");
|
||||||
|
return namespace;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getEmbedTheme = () => {
|
||||||
|
return "auto";
|
||||||
|
};
|
||||||
Vendored
-5
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
||||||
+25
-23
@@ -6,7 +6,7 @@
|
|||||||
"dev:next": "next dev",
|
"dev:next": "next dev",
|
||||||
"build:next": "next build",
|
"build:next": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/storybook.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/storybook.json",
|
||||||
"dev": "start-storybook -p 6006",
|
"dev": "start-storybook -p 6006",
|
||||||
"build": "build-storybook",
|
"build": "build-storybook",
|
||||||
@@ -23,40 +23,42 @@
|
|||||||
"@radix-ui/react-radio-group": "^0.1.1",
|
"@radix-ui/react-radio-group": "^0.1.1",
|
||||||
"@radix-ui/react-slider": "^0.1.1",
|
"@radix-ui/react-slider": "^0.1.1",
|
||||||
"@radix-ui/react-switch": "^0.1.1",
|
"@radix-ui/react-switch": "^0.1.1",
|
||||||
"@radix-ui/react-tooltip": "^0.1.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"next": "^12.2.0",
|
"next": "^12.2.5",
|
||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^9.0.0",
|
||||||
"react": "18.1.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "18.1.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-feather": "^2.0.9",
|
"react-feather": "^2.0.10",
|
||||||
"react-hot-toast": "^2.2.0"
|
"react-hot-toast": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.6",
|
"@babel/core": "^7.18.10",
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "*",
|
||||||
"@storybook/addon-a11y": "^6.5.9",
|
"@storybook/addon-a11y": "^6.5.10",
|
||||||
"@storybook/addon-actions": "^6.5.9",
|
"@storybook/addon-actions": "^6.5.10",
|
||||||
"@storybook/addon-essentials": "^6.5.9",
|
"@storybook/addon-essentials": "^6.5.10",
|
||||||
"@storybook/addon-interactions": "^6.5.9",
|
"@storybook/addon-interactions": "^6.5.10",
|
||||||
"@storybook/addon-links": "^6.5.9",
|
"@storybook/addon-links": "^6.5.10",
|
||||||
"@storybook/addon-postcss": "^2.0.0",
|
"@storybook/addon-postcss": "^2.0.0",
|
||||||
"@storybook/builder-webpack5": "^6.5.9",
|
"@storybook/builder-vite": "^0.2.2",
|
||||||
"@storybook/manager-webpack5": "^6.5.9",
|
"@storybook/builder-webpack5": "^6.5.10",
|
||||||
"@storybook/react": "^6.5.9",
|
"@storybook/manager-webpack5": "^6.5.10",
|
||||||
|
"@storybook/react": "^6.5.10",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.0",
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "18.0.4",
|
"@types/react-dom": "18.0.4",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.8",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
"chromatic": "^6.6.4",
|
"chromatic": "^6.7.4",
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.22.0",
|
||||||
"postcss": "^8.4.13",
|
"postcss": "^8.4.16",
|
||||||
"postcss-loader": "^7.0.0",
|
"postcss-loader": "^7.0.0",
|
||||||
"storybook-addon-designs": "^6.3.1",
|
"storybook-addon-designs": "^6.3.1",
|
||||||
"storybook-addon-next": "^1.6.7",
|
"storybook-addon-next": "^1.6.7",
|
||||||
"tailwindcss": "^3.1.3",
|
"tailwindcss": "^3.1.8",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||||
"typescript": "^4.6.4"
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
"readme": "ERROR: No README data found!",
|
"readme": "ERROR: No README data found!",
|
||||||
"_id": "@calcom/storybook@0.1.0"
|
"_id": "@calcom/storybook@0.1.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
import Avatar from "@calcom/ui/v2/Avatar";
|
import Avatar from "@calcom/ui/v2/core/Avatar";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Avatar",
|
title: "Avatar",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
import Avatar from "@calcom/ui/v2/Avatar";
|
import AvatarGroup from "@calcom/ui/v2/core/AvatarGroup";
|
||||||
import AvatarGroup from "@calcom/ui/v2/AvatarGroup";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Avatar/Group",
|
title: "Avatar/Group",
|
||||||
@@ -53,8 +53,8 @@ const IMAGES = [
|
|||||||
|
|
||||||
export const Default = () => {
|
export const Default = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<TooltipProvider>
|
||||||
<AvatarGroup size="lg" items={IMAGES} />
|
<AvatarGroup size="lg" items={IMAGES} />
|
||||||
</>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { Bell } from "react-feather";
|
import { Bell } from "react-feather";
|
||||||
|
|
||||||
import { Badge } from "@calcom/ui/v2";
|
import Badge from "@calcom/ui/v2/core/Badge";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Badge",
|
title: "Badge",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||||
import { Info } from "react-feather";
|
import { Info } from "react-feather";
|
||||||
|
|
||||||
import { Banner } from "@calcom/ui/v2";
|
import Banner from "@calcom/ui/v2/core/banner";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Banner",
|
title: "Banner",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
import { Breadcrumb, BreadcrumbItem } from "@calcom/ui/v2";
|
import Breadcrumb, { BreadcrumbItem } from "@calcom/ui/v2/core/Breadcrumb";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Breadcrumbs",
|
title: "Breadcrumbs",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||||
import { Trash2 } from "react-feather";
|
import { Trash2 } from "react-feather";
|
||||||
|
|
||||||
import { Button as ButtonComponent } from "@calcom/ui/v2";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Button",
|
title: "Button",
|
||||||
component: ButtonComponent,
|
component: Button,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
color: {
|
color: {
|
||||||
options: ["primary", "secondary", "minimal", "destructive"],
|
options: ["primary", "secondary", "minimal", "destructive"],
|
||||||
@@ -24,67 +25,69 @@ export default {
|
|||||||
control: { type: "radio" },
|
control: { type: "radio" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as ComponentMeta<typeof ButtonComponent>;
|
} as ComponentMeta<typeof Button>;
|
||||||
|
|
||||||
const Template: ComponentStory<typeof ButtonComponent> = (args) => <ButtonComponent {...args} />;
|
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
|
||||||
|
|
||||||
export const All = () => (
|
export const All = () => (
|
||||||
<div>
|
<div>
|
||||||
<h1>Primary</h1>
|
<TooltipProvider>
|
||||||
<div className="flex space-x-2">
|
<h1>Primary</h1>
|
||||||
<ButtonComponent aria-label="Button Text">Button Text</ButtonComponent>
|
<div className="flex space-x-2">
|
||||||
<ButtonComponent disabled aria-label="Button Text">
|
<Button aria-label="Button Text">Button Text</Button>
|
||||||
Button Text
|
<Button disabled aria-label="Button Text">
|
||||||
</ButtonComponent>
|
Button Text
|
||||||
</div>
|
</Button>
|
||||||
<h1>Secondary</h1>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<h1>Secondary</h1>
|
||||||
<ButtonComponent color="secondary" aria-label="Button Text">
|
<div className="flex space-x-2">
|
||||||
Button Text
|
<Button color="secondary" aria-label="Button Text">
|
||||||
</ButtonComponent>
|
Button Text
|
||||||
<ButtonComponent disabled color="secondary" aria-label="Button Text">
|
</Button>
|
||||||
Button Text
|
<Button disabled color="secondary" aria-label="Button Text">
|
||||||
</ButtonComponent>
|
Button Text
|
||||||
<ButtonComponent size="icon" color="secondary" StartIcon={Trash2} aria-label="Button Text" />
|
</Button>
|
||||||
</div>
|
<Button size="icon" color="secondary" StartIcon={Trash2} aria-label="Button Text" />
|
||||||
<h1>Minimal</h1>
|
</div>
|
||||||
<div className="flex">
|
<h1>Minimal</h1>
|
||||||
<ButtonComponent color="minimal" aria-label="Button Text">
|
<div className="flex">
|
||||||
Button Text
|
<Button color="minimal" aria-label="Button Text">
|
||||||
</ButtonComponent>
|
Button Text
|
||||||
<ButtonComponent disabled color="minimal" aria-label="Button Text">
|
</Button>
|
||||||
Button Text
|
<Button disabled color="minimal" aria-label="Button Text">
|
||||||
</ButtonComponent>
|
Button Text
|
||||||
<ButtonComponent size="icon" color="minimal" StartIcon={Trash2} aria-label="Button Text" />
|
</Button>
|
||||||
</div>
|
<Button size="icon" color="minimal" StartIcon={Trash2} aria-label="Button Text" />
|
||||||
<h1>Destructive</h1>
|
</div>
|
||||||
<ButtonComponent size="icon" color="destructive" StartIcon={Trash2} aria-label="Button Text" />
|
<h1>Destructive</h1>
|
||||||
<h1>Tooltip</h1>
|
<Button size="icon" color="destructive" StartIcon={Trash2} aria-label="Button Text" />
|
||||||
<ButtonComponent
|
<h1>Tooltip</h1>
|
||||||
tooltip="Deletes EventTypes"
|
<Button
|
||||||
size="icon"
|
tooltip="Deletes EventTypes"
|
||||||
color="destructive"
|
size="icon"
|
||||||
StartIcon={Trash2}
|
color="destructive"
|
||||||
aria-label="Button Text"
|
StartIcon={Trash2}
|
||||||
/>
|
aria-label="Button Text"
|
||||||
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Button = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
Button.args = {
|
Default.args = {
|
||||||
color: "primary",
|
color: "primary",
|
||||||
children: "Button Text",
|
children: "Button Text",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Disabled = Template.bind({});
|
export const Disabled = Template.bind({});
|
||||||
Disabled.args = {
|
Disabled.args = {
|
||||||
...Button.args,
|
...Default.args,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Loading = Template.bind({});
|
export const Loading = Template.bind({});
|
||||||
Loading.args = {
|
Loading.args = {
|
||||||
...Button.args,
|
...Default.args,
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
export const Icon = Template.bind({});
|
export const Icon = Template.bind({});
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { ArrowLeft, ArrowRight, Clipboard, Navigation2, Trash2 } from "react-feather";
|
import { ArrowLeft, ArrowRight, Clipboard, Navigation2, Trash2 } from "react-feather";
|
||||||
|
|
||||||
import { Button, ButtonGroup } from "@calcom/ui/v2";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
import ButtonGroup from "@calcom/ui/v2/core/ButtonGroup";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Button Group",
|
title: "Button Group",
|
||||||
component: ButtonGroup,
|
component: ButtonGroup,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Story />
|
||||||
|
</TooltipProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
} as ComponentMeta<typeof ButtonGroup>;
|
} as ComponentMeta<typeof ButtonGroup>;
|
||||||
|
|
||||||
export const Default = () => (
|
export const Default = () => (
|
||||||
@@ -19,13 +28,14 @@ export const Default = () => (
|
|||||||
export const Combined = () => (
|
export const Combined = () => (
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<ButtonGroup combined>
|
<ButtonGroup combined>
|
||||||
<Button StartIcon={Trash2} size="icon" color="secondary" combined />
|
<Button StartIcon={Trash2} size="icon" color="secondary" />
|
||||||
<Button StartIcon={Navigation2} size="icon" color="secondary" combined />
|
<Button StartIcon={Navigation2} size="icon" color="secondary" />
|
||||||
<Button StartIcon={Clipboard} size="icon" color="secondary" combined />
|
<Button StartIcon={Clipboard} size="icon" color="secondary" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup combined>
|
<ButtonGroup combined>
|
||||||
<Button StartIcon={ArrowLeft} size="icon" color="secondary" combined />
|
<Button StartIcon={ArrowLeft} size="icon" color="secondary" />
|
||||||
<Button StartIcon={ArrowRight} size="icon" color="secondary" combined />
|
<Button StartIcon={ArrowRight} size="icon" color="secondary" />
|
||||||
|
<Button StartIcon={ArrowRight} href="/" size="icon" color="secondary" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import Card, { BaseCardProps } from "@calcom/ui/v2/core/Card";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Cards",
|
||||||
|
component: Card,
|
||||||
|
} as ComponentMeta<typeof Card>;
|
||||||
|
|
||||||
|
const profileProps: BaseCardProps[] = [
|
||||||
|
{
|
||||||
|
variant: "ProfileCard",
|
||||||
|
title: "Alex Fisher",
|
||||||
|
image:
|
||||||
|
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
|
||||||
|
description: "Co-founder at Attio. Book a meeting to discuss enterprise deals.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant: "ProfileCard",
|
||||||
|
title: "Julian Wan",
|
||||||
|
image:
|
||||||
|
"https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80",
|
||||||
|
description: "Co-founder at Attio. Book a meeting to discuss enterprise deals.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Profile = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full space-x-2 bg-gray-200 p-4">
|
||||||
|
<Card {...profileProps[0]} />
|
||||||
|
<Card {...profileProps[1]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appStoreProps: BaseCardProps = {
|
||||||
|
variant: "AppStore",
|
||||||
|
title: "Giphy",
|
||||||
|
image: "https://app.cal.com/api/app-store/giphy/icon.svg",
|
||||||
|
description: "Add GIFs to spice up your booking confirmations.",
|
||||||
|
actionButton: {
|
||||||
|
href: "#",
|
||||||
|
child: "View Details",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppStore = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-gray-200 p-4">
|
||||||
|
<Card {...appStoreProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sidebarCardProps: BaseCardProps = {
|
||||||
|
variant: "SidebarCard",
|
||||||
|
thumbnailUrl: "https://img.youtube.com/vi/60HJt8DOVNo/0.jpg",
|
||||||
|
mediaLink: "https://www.youtube.com/watch?v=60HJt8DOVNo",
|
||||||
|
title: "Dynamic boooking links",
|
||||||
|
description: "Booking link that allows people to quickly schedule meetings.",
|
||||||
|
learnMore: {
|
||||||
|
href: "https://cal.com/blog/cal-v-1-9",
|
||||||
|
text: "Learn more",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarCard = () => {
|
||||||
|
const [visible, setVisible] = useState(true); // save state in localStorage, cookie or db
|
||||||
|
return (
|
||||||
|
<header className="w-full max-w-[225px] bg-gray-100 p-3">
|
||||||
|
{visible && (
|
||||||
|
<div>
|
||||||
|
<Card {...sidebarCardProps} actionButton={{ onClick: () => setVisible(false), child: "Dismiss" }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
import { Checkbox } from "@calcom/ui/v2";
|
import Checkbox from "@calcom/ui/v2/core/form/Checkbox";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Checkbox",
|
title: "Checkbox",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import ColorPicker from "@calcom/ui/v2/colorpicker";
|
import ColorPicker from "@calcom/ui/v2/core/colorpicker";
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import DatePicker from "@calcom/ui/v2/booker/DatePicker";
|
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Datepicker",
|
title: "Datepicker",
|
||||||
@@ -8,7 +8,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Default = () => {
|
export const Default = () => {
|
||||||
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
const [selected, setSelected] = useState<Date>(new Date());
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "455px" }}>
|
<div style={{ width: "455px" }}>
|
||||||
<DatePicker selected={selected} onChange={setSelected} locale="en" />
|
<DatePicker selected={selected} onChange={setSelected} locale="en" />
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||||
|
import "@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
|
||||||
|
import "@calcom/ui/v2/core/form/date-range-picker/styles.css";
|
||||||
|
|
||||||
|
function getISOLocalDate(date) {
|
||||||
|
const year = padStart(date.getFullYear(), 4);
|
||||||
|
const month = padStart(date.getMonth() + 1);
|
||||||
|
const day = padStart(date.getDate());
|
||||||
|
|
||||||
|
return [`${year}-${month}-${day}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function padStart(num, val = 2) {
|
||||||
|
const numStr = `${num}`;
|
||||||
|
if (numStr.length >= val) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `0000${numStr}`.slice(-val);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Date Range Picker",
|
||||||
|
component: DateRangePicker,
|
||||||
|
} as ComponentMeta<typeof DateRangePicker>;
|
||||||
|
|
||||||
|
export const Default: ComponentStory<typeof DateRangePicker> = () => {
|
||||||
|
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||||
|
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DateRangePicker
|
||||||
|
startDate={getISOLocalDate(startDate) as unknown as Date}
|
||||||
|
endDate={getISOLocalDate(endDate) as unknown as Date}
|
||||||
|
onDatesChange={({ startDate, endDate }) => {
|
||||||
|
setStartDate(startDate);
|
||||||
|
setEndDate(endDate);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { Bell } from "react-feather";
|
import { Bell } from "react-feather";
|
||||||
|
|
||||||
import { EmptyScreen as EmptyScreenPattern } from "@calcom/ui/v2";
|
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "pattern/Empty Screen",
|
title: "pattern/Empty Screen",
|
||||||
component: EmptyScreenPattern,
|
component: EmptyScreen,
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
@@ -13,13 +13,13 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
} as ComponentMeta<typeof EmptyScreenPattern>;
|
} as ComponentMeta<typeof EmptyScreen>;
|
||||||
|
|
||||||
export const EmptyScreenS = () => (
|
export const Default = () => (
|
||||||
<EmptyScreenPattern
|
<EmptyScreen
|
||||||
Icon={Bell}
|
Icon={Bell}
|
||||||
headline="Empty State Header"
|
headline="Empty State Header"
|
||||||
description="Ullamco dolor nulla sint nulla occaecat aliquip id elit fugiat et excepteur magna. Nisi tempor anim do tempor irure fugiat ad occaecat. Mollit ea eiusmod pariatur sunt deserunt eu eiusmod. Sit reprehenderit cupidatat consequat commodo in aliqua ea et. Et quis sit enim proident dolor mollit consectetur tempor dolore reprehenderit consequat adipisicing reprehenderit officia. Sint eu sunt fugiat laborum Lorem irure aute nulla et. Do non in enim ipsum ea."
|
description="Ullamco dolor nulla sint nulla occaecat aliquip id elit fugiat et excepteur magna. Nisi tempor anim do tempor irure fugiat ad occaecat. Mollit ea eiusmod pariatur sunt deserunt eu eiusmod. Sit reprehenderit "
|
||||||
buttonText="Veniam ut ipsum"
|
buttonText="Veniam ut ipsum"
|
||||||
buttonOnClick={() => console.log("Button Clicked")}
|
buttonOnClick={() => console.log("Button Clicked")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { FormStep } from "@calcom/ui/v2";
|
import FormStep from "@calcom/ui/v2/core/form/FormStep";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Form Step",
|
title: "Form Step",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Disabling until we figure out what is happening with the RouterMock.
|
||||||
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
|
import { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem";
|
||||||
|
import HorizontalTabs from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Horizontal Tabs",
|
||||||
|
component: HorizontalTabs,
|
||||||
|
} as ComponentMeta<typeof HorizontalTabs>;
|
||||||
|
|
||||||
|
const HorizontalTabsPropsDefault: HorizontalTabItemProps[] = [
|
||||||
|
{
|
||||||
|
name: "Tab One",
|
||||||
|
href: "/tab-one",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tab Two",
|
||||||
|
href: "/tab-two",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tab Disabled",
|
||||||
|
href: "/tab-disabled",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Default = () => (
|
||||||
|
<div className="w-full p-4">
|
||||||
|
<HorizontalTabs tabs={HorizontalTabsPropsDefault} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Default.parameters = {
|
||||||
|
nextRouter: {
|
||||||
|
path: "/[page]",
|
||||||
|
asPath: "/tab-one",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||||
import { Copy } from "react-feather";
|
import { Copy } from "react-feather";
|
||||||
|
|
||||||
import DatePicker from "@calcom/ui/v2/form/DatePicker";
|
import { TextAreaField, TextField, PasswordField } from "@calcom/ui/v2/core/form/fields";
|
||||||
import { TextAreaField, TextField } from "@calcom/ui/v2/form/fields";
|
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Inputs",
|
title: "Inputs",
|
||||||
@@ -66,3 +67,9 @@ export const TextAreaInput: ComponentStory<typeof TextAreaField> = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const DatePickerInput: ComponentStory<typeof DatePicker> = () => <DatePicker date={new Date()} />;
|
export const DatePickerInput: ComponentStory<typeof DatePicker> = () => <DatePicker date={new Date()} />;
|
||||||
|
|
||||||
|
export const PasswordInput: ComponentStory<typeof PasswordField> = () => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<PasswordField />
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Info } from "react-feather";
|
import { Info } from "react-feather";
|
||||||
|
|
||||||
import { Button } from "@calcom/ui/v2";
|
import { Button, Dialog, DialogContent, DialogTrigger, TextField } from "@calcom/ui/v2";
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/v2/Dialog";
|
|
||||||
import { TextField } from "@calcom/ui/v2/form/fields";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "pattern/Modal",
|
title: "pattern/Modal",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import { Button } from "@calcom/ui/v2";
|
import { Button, showToast } from "@calcom/ui/v2";
|
||||||
import showToast from "@calcom/ui/v2/notfications";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Notifcations-Toasts",
|
title: "Notifcations-Toasts",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { Search } from "react-feather";
|
import { Search } from "react-feather";
|
||||||
|
|
||||||
import { Button, PageHeader } from "@calcom/ui/v2";
|
import { Button, TextField } from "@calcom/ui/v2";
|
||||||
import { TextField } from "@calcom/ui/v2/form";
|
import PageHeader from "@calcom/ui/v2/core/PageHeader";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "pattern/Page Header",
|
title: "pattern/Page Header",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as Radio from "@calcom/ui/v2/form/radio-area/Radio";
|
import * as Radio from "@calcom/ui/v2/core/form/radio-area/Radio";
|
||||||
import { RadioField } from "@calcom/ui/v2/form/radio-area/Radio";
|
import { RadioField } from "@calcom/ui/v2/core/form/radio-area/Radio";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Radio",
|
title: "Radio",
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
import { Select } from "@calcom/ui/v2";
|
import { Select } from "@calcom/ui/v2";
|
||||||
|
import MultiDropdownSelect from "@calcom/ui/v2/modules/event-types/MultiDropdownSelect";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Form/Select",
|
title: "Form/Select",
|
||||||
component: Select,
|
component: Select,
|
||||||
} as ComponentMeta<typeof Select>;
|
} as ComponentMeta<typeof Select>;
|
||||||
|
|
||||||
export const Single = () => <Select items={[{ value: "Test Test" }]} />;
|
const singleOptions = [
|
||||||
|
{
|
||||||
|
label: "Select",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||||
|
label: minutes + " " + "minutes",
|
||||||
|
value: minutes,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
const multiOptions = [
|
||||||
|
{
|
||||||
|
label: "Select",
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||||
|
label: minutes + " " + "minutes",
|
||||||
|
value: minutes,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Single = () => <Select options={singleOptions} />;
|
||||||
|
export const Multi = () => <MultiDropdownSelect options={multiOptions} />;
|
||||||
@@ -16,52 +16,52 @@ const TabItemProps = {
|
|||||||
info: "60 mins, Zoom",
|
info: "60 mins, Zoom",
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const Default = () => (
|
export const Default = () => (
|
||||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
<div className="h-20 w-full bg-gray-100 p-4">
|
||||||
// <VerticalTabItem {...TabItemProps}></VerticalTabItem>
|
<VerticalTabItem {...TabItemProps} />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
// export const Active = () => (
|
export const Active = () => (
|
||||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
<div className="h-20 w-full bg-gray-100 p-4">
|
||||||
// <VerticalTabItem {...TabItemProps}></VerticalTabItem>
|
<VerticalTabItem {...TabItemProps} />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
|
|
||||||
// Mocking next router to show active state
|
// Mocking next router to show active state
|
||||||
// Active.parameters = {
|
Active.parameters = {
|
||||||
// nextRouter: {
|
nextRouter: {
|
||||||
// path: "/settings/[tab]",
|
path: "/settings/[tab]",
|
||||||
// asPath: "/settings/event",
|
asPath: "/settings/event",
|
||||||
// },
|
},
|
||||||
// };
|
};
|
||||||
|
|
||||||
// export const Disabled = () => (
|
export const Disabled = () => (
|
||||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
<div className="h-20 w-full bg-gray-100 p-4">
|
||||||
// <VerticalTabItem {...TabItemProps} disabled></VerticalTabItem>
|
<VerticalTabItem {...TabItemProps} disabled />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
|
|
||||||
// export const NoIconNoInfo = () => (
|
export const NoIconNoInfo = () => (
|
||||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
<div className="h-20 w-full bg-gray-100 p-4">
|
||||||
// <VerticalTabItem name="Event Setup" href="/settings/event"></VerticalTabItem>
|
<VerticalTabItem name="Event Setup" href="/settings/event" />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
|
|
||||||
// export const IconNoInfo = () => (
|
export const IconNoInfo = () => (
|
||||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
<div className="h-20 w-full bg-gray-100 p-4">
|
||||||
// <VerticalTabItem name="Event Setup" href="/settings/event" icon={Link}></VerticalTabItem>
|
<VerticalTabItem name="Event Setup" href="/settings/event" icon={Link} />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
|
|
||||||
// export const IconNoInfoActive = () => (
|
export const IconNoInfoActive = () => (
|
||||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
<div className="h-20 w-full bg-gray-100 p-4">
|
||||||
// <VerticalTabItem name="Event Setup" href="/settings/events" icon={Link}></VerticalTabItem>
|
<VerticalTabItem name="Event Setup" href="/settings/events" icon={Link} />
|
||||||
// </div>
|
</div>
|
||||||
// );
|
);
|
||||||
|
|
||||||
// IconNoInfoActive.paramaters = {
|
IconNoInfoActive.paramaters = {
|
||||||
// nextRouter: {
|
nextRouter: {
|
||||||
// path: "/settings/[tab]",
|
path: "/settings/[tab]",
|
||||||
// asPath: "/settings/events",
|
asPath: "/settings/events",
|
||||||
// },
|
},
|
||||||
// };
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ComponentMeta } from "@storybook/react";
|
import { ComponentMeta } from "@storybook/react";
|
||||||
import { Calendar, Clock, Grid, Link, RefreshCw, User, Users } from "react-feather";
|
import { Calendar, Clock, Grid, Link, RefreshCw, User, Users } from "react-feather";
|
||||||
|
|
||||||
import { VerticalTabs } from "@calcom/ui/v2/navigation/tabs";
|
import VerticalTabs from "@calcom/ui/v2/core/navigation/tabs/VerticalTabs";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Vertical Tabs",
|
title: "Vertical Tabs",
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import "../../../packages/ui/v2/core/form/date-range-picker/styles.css";
|
||||||
|
@import "../../../node_modules/@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--brand-color: #292929;
|
--brand-color: #292929;
|
||||||
--brand-text-color: #ffffff;
|
--brand-text-color: #ffffff;
|
||||||
--brand-color-dark-mode: #fafafa;
|
--brand-color-dark-mode: #fafafa;
|
||||||
--brand-text-color-dark-mode: #292929;
|
--brand-text-color-dark-mode: #292929;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
@@ -6,24 +6,24 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||||
"dev": "PORT=4200 next dev",
|
"dev": "PORT=4200 next dev",
|
||||||
"lint": "next lint",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/swagger.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/swagger.json",
|
||||||
"start": "PORT=4200 next start"
|
"start": "PORT=4200 next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.5.1",
|
"highlight.js": "^11.6.0",
|
||||||
"isarray": "2.0.5",
|
"isarray": "2.0.5",
|
||||||
"next": "12.2.0",
|
"next": "^12.2.5",
|
||||||
"openapi-snippet": "^0.13.0",
|
"openapi-snippet": "^0.13.0",
|
||||||
"react": "18.1.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "18.1.0",
|
"react-dom": "^18.2.0",
|
||||||
"swagger-ui-react": "4.11.1"
|
"swagger-ui-react": "4.11.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "18.0.4",
|
"@types/react-dom": "18.0.4",
|
||||||
"typescript": "4.6.4"
|
"typescript": "^4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ playwright/reports/*
|
|||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
@@ -64,3 +65,6 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
# Autogenerated embed content
|
# Autogenerated embed content
|
||||||
public/embed
|
public/embed
|
||||||
|
|
||||||
|
# Copied app-store images
|
||||||
|
public/app-store
|
||||||
+1
-174
@@ -277,179 +277,6 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
integration:
|
integration:
|
||||||
type: string
|
type: string
|
||||||
/api/availability/day:
|
|
||||||
patch:
|
|
||||||
description: Updates the start and end times for a user's availability.
|
|
||||||
summary: Updates the user's start and end times
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
startMins:
|
|
||||||
type: string
|
|
||||||
endMins:
|
|
||||||
type: string
|
|
||||||
bufferMins:
|
|
||||||
type: string
|
|
||||||
description: ""
|
|
||||||
/api/availability/eventtype:
|
|
||||||
post:
|
|
||||||
description: Adds a new event type for the user.
|
|
||||||
summary: Adds a new event type
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
eventType:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
patch:
|
|
||||||
description: Updates an event type for the user.
|
|
||||||
summary: Updates an event type
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
eventType:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
delete:
|
|
||||||
description: Deletes an event type for the user.
|
|
||||||
summary: Deletes an event type
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
"/api/book/event":
|
"/api/book/event":
|
||||||
post:
|
post:
|
||||||
description: Creates a booking in the user's calendar.
|
description: Creates a booking in the user's calendar.
|
||||||
@@ -599,7 +426,7 @@ paths:
|
|||||||
- installed: true
|
- installed: true
|
||||||
type: caldav_calendar
|
type: caldav_calendar
|
||||||
credential: null
|
credential: null
|
||||||
title: CalDav Server
|
title: CalDav (Beta)
|
||||||
imageSrc: integrations/caldav.svg
|
imageSrc: integrations/caldav.svg
|
||||||
description: For personal and business calendars
|
description: For personal and business calendars
|
||||||
__N_SSP: true
|
__N_SSP: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { XIcon } from "@heroicons/react/outline";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<span className="sr-only">{t("dismiss")}</span>
|
<span className="sr-only">{t("dismiss")}</span>
|
||||||
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
<Icon.FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
const options = data.items.map((item) => ({
|
const options = data.items.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
slug: item.slug,
|
slug: item.slug,
|
||||||
image: item.imageSrc,
|
image: item.logo,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +73,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 font-medium text-gray-700 sm:text-sm"
|
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
components={{ Option: ImageOption }}
|
components={{ Option: ImageOption }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+49
-69
@@ -1,28 +1,20 @@
|
|||||||
import {
|
|
||||||
BookOpenIcon,
|
|
||||||
CheckIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
FlagIcon,
|
|
||||||
MailIcon,
|
|
||||||
PlusIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
} from "@heroicons/react/outline";
|
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/solid";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, SkeletonButton } from "@calcom/ui";
|
import { Button, SkeletonButton } from "@calcom/ui";
|
||||||
import LicenseRequired from "@ee/components/LicenseRequired";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import Shell from "@calcom/ui/Shell";
|
||||||
|
import Badge from "@calcom/ui/v2/core/Badge";
|
||||||
|
|
||||||
import Shell from "@components/Shell";
|
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||||
import Badge from "@components/ui/Badge";
|
|
||||||
|
|
||||||
const Component = ({
|
const Component = ({
|
||||||
name,
|
name,
|
||||||
@@ -43,8 +35,15 @@ const Component = ({
|
|||||||
isProOnly,
|
isProOnly,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||||
|
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const handleOpenChange = () => {
|
||||||
|
utils.invalidateQueries(["viewer.integrations"]);
|
||||||
|
router.replace("/apps/installed");
|
||||||
|
};
|
||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast("App successfully installed", "success");
|
showToast("App successfully installed", "success");
|
||||||
@@ -59,42 +58,22 @@ const Component = ({
|
|||||||
currency: "USD",
|
currency: "USD",
|
||||||
useGrouping: false,
|
useGrouping: false,
|
||||||
}).format(price);
|
}).format(price);
|
||||||
const [installedAppCount, setInstalledAppCount] = useState(0);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
||||||
useEffect(() => {
|
const appCredentials = trpc.useQuery(["viewer.appCredentialsByType", { appType: type }], {
|
||||||
async function getInstalledApp(appCredentialType: string) {
|
onSuccess(data) {
|
||||||
const queryParam = new URLSearchParams();
|
setExistingCredentials(data);
|
||||||
queryParam.set("app-credential-type", appCredentialType);
|
},
|
||||||
try {
|
});
|
||||||
const result = await fetch(`/api/app-store/installed?${queryParam.toString()}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}).then((data) => {
|
|
||||||
setIsLoading(false);
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
if (result.status === 200) {
|
|
||||||
const res = await result.json();
|
|
||||||
setInstalledAppCount(res.count);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.log(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getInstalledApp(type);
|
|
||||||
}, [type]);
|
|
||||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-mx-4 md:-mx-8">
|
<div className="-mx-4 md:-mx-8">
|
||||||
<div className="bg-gray-50 px-8">
|
<div className="px-8">
|
||||||
<Link href="/apps">
|
<Link href="/apps">
|
||||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
|
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
|
||||||
<ChevronLeftIcon className="h-5 w-5" /> {t("browse_apps")}
|
<Icon.FiChevronLeft className="h-5 w-5" /> {t("browse_apps")}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="items-center justify-between py-4 sm:flex sm:py-8">
|
<div className="items-center justify-between py-4 sm:flex sm:py-8">
|
||||||
@@ -103,11 +82,6 @@ const Component = ({
|
|||||||
<header className="px-4 py-2">
|
<header className="px-4 py-2">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
||||||
{isProOnly && user?.plan === "FREE" ? (
|
|
||||||
<Badge className="ml-2" variant="default">
|
|
||||||
PRO
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm text-gray-500">
|
<h2 className="text-sm text-gray-500">
|
||||||
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
|
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
|
||||||
@@ -116,12 +90,13 @@ const Component = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 sm:mt-0 sm:text-right">
|
<div className="mt-4 sm:mt-0 sm:text-right">
|
||||||
{!isLoading ? (
|
{!appCredentials.isLoading ? (
|
||||||
isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
|
isGlobal ||
|
||||||
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button StartIcon={CheckIcon} color="secondary" disabled>
|
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
|
||||||
{installedAppCount > 0
|
{existingCredentials.length > 0
|
||||||
? t("active_install", { count: installedAppCount })
|
? t("active_install", { count: existingCredentials.length })
|
||||||
: t("globally_install")}
|
: t("globally_install")}
|
||||||
</Button>
|
</Button>
|
||||||
{!isGlobal && (
|
{!isGlobal && (
|
||||||
@@ -138,7 +113,7 @@ const Component = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button StartIcon={PlusIcon} {...props} data-testid="install-app-button">
|
<Button StartIcon={Icon.FiPlus} {...props} data-testid="install-app-button">
|
||||||
{t("add_another")}
|
{t("add_another")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -146,10 +121,16 @@ const Component = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : installedAppCount > 0 ? (
|
) : existingCredentials.length > 0 ? (
|
||||||
<Button color="secondary" disabled title="App already installed">
|
<DisconnectIntegration
|
||||||
{t("installed")}
|
id={existingCredentials[0]}
|
||||||
</Button>
|
render={(btnProps) => (
|
||||||
|
<Button {...btnProps} color="warn" data-testid={type + "-integration-disconnect-button"}>
|
||||||
|
{t("disconnect")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={type}
|
type={type}
|
||||||
@@ -170,7 +151,7 @@ const Component = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
) : (
|
) : (
|
||||||
<SkeletonButton width="24" height="10" />
|
<SkeletonButton width="24" height="10" />
|
||||||
)}
|
)}
|
||||||
@@ -182,8 +163,6 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* reintroduce once we show permissions and features
|
|
||||||
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="justify-between px-8 py-10 md:flex">
|
<div className="justify-between px-8 py-10 md:flex">
|
||||||
@@ -223,7 +202,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-500 no-underline hover:underline"
|
className="text-blue-500 no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<BookOpenIcon className="mr-1 -mt-1 inline h-4 w-4" />
|
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -235,7 +214,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-500 no-underline hover:underline"
|
className="text-blue-500 no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<ExternalLinkIcon className="mr-1 -mt-px inline h-4 w-4" />
|
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -247,7 +226,8 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-500 no-underline hover:underline"
|
className="text-blue-500 no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={"mailto:" + email}>
|
||||||
<MailIcon className="mr-1 -mt-px inline h-4 w-4" />
|
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -259,7 +239,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-500 no-underline hover:underline"
|
className="text-blue-500 no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<DocumentTextIcon className="mr-1 -mt-px inline h-4 w-4" />
|
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -271,7 +251,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-500 no-underline hover:underline"
|
className="text-blue-500 no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<ShieldCheckIcon className="mr-1 -mt-px inline h-4 w-4" />
|
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -284,7 +264,7 @@ const Component = ({
|
|||||||
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
|
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
|
||||||
</small>
|
</small>
|
||||||
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
||||||
<FlagIcon className="inline h-3 w-3" /> Report App
|
<Icon.FiFlag className="inline h-3 w-3" /> Report App
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ export default function AppsShell({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-12 block lg:hidden">
|
<div className="mb-12 block lg:hidden">{status === "authenticated" && <NavTabs tabs={tabs} />}</div>
|
||||||
{status === "authenticated" && <NavTabs tabs={tabs} linkProps={{ shallow: true }} />}
|
|
||||||
</div>
|
|
||||||
<main className="pb-6">{children}</main>
|
<main className="pb-6">{children}</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -11,6 +12,7 @@ interface Props {
|
|||||||
hidePlaceholder?: boolean;
|
hidePlaceholder?: boolean;
|
||||||
/** The external Id of the connected calendar */
|
/** The external Id of the connected calendar */
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
|
destinationCalendar?: DestinationCalendar | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
const DestinationCalendarSelector = ({
|
||||||
@@ -18,6 +20,7 @@ const DestinationCalendarSelector = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
value,
|
value,
|
||||||
hidePlaceholder,
|
hidePlaceholder,
|
||||||
|
destinationCalendar,
|
||||||
}: Props): JSX.Element | null => {
|
}: Props): JSX.Element | null => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||||
@@ -67,11 +70,26 @@ const DestinationCalendarSelector = ({
|
|||||||
value: `${cal.integration}:${cal.externalId}`,
|
value: `${cal.integration}:${cal.externalId}`,
|
||||||
})),
|
})),
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
||||||
|
? `(${
|
||||||
|
destinationCalendar.externalId.length > 15
|
||||||
|
? destinationCalendar.externalId.substring(0, 15) + "..."
|
||||||
|
: destinationCalendar.externalId
|
||||||
|
})`
|
||||||
|
: "";
|
||||||
return (
|
return (
|
||||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
||||||
<Select
|
<Select
|
||||||
name="primarySelectedCalendar"
|
name="primarySelectedCalendar"
|
||||||
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
|
placeholder={
|
||||||
|
!hidePlaceholder ? (
|
||||||
|
`${t("select_destination_calendar")}`
|
||||||
|
) : (
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
options={options}
|
options={options}
|
||||||
styles={{
|
styles={{
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
||||||
@@ -89,17 +107,17 @@ const DestinationCalendarSelector = ({
|
|||||||
control: (defaultStyles) => {
|
control: (defaultStyles) => {
|
||||||
return {
|
return {
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
borderRadius: "2px",
|
borderRadius: "6px",
|
||||||
"@media only screen and (min-width: 640px)": {
|
"@media only screen and (min-width: 640px)": {
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
||||||
maxWidth: "320px",
|
width: "100%",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm",
|
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
||||||
!hidePlaceholder && "font-medium"
|
!hidePlaceholder && "font-medium"
|
||||||
)}
|
)}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
@@ -118,6 +136,9 @@ const DestinationCalendarSelector = ({
|
|||||||
}}
|
}}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
value={selectedOption}
|
value={selectedOption}
|
||||||
|
components={{
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+142
-164
@@ -1,21 +1,17 @@
|
|||||||
import { ArrowLeftIcon, ChevronRightIcon, CodeIcon, EyeIcon, SunIcon } from "@heroicons/react/solid";
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useRouter } from "next/router";
|
import { NextRouter, useRouter } from "next/router";
|
||||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
||||||
import { components, ControlProps } from "react-select";
|
import { components, ControlProps } from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { EventType } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Button, Switch } from "@calcom/ui";
|
|
||||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
|
||||||
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
|
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
|
||||||
|
import { Button, HorizontalTabs, showToast, Switch } from "@calcom/ui/v2";
|
||||||
|
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
||||||
|
|
||||||
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
|
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
|
||||||
|
|
||||||
import NavTabs from "@components/NavTabs";
|
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
import ColorPicker from "@components/ui/colorpicker";
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ type PreviewState = {
|
|||||||
brandColor: string;
|
brandColor: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const queryParamsForDialog = ["embedType", "tabName", "eventTypeId"];
|
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||||
|
|
||||||
const getDimension = (dimension: string) => {
|
const getDimension = (dimension: string) => {
|
||||||
if (dimension.match(/^\d+$/)) {
|
if (dimension.match(/^\d+$/)) {
|
||||||
@@ -49,6 +45,23 @@ const getDimension = (dimension: string) => {
|
|||||||
return dimension;
|
return dimension;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goto = (router: NextRouter, searchParams: Record<string, string>) => {
|
||||||
|
const newQuery = new URLSearchParams(router.asPath.split("?")[1]);
|
||||||
|
Object.keys(searchParams).forEach((key) => {
|
||||||
|
newQuery.set(key, searchParams[key]);
|
||||||
|
});
|
||||||
|
router.push(`${router.asPath.split("?")[0]}?${newQuery.toString()}`, undefined, {
|
||||||
|
shallow: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeQueryParams = (router: NextRouter, queryParams: string[]) => {
|
||||||
|
queryParams.forEach((param) => {
|
||||||
|
delete router.query[param];
|
||||||
|
});
|
||||||
|
router.push(`${router.asPath.split("?")[0]}?${router.query.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It allows us to show code with certain reusable blocks indented according to the block variable placement
|
* It allows us to show code with certain reusable blocks indented according to the block variable placement
|
||||||
* So, if you add a variable ${abc} with indentation of 4 spaces, it will automatically indent all newlines in `abc` with the same indent before constructing the final string
|
* So, if you add a variable ${abc} with indentation of 4 spaces, it will automatically indent all newlines in `abc` with the same indent before constructing the final string
|
||||||
@@ -224,7 +237,11 @@ const getEmbedTypeSpecificString = ({
|
|||||||
if (!frameworkCodes) {
|
if (!frameworkCodes) {
|
||||||
throw new Error(`No code available for the framework:${embedFramework}`);
|
throw new Error(`No code available for the framework:${embedFramework}`);
|
||||||
}
|
}
|
||||||
let uiInstructionStringArg = undefined;
|
let uiInstructionStringArg: {
|
||||||
|
apiName: string;
|
||||||
|
theme: PreviewState["theme"];
|
||||||
|
brandColor: string;
|
||||||
|
};
|
||||||
if (embedFramework === "react") {
|
if (embedFramework === "react") {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "cal",
|
apiName: "cal",
|
||||||
@@ -451,8 +468,8 @@ const embeds: {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
tabName: "embed-code",
|
href: "embedTabName=embed-code",
|
||||||
icon: CodeIcon,
|
icon: Icon.FiCode,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -474,7 +491,8 @@ const tabs = [
|
|||||||
data-testid="embed-code"
|
data-testid="embed-code"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
name="embed-code"
|
name="embed-code"
|
||||||
className="h-[calc(100%-50px)]"
|
className="h-[calc(100%-50px)] font-mono"
|
||||||
|
style={{ resize: "none", overflow: "auto" }}
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
`<!-- Cal ${embedType} embed code begins -->\n` +
|
`<!-- Cal ${embedType} embed code begins -->\n` +
|
||||||
@@ -501,8 +519,8 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "React",
|
name: "React",
|
||||||
tabName: "embed-react",
|
href: "embedTabName=embed-react",
|
||||||
icon: CodeIcon,
|
icon: Icon.FiCode,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -522,8 +540,9 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
data-testid="embed-react"
|
data-testid="embed-react"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
name="embed-react"
|
name="embed-react"
|
||||||
className="h-[calc(100%-50px)]"
|
className="h-[calc(100%-50px)] font-mono"
|
||||||
readOnly
|
readOnly
|
||||||
|
style={{ resize: "none", overflow: "auto" }}
|
||||||
value={`/* First make sure that you have installed the package */
|
value={`/* First make sure that you have installed the package */
|
||||||
|
|
||||||
/* If you are using yarn */
|
/* If you are using yarn */
|
||||||
@@ -540,12 +559,12 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Preview",
|
name: "Preview",
|
||||||
tabName: "embed-preview",
|
href: "embedTabName=embed-preview",
|
||||||
icon: EyeIcon,
|
icon: Icon.FiTrello,
|
||||||
type: "iframe",
|
type: "iframe",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||||
{ calLink: string; embedType: EmbedType }
|
{ calLink: string; embedType: EmbedType; previewState: PreviewState }
|
||||||
>(function Preview({ calLink, embedType }, ref) {
|
>(function Preview({ calLink, embedType }, ref) {
|
||||||
if (ref instanceof Function || !ref) {
|
if (ref instanceof Function || !ref) {
|
||||||
return null;
|
return null;
|
||||||
@@ -557,7 +576,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
<iframe
|
<iframe
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
|
||||||
data-testid="embed-preview"
|
data-testid="embed-preview"
|
||||||
className="border-1 h-[75vh] border"
|
className="border-1 h-[100vh] border"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
|
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
|
||||||
@@ -577,7 +596,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
|||||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||||
return (
|
return (
|
||||||
<components.Control {...props}>
|
<components.Control {...props}>
|
||||||
<SunIcon className="h-[32px] w-[32px] text-gray-500" />
|
<Icon.FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||||
{children}
|
{children}
|
||||||
</components.Control>
|
</components.Control>
|
||||||
);
|
);
|
||||||
@@ -587,7 +606,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<DialogContent size="lg">
|
<DialogContent type="creation" useOwnActionButtons size="lg">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
{t("how_you_want_add_cal_site")}
|
{t("how_you_want_add_cal_site")}
|
||||||
@@ -596,18 +615,15 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
|
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex items-start">
|
||||||
{embeds.map((embed, index) => (
|
{embeds.map((embed, index) => (
|
||||||
<button
|
<button
|
||||||
className="mr-2 w-1/3 p-3 text-left hover:rounded-md hover:border hover:bg-neutral-100"
|
className="mr-2 w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100"
|
||||||
key={index}
|
key={index}
|
||||||
data-testid={embed.type}
|
data-testid={embed.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push({
|
goto(router, {
|
||||||
query: {
|
embedType: embed.type,
|
||||||
...router.query,
|
|
||||||
embedType: embed.type,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
||||||
@@ -623,15 +639,22 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EmbedTypeCodeAndPreviewDialogContent = ({
|
const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||||
eventTypeId,
|
|
||||||
embedType,
|
embedType,
|
||||||
|
embedUrl,
|
||||||
}: {
|
}: {
|
||||||
eventTypeId: EventType["id"];
|
|
||||||
embedType: EmbedType;
|
embedType: EmbedType;
|
||||||
|
embedUrl: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const s = (href: string) => {
|
||||||
|
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
||||||
|
const [a, b] = href.split("=");
|
||||||
|
searchParams.set(a, b);
|
||||||
|
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
||||||
|
};
|
||||||
|
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
||||||
const embedCodeRefs: Record<typeof tabs[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
const embedCodeRefs: Record<typeof tabs[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
||||||
tabs
|
tabs
|
||||||
.filter((tab) => tab.type === "code")
|
.filter((tab) => tab.type === "code")
|
||||||
@@ -642,13 +665,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
||||||
const embed = embeds.find((embed) => embed.type === embedType);
|
const embed = embeds.find((embed) => embed.type === embedType);
|
||||||
|
|
||||||
const { data: eventType, isLoading } = trpc.useQuery([
|
|
||||||
"viewer.eventTypes.get",
|
|
||||||
{
|
|
||||||
id: +eventTypeId,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||||
const [previewState, setPreviewState] = useState({
|
const [previewState, setPreviewState] = useState({
|
||||||
@@ -665,43 +681,22 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
const noPopupQuery = {
|
removeQueryParams(router, ["dialog", ...queryParamsForDialog]);
|
||||||
...router.query,
|
|
||||||
};
|
|
||||||
|
|
||||||
delete noPopupQuery.dialog;
|
|
||||||
|
|
||||||
queryParamsForDialog.forEach((queryParam) => {
|
|
||||||
delete noPopupQuery[queryParam];
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
query: noPopupQuery,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use embed-code as default tab
|
// Use embed-code as default tab
|
||||||
if (!router.query.tabName) {
|
if (!router.query.embedTabName) {
|
||||||
router.query.tabName = "embed-code";
|
goto(router, {
|
||||||
router.push({
|
embedTabName: "embed-code",
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (!embed || !embedUrl) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!embed || !eventType) {
|
|
||||||
close();
|
close();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calLink = `${eventType.team ? `team/${eventType.team.slug}` : eventType.users[0].username}/${
|
const calLink = decodeURIComponent(embedUrl);
|
||||||
eventType.slug
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const addToPalette = (update: typeof previewState["palette"]) => {
|
const addToPalette = (update: typeof previewState["palette"]) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -789,22 +784,15 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent size="xl">
|
<DialogContent size="xl" className="p-0.5" type="creation" useOwnActionButtons>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-white p-6">
|
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newQuery = { ...router.query };
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
delete newQuery.embedType;
|
|
||||||
delete newQuery.tabName;
|
|
||||||
router.push({
|
|
||||||
query: {
|
|
||||||
...newQuery,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}>
|
}}>
|
||||||
<ArrowLeftIcon className="mr-4 w-4" />
|
<Icon.FiArrowLeft className="mr-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -823,7 +811,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
? "Floating Popup Customization"
|
? "Floating Popup Customization"
|
||||||
: "Element Click Customization"}
|
: "Element Click Customization"}
|
||||||
</div>
|
</div>
|
||||||
<ChevronRightIcon
|
<Icon.FiChevronRight
|
||||||
className={`${
|
className={`${
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-neutral-500`}
|
} ml-auto h-5 w-5 text-neutral-500`}
|
||||||
@@ -854,7 +842,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
}}
|
}}
|
||||||
addOnLeading={<InputLeading>W</InputLeading>}
|
addOnLeading={<InputLeading>W</InputLeading>}
|
||||||
/>
|
/>
|
||||||
<span className="p-2">x</span>
|
<span className="p-2">×</span>
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
name="height"
|
name="height"
|
||||||
@@ -880,9 +868,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"mt-4 items-center justify-between",
|
||||||
embedType === "floating-popup" ? "flex" : "hidden"
|
embedType === "floating-popup" ? "" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div className="text-sm">Button Text</div>
|
<div className="mb-2 text-sm">Button Text</div>
|
||||||
{/* Default Values should come from preview iframe */}
|
{/* Default Values should come from preview iframe */}
|
||||||
<TextField
|
<TextField
|
||||||
name="buttonText"
|
name="buttonText"
|
||||||
@@ -904,10 +892,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 flex items-center justify-between",
|
"mt-4 flex items-center justify-start",
|
||||||
embedType === "floating-popup" ? "flex" : "hidden"
|
embedType === "floating-popup" ? "space-x-2" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div className="text-sm">Display Calendar Icon Button</div>
|
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@@ -922,13 +909,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className="text-sm">Display Calendar Icon Button</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 flex items-center justify-between",
|
"mt-4 items-center justify-between",
|
||||||
embedType === "floating-popup" ? "flex" : "hidden"
|
embedType === "floating-popup" ? "" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div>Position of Button</div>
|
<div className="mb-2">Position of Button</div>
|
||||||
<Select
|
<Select
|
||||||
onChange={(position) => {
|
onChange={(position) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -945,13 +933,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={FloatingPopupPositionOptions}
|
options={FloatingPopupPositionOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
className={classNames(
|
|
||||||
"mt-4 flex items-center justify-between",
|
|
||||||
embedType === "floating-popup" ? "flex" : "hidden"
|
|
||||||
)}>
|
|
||||||
<div>Button Color</div>
|
<div>Button Color</div>
|
||||||
<div className="w-36">
|
<div className="w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
@@ -968,13 +952,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
className={classNames(
|
|
||||||
"mt-4 flex items-center justify-between",
|
|
||||||
embedType === "floating-popup" ? "flex" : "hidden"
|
|
||||||
)}>
|
|
||||||
<div>Text Color</div>
|
<div>Text Color</div>
|
||||||
<div className="w-36">
|
<div className="w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
@@ -1001,7 +981,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
<CollapsibleTrigger className="flex w-full" type="button">
|
||||||
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
|
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
|
||||||
<ChevronRightIcon
|
<Icon.FiChevronRight
|
||||||
className={`${
|
className={`${
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-neutral-500`}
|
} ml-auto h-5 w-5 text-neutral-500`}
|
||||||
@@ -1009,10 +989,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="mt-6 text-sm">
|
||||||
<Label className="flex items-center justify-between">
|
<Label className="">
|
||||||
<div>Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
className="w-36"
|
className="w-full"
|
||||||
defaultValue={ThemeOptions[0]}
|
defaultValue={ThemeOptions[0]}
|
||||||
components={{
|
components={{
|
||||||
Control: ThemeSelectControl,
|
Control: ThemeSelectControl,
|
||||||
@@ -1039,9 +1019,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
// { name: "highlightColor", title: "Highlight Color" },
|
// { name: "highlightColor", title: "Highlight Color" },
|
||||||
// { name: "medianColor", title: "Median Color" },
|
// { name: "medianColor", title: "Median Color" },
|
||||||
].map((palette) => (
|
].map((palette) => (
|
||||||
<Label key={palette.name} className="flex items-center justify-between">
|
<Label key={palette.name} className="pb-4">
|
||||||
<div>{palette.title}</div>
|
<div className="mb-2 pt-2">{palette.title}</div>
|
||||||
<div className="w-36">
|
<div className="w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
@@ -1058,33 +1038,33 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/3 bg-gray-50 p-6">
|
<div className="flex w-2/3 flex-col p-8">
|
||||||
<NavTabs data-testid="embed-tabs" tabs={tabs} linkProps={{ shallow: true }} />
|
<HorizontalTabs data-testid="embed-tabs" tabs={parsedTabs} linkProps={{ shallow: true }} />
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tab.tabName}
|
key={tab.href}
|
||||||
className={classNames(router.query.tabName === tab.tabName ? "block" : "hidden")}>
|
className={classNames(
|
||||||
<div>
|
router.query.embedTabName === tab.href.split("=")[1] ? "flex flex-grow flex-col" : "hidden"
|
||||||
<div className={classNames(tab.type === "code" ? "h-[75vh]" : "")}>
|
)}>
|
||||||
{tab.type === "code" ? (
|
<div className="flex h-[55vh] flex-grow flex-col">
|
||||||
<tab.Component
|
{tab.type === "code" ? (
|
||||||
embedType={embedType}
|
<tab.Component
|
||||||
calLink={calLink}
|
embedType={embedType}
|
||||||
previewState={previewState}
|
calLink={calLink}
|
||||||
ref={refOfEmbedCodesRefs.current[tab.name]}
|
previewState={previewState}
|
||||||
/>
|
ref={refOfEmbedCodesRefs.current[tab.name]}
|
||||||
) : (
|
/>
|
||||||
<tab.Component
|
) : (
|
||||||
embedType={embedType}
|
<tab.Component
|
||||||
calLink={calLink}
|
embedType={embedType}
|
||||||
previewState={previewState}
|
calLink={calLink}
|
||||||
ref={iframeRef}
|
previewState={previewState}
|
||||||
/>
|
ref={iframeRef}
|
||||||
)}
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"} />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className={router.query.embedTabName == "embed-preview" ? "block" : "hidden"} />
|
||||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||||
{tab.type === "code" ? (
|
{tab.type === "code" ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -1115,62 +1095,60 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
|
|
||||||
export const EmbedDialog = () => {
|
export const EmbedDialog = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const eventTypeId: EventType["id"] = +(router.query.eventTypeId as string);
|
const embedUrl: string = router.query.embedUrl as string;
|
||||||
return (
|
return (
|
||||||
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
|
<Dialog
|
||||||
|
name="embed"
|
||||||
|
clearQueryParamsOnClose={queryParamsForDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) window.resetEmbedStatus();
|
||||||
|
}}>
|
||||||
{!router.query.embedType ? (
|
{!router.query.embedType ? (
|
||||||
<ChooseEmbedTypesDialogContent />
|
<ChooseEmbedTypesDialogContent />
|
||||||
) : (
|
) : (
|
||||||
<EmbedTypeCodeAndPreviewDialogContent
|
<EmbedTypeCodeAndPreviewDialogContent
|
||||||
eventTypeId={eventTypeId}
|
|
||||||
embedType={router.query.embedType as EmbedType}
|
embedType={router.query.embedType as EmbedType}
|
||||||
|
embedUrl={embedUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
type EmbedButtonProps<T> = {
|
||||||
|
embedUrl: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
as?: T;
|
||||||
|
};
|
||||||
|
|
||||||
export const EmbedButton = ({
|
export const EmbedButton = <T extends React.ElementType>({
|
||||||
eventTypeId,
|
embedUrl,
|
||||||
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
dark,
|
as,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
|
||||||
eventTypeId: EventType["id"];
|
|
||||||
className: string;
|
|
||||||
dark?: boolean;
|
|
||||||
}) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
className = classNames(className, "hidden lg:flex");
|
className = classNames(className, "hidden lg:inline-flex");
|
||||||
const openEmbedModal = () => {
|
const openEmbedModal = () => {
|
||||||
const query = {
|
goto(router, {
|
||||||
...router.query,
|
|
||||||
dialog: "embed",
|
dialog: "embed",
|
||||||
eventTypeId,
|
embedUrl,
|
||||||
};
|
});
|
||||||
router.push(
|
|
||||||
{
|
|
||||||
pathname: router.pathname,
|
|
||||||
query,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ shallow: true }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
const Component = as ?? Button;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Component
|
||||||
type="button"
|
|
||||||
color="minimal"
|
|
||||||
size="sm"
|
|
||||||
className={className}
|
|
||||||
{...props}
|
{...props}
|
||||||
data-test-eventtype-id={eventTypeId}
|
className={className}
|
||||||
data-testid="event-type-embed"
|
data-test-embed-url={embedUrl}
|
||||||
onClick={() => openEmbedModal()}>
|
data-testid="embed"
|
||||||
<CodeIcon className={classNames("h-4 w-4 ltr:mr-2 rtl:ml-2", dark ? "" : "text-neutral-500")} />
|
type="button"
|
||||||
{t("Embed")}
|
onClick={() => {
|
||||||
</Button>
|
openEmbedModal();
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { Dispatch, useState, useEffect } from "react";
|
||||||
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
|
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
||||||
|
|
||||||
|
export type GateState = {
|
||||||
|
rainbowToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GateProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
gates: Gate[];
|
||||||
|
metadata: JSONObject;
|
||||||
|
dispatch: Dispatch<Partial<GateState>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RainbowGate = dynamic(() => import("@calcom/app-store/rainbow/components/RainbowKit"));
|
||||||
|
|
||||||
|
// To add a new Gate just add the gate logic to the switch statement
|
||||||
|
const Gates: React.FC<GateProps> = ({ children, gates, metadata, dispatch }) => {
|
||||||
|
const [rainbowToken, setRainbowToken] = useState<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({ rainbowToken });
|
||||||
|
}, [rainbowToken, dispatch]);
|
||||||
|
|
||||||
|
let gateWrappers = <>{children}</>;
|
||||||
|
|
||||||
|
// Recursively wraps the `gateWrappers` with new gates allowing for multiple gates
|
||||||
|
for (const gate of gates) {
|
||||||
|
switch (gate) {
|
||||||
|
case "rainbow":
|
||||||
|
if (metadata.blockchainId && metadata.smartContractAddress && !rainbowToken) {
|
||||||
|
gateWrappers = (
|
||||||
|
<RainbowGate
|
||||||
|
setToken={setRainbowToken}
|
||||||
|
chainId={metadata.blockchainId as number}
|
||||||
|
tokenAddress={metadata.smartContractAddress as string}>
|
||||||
|
{gateWrappers}
|
||||||
|
</RainbowGate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateWrappers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gates;
|
||||||
@@ -63,6 +63,7 @@ function CropContainer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `packages/ui/v2/core/ImageUploader.tsx` */
|
||||||
export default function ImageUploader({
|
export default function ImageUploader({
|
||||||
target,
|
target,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* @deprecated Use custom Skeletons instead
|
||||||
|
**/
|
||||||
export { default } from "@calcom/ui/Loader";
|
export { default } from "@calcom/ui/Loader";
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { LOGO_ICON, LOGO } from "@calcom/lib/constants";
|
|
||||||
|
|
||||||
export default function Logo({ small, icon }: { small?: boolean; icon?: boolean }) {
|
|
||||||
return (
|
|
||||||
<h1 className="inline">
|
|
||||||
<strong>
|
|
||||||
{icon ? (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img className="mx-auto w-9" alt="Cal" title="Cal" src={LOGO_ICON} />
|
|
||||||
) : (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img className={small ? "h-4 w-auto" : "h-5 w-auto"} alt="Cal" title="Cal" src={LOGO} />
|
|
||||||
)}
|
|
||||||
</strong>
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -77,7 +77,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
<tab.icon
|
<tab.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
||||||
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,48 +1,41 @@
|
|||||||
import {
|
|
||||||
CreditCardIcon,
|
|
||||||
KeyIcon,
|
|
||||||
LockClosedIcon,
|
|
||||||
UserGroupIcon,
|
|
||||||
UserIcon,
|
|
||||||
CodeIcon,
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
import ErrorBoundary from "@lib/ErrorBoundary";
|
import ErrorBoundary from "@calcom/ui/ErrorBoundary";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import Shell from "@calcom/ui/Shell";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
import Shell from "./Shell";
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: "profile",
|
name: "profile",
|
||||||
href: "/settings/profile",
|
href: "/settings/profile",
|
||||||
icon: UserIcon,
|
icon: Icon.FiUser,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "teams",
|
name: "teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
icon: UserGroupIcon,
|
icon: Icon.FiUsers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "security",
|
name: "security",
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: KeyIcon,
|
icon: Icon.FiKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "developer",
|
name: "developer",
|
||||||
href: "/settings/developer",
|
href: "/settings/developer",
|
||||||
icon: CodeIcon,
|
icon: Icon.FiTerminal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "billing",
|
name: "billing",
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: CreditCardIcon,
|
icon: Icon.FiCreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "admin",
|
name: "admin",
|
||||||
href: "/settings/admin",
|
href: "/settings/admin",
|
||||||
icon: LockClosedIcon,
|
icon: Icon.FiLock,
|
||||||
adminRequired: true,
|
adminRequired: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,689 +0,0 @@
|
|||||||
import { SelectorIcon } from "@heroicons/react/outline";
|
|
||||||
import {
|
|
||||||
ArrowLeftIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CogIcon,
|
|
||||||
CollectionIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
LightningBoltIcon,
|
|
||||||
LinkIcon,
|
|
||||||
LogoutIcon,
|
|
||||||
MapIcon,
|
|
||||||
MoonIcon,
|
|
||||||
QuestionMarkCircleIcon,
|
|
||||||
ViewGridIcon,
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
import { UserPlan } from "@prisma/client";
|
|
||||||
import { SessionContextValue, signOut, useSession } from "next-auth/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import React, { Fragment, ReactNode, useEffect, useState } from "react";
|
|
||||||
import { Toaster } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
|
||||||
import { WEBAPP_URL, JOIN_SLACK, ROADMAP } from "@calcom/lib/constants";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
import Dropdown, {
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@calcom/ui/Dropdown";
|
|
||||||
import LicenseBanner from "@ee/components/LicenseBanner";
|
|
||||||
import TrialBanner from "@ee/components/TrialBanner";
|
|
||||||
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
|
|
||||||
|
|
||||||
import ErrorBoundary from "@lib/ErrorBoundary";
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
import { shouldShowOnboarding } from "@lib/getting-started";
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
|
||||||
|
|
||||||
import { KBarRoot, KBarContent, KBarTrigger } from "@components/Kbar";
|
|
||||||
import Loader from "@components/Loader";
|
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
|
||||||
import Badge from "@components/ui/Badge";
|
|
||||||
import ImpersonatingBanner from "@components/ui/ImpersonatingBanner";
|
|
||||||
|
|
||||||
import pkg from "../package.json";
|
|
||||||
import { useViewerI18n } from "./I18nLanguageHandler";
|
|
||||||
import Logo from "./Logo";
|
|
||||||
|
|
||||||
function useRedirectToLoginIfUnauthenticated(isPublic = false) {
|
|
||||||
const { data: session, status } = useSession();
|
|
||||||
const loading = status === "loading";
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPublic) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loading && !session) {
|
|
||||||
router.replace({
|
|
||||||
pathname: "/auth/login",
|
|
||||||
query: {
|
|
||||||
callbackUrl: `${WEBAPP_URL}${location.pathname}${location.search}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [loading, session, isPublic]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading: loading && !session,
|
|
||||||
session,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useRedirectToOnboardingIfNeeded() {
|
|
||||||
const router = useRouter();
|
|
||||||
const query = useMeQuery();
|
|
||||||
const user = query.data;
|
|
||||||
|
|
||||||
const isRedirectingToOnboarding = user && shouldShowOnboarding(user);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRedirectingToOnboarding) {
|
|
||||||
router.replace({
|
|
||||||
pathname: "/getting-started",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isRedirectingToOnboarding]);
|
|
||||||
return {
|
|
||||||
isRedirectingToOnboarding,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShellSubHeading(props: {
|
|
||||||
title: ReactNode;
|
|
||||||
subtitle?: ReactNode;
|
|
||||||
actions?: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={classNames("mb-3 block justify-between sm:flex", props.className)}>
|
|
||||||
<div>
|
|
||||||
<h2 className="flex content-center items-center space-x-2 text-base font-bold leading-6 text-gray-900 rtl:space-x-reverse">
|
|
||||||
{props.title}
|
|
||||||
</h2>
|
|
||||||
{props.subtitle && <p className="text-sm text-neutral-500 ltr:mr-4">{props.subtitle}</p>}
|
|
||||||
</div>
|
|
||||||
{props.actions && <div className="flex-shrink-0">{props.actions}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Layout = ({
|
|
||||||
status,
|
|
||||||
plan,
|
|
||||||
...props
|
|
||||||
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
|
|
||||||
const isEmbed = useIsEmbed();
|
|
||||||
const router = useRouter();
|
|
||||||
const { data: routingForms } = trpc.useQuery(["viewer.appById", { appId: "routing_forms" }], {
|
|
||||||
enabled: status === "authenticated",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useLocale();
|
|
||||||
const navigation = [
|
|
||||||
{
|
|
||||||
name: t("event_types_page_title"),
|
|
||||||
href: "/event-types",
|
|
||||||
icon: LinkIcon,
|
|
||||||
current: router.asPath.startsWith("/event-types"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t("bookings"),
|
|
||||||
href: "/bookings/upcoming",
|
|
||||||
icon: CalendarIcon,
|
|
||||||
current: router.asPath.startsWith("/bookings"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t("availability"),
|
|
||||||
href: "/availability",
|
|
||||||
icon: ClockIcon,
|
|
||||||
current: router.asPath.startsWith("/availability"),
|
|
||||||
},
|
|
||||||
routingForms
|
|
||||||
? {
|
|
||||||
name: "Routing Forms",
|
|
||||||
href: "/apps/routing_forms/forms",
|
|
||||||
icon: CollectionIcon,
|
|
||||||
current: router.asPath.startsWith("/apps/routing_forms/"),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
{
|
|
||||||
name: t("workflows"),
|
|
||||||
href: "/workflows",
|
|
||||||
icon: LightningBoltIcon,
|
|
||||||
current: router.asPath.startsWith("/workflows"),
|
|
||||||
pro: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t("apps"),
|
|
||||||
href: "/apps",
|
|
||||||
icon: ViewGridIcon,
|
|
||||||
current: router.asPath.startsWith("/apps") && !router.asPath.startsWith("/apps/routing_forms/"),
|
|
||||||
child: [
|
|
||||||
{
|
|
||||||
name: t("app_store"),
|
|
||||||
href: "/apps",
|
|
||||||
current: router.asPath === "/apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t("installed_apps"),
|
|
||||||
href: "/apps/installed",
|
|
||||||
current: router.asPath === "/apps/installed",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t("settings"),
|
|
||||||
href: "/settings/profile",
|
|
||||||
icon: CogIcon,
|
|
||||||
current: router.asPath.startsWith("/settings"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const pageTitle = typeof props.heading === "string" ? props.heading : props.title;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HeadSeo
|
|
||||||
title={pageTitle ?? "Cal.com"}
|
|
||||||
description={props.subtitle ? props.subtitle?.toString() : ""}
|
|
||||||
nextSeoProps={{
|
|
||||||
nofollow: true,
|
|
||||||
noindex: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Toaster position="bottom-right" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames("flex h-screen overflow-hidden", props.large ? "bg-white" : "bg-gray-100")}
|
|
||||||
data-testid="dashboard-shell">
|
|
||||||
{status === "authenticated" && (
|
|
||||||
<div style={isEmbed ? { display: "none" } : {}} className="hidden md:flex lg:flex-shrink-0">
|
|
||||||
<div className="flex w-14 flex-col lg:w-56">
|
|
||||||
<div className="flex h-0 flex-1 flex-col border-r border-gray-200 bg-white">
|
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
|
|
||||||
<div className="items-center justify-between md:hidden lg:flex">
|
|
||||||
<Link href="/event-types">
|
|
||||||
<a className="px-4">
|
|
||||||
<Logo small />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<div className="px-4">
|
|
||||||
<KBarTrigger />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* logo icon for tablet */}
|
|
||||||
<Link href="/event-types">
|
|
||||||
<a className="text-center md:inline lg:hidden">
|
|
||||||
<Logo small icon />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
|
|
||||||
{navigation.map((item) =>
|
|
||||||
!item ? null : (
|
|
||||||
<Fragment key={item.name}>
|
|
||||||
<Link href={item.href}>
|
|
||||||
<a
|
|
||||||
aria-label={item.name}
|
|
||||||
className={classNames(
|
|
||||||
item.current
|
|
||||||
? "bg-neutral-100 text-neutral-900"
|
|
||||||
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
|
|
||||||
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
|
|
||||||
)}>
|
|
||||||
<item.icon
|
|
||||||
className={classNames(
|
|
||||||
item.current
|
|
||||||
? "text-neutral-500"
|
|
||||||
: "text-neutral-400 group-hover:text-neutral-500",
|
|
||||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="hidden lg:inline">{item.name}</span>
|
|
||||||
{item.pro && (
|
|
||||||
<span className="ml-1">
|
|
||||||
{plan === "FREE" && <Badge variant="default">PRO</Badge>}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
{item.child &&
|
|
||||||
router.asPath.startsWith(item.href) &&
|
|
||||||
item.child.map((item) => {
|
|
||||||
return (
|
|
||||||
<Link key={item.name} href={item.href}>
|
|
||||||
<a
|
|
||||||
className={classNames(
|
|
||||||
item.current
|
|
||||||
? "text-neutral-900"
|
|
||||||
: "text-neutral-500 hover:text-neutral-900",
|
|
||||||
"group hidden items-center rounded-sm px-2 py-2 pl-10 text-sm font-medium lg:flex"
|
|
||||||
)}>
|
|
||||||
<span className="hidden lg:inline">{item.name}</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<span className="group flex items-center rounded-sm px-2 py-2 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
|
|
||||||
<KBarTrigger />
|
|
||||||
</span>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<TrialBanner />
|
|
||||||
<div
|
|
||||||
className="rounded-sm pt-2 pb-2 pl-3 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2"
|
|
||||||
data-testid="user-dropdown-trigger">
|
|
||||||
<span className="hidden lg:inline">
|
|
||||||
<UserDropdown />
|
|
||||||
</span>
|
|
||||||
<span className="hidden md:inline lg:hidden">
|
|
||||||
<UserDropdown small />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
|
|
||||||
© {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"}
|
|
||||||
<span className="lowercase" data-testid={`plan-${plan?.toLowerCase()}`}>
|
|
||||||
-{plan}
|
|
||||||
</span>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex w-0 flex-1 flex-col overflow-hidden">
|
|
||||||
<ImpersonatingBanner />
|
|
||||||
<main
|
|
||||||
className={classNames(
|
|
||||||
"relative z-0 flex-1 overflow-y-auto focus:outline-none",
|
|
||||||
status === "authenticated" && "max-w-[1700px]",
|
|
||||||
props.flexChildrenContainer && "flex flex-col"
|
|
||||||
)}>
|
|
||||||
{/* show top navigation for md and smaller (tablet and phones) */}
|
|
||||||
{status === "authenticated" && (
|
|
||||||
<nav
|
|
||||||
style={isEmbed ? { display: "none" } : {}}
|
|
||||||
className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
|
|
||||||
<Link href="/event-types">
|
|
||||||
<a>
|
|
||||||
<Logo />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-2 self-center">
|
|
||||||
<span className="group flex items-center rounded-full p-2.5 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
|
|
||||||
<KBarTrigger />
|
|
||||||
</span>
|
|
||||||
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
|
||||||
<span className="sr-only">{t("settings")}</span>
|
|
||||||
<Link href="/settings/profile">
|
|
||||||
<a>
|
|
||||||
<CogIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</button>
|
|
||||||
<UserDropdown small />
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
props.centered && "mx-auto md:max-w-5xl",
|
|
||||||
props.flexChildrenContainer && "flex flex-1 flex-col",
|
|
||||||
!props.large && "py-8"
|
|
||||||
)}>
|
|
||||||
{!!props.backPath && (
|
|
||||||
<div className="mx-3 mb-8 sm:mx-8">
|
|
||||||
<Button
|
|
||||||
onClick={() => router.push(props.backPath as string)}
|
|
||||||
StartIcon={ArrowLeftIcon}
|
|
||||||
color="secondary">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{props.heading && (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
props.large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7",
|
|
||||||
"block justify-between px-4 sm:flex sm:px-6 md:px-8"
|
|
||||||
)}>
|
|
||||||
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
|
||||||
<div className="mb-8 w-full">
|
|
||||||
{props.isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200" />
|
|
||||||
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
|
|
||||||
{props.heading}
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{props.CTA && <div className="mb-4 flex-shrink-0">{props.CTA}</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"px-4 sm:px-6 md:px-8",
|
|
||||||
props.flexChildrenContainer && "flex flex-1 flex-col"
|
|
||||||
)}>
|
|
||||||
<ErrorBoundary>{!props.isLoading ? props.children : props.customLoader}</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
{/* show bottom navigation for md and smaller (tablet and phones) */}
|
|
||||||
{status === "authenticated" && (
|
|
||||||
<nav
|
|
||||||
style={isEmbed ? { display: "none" } : {}}
|
|
||||||
className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
|
|
||||||
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
|
|
||||||
{navigation.flatMap((item, itemIdx) => {
|
|
||||||
if (!item) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return item.href === "/settings/profile" ? (
|
|
||||||
[]
|
|
||||||
) : (
|
|
||||||
<Link key={item.name} href={item.href}>
|
|
||||||
<a
|
|
||||||
className={classNames(
|
|
||||||
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
|
|
||||||
itemIdx === 0 ? "rounded-l-lg" : "",
|
|
||||||
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
|
|
||||||
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
|
|
||||||
)}
|
|
||||||
aria-current={item.current ? "page" : undefined}>
|
|
||||||
<item.icon
|
|
||||||
className={classNames(
|
|
||||||
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
|
||||||
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="block truncate">{item.name}</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
{/* add padding to content for mobile navigation*/}
|
|
||||||
<div className="block pt-12 md:hidden" />
|
|
||||||
</div>
|
|
||||||
<LicenseBanner />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemoizedLayout = React.memo(Layout);
|
|
||||||
|
|
||||||
type LayoutProps = {
|
|
||||||
centered?: boolean;
|
|
||||||
title?: string;
|
|
||||||
heading?: ReactNode;
|
|
||||||
subtitle?: ReactNode;
|
|
||||||
children: ReactNode;
|
|
||||||
CTA?: ReactNode;
|
|
||||||
large?: boolean;
|
|
||||||
HeadingLeftIcon?: ReactNode;
|
|
||||||
backPath?: string; // renders back button to specified path
|
|
||||||
// use when content needs to expand with flex
|
|
||||||
flexChildrenContainer?: boolean;
|
|
||||||
isPublic?: boolean;
|
|
||||||
customLoader?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Shell(props: LayoutProps) {
|
|
||||||
const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
|
|
||||||
const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
|
|
||||||
|
|
||||||
const query = useMeQuery();
|
|
||||||
const user = query.data;
|
|
||||||
|
|
||||||
const i18n = useViewerI18n();
|
|
||||||
const { status } = useSession();
|
|
||||||
|
|
||||||
const isLoading = isRedirectingToOnboarding || loading;
|
|
||||||
|
|
||||||
// Don't show any content till translations are loaded.
|
|
||||||
// As they are cached infintely, this status would be loading just once for the app's lifetime until refresh
|
|
||||||
if (i18n.status === "loading") {
|
|
||||||
return (
|
|
||||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session && !props.isPublic) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<KBarRoot>
|
|
||||||
<CustomBranding lightVal={user?.brandColor} darkVal={user?.darkBrandColor} />
|
|
||||||
<MemoizedLayout plan={user?.plan} status={status} {...props} isLoading={isLoading} />
|
|
||||||
<KBarContent />
|
|
||||||
</KBarRoot>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserDropdown({ small }: { small?: boolean }) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = useMeQuery();
|
|
||||||
const user = query.data;
|
|
||||||
useEffect(() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
const Beacon = window.Beacon;
|
|
||||||
// window.Beacon is defined when user actually opens up HelpScout and username is available here. On every re-render update session info, so that it is always latest.
|
|
||||||
Beacon &&
|
|
||||||
Beacon("session-data", {
|
|
||||||
username: user?.username || "Unknown",
|
|
||||||
screenResolution: `${screen.width}x${screen.height}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const mutation = trpc.useMutation("viewer.away", {
|
|
||||||
onSettled() {
|
|
||||||
utils.invalidateQueries("viewer.me");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const [helpOpen, setHelpOpen] = useState(false);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const onHelpItemSelect = () => {
|
|
||||||
setHelpOpen(false);
|
|
||||||
setMenuOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prevent rendering dropdown if user isn't available.
|
|
||||||
// We don't want to show nameless user.
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Dropdown open={menuOpen} onOpenChange={() => setHelpOpen(false)}>
|
|
||||||
<DropdownMenuTrigger asChild onClick={() => setMenuOpen(true)}>
|
|
||||||
<button className="group flex w-full cursor-pointer appearance-none items-center text-left">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
small ? "h-8 w-8" : "h-10 w-10",
|
|
||||||
"relative flex-shrink-0 rounded-full bg-gray-300 ltr:mr-3 rtl:ml-3"
|
|
||||||
)}>
|
|
||||||
{
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
className="rounded-full"
|
|
||||||
src={WEBAPP_URL + "/" + user.username + "/avatar.png"}
|
|
||||||
alt={user.username || "Nameless User"}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{!user.away && (
|
|
||||||
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500" />
|
|
||||||
)}
|
|
||||||
{user.away && (
|
|
||||||
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{!small && (
|
|
||||||
<span className="flex flex-grow items-center truncate">
|
|
||||||
<span className="flex-grow truncate text-sm">
|
|
||||||
<span className="block truncate font-medium text-gray-900">
|
|
||||||
{user.name || "Nameless User"}
|
|
||||||
</span>
|
|
||||||
<span className="block truncate font-normal text-neutral-500">
|
|
||||||
{user.username
|
|
||||||
? process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
|
|
||||||
? `cal.com/${user.username}`
|
|
||||||
: `/${user.username}`
|
|
||||||
: "No public page"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<SelectorIcon
|
|
||||||
className="h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent portalled={true} onInteractOutside={() => setMenuOpen(false)}>
|
|
||||||
{helpOpen ? (
|
|
||||||
<HelpMenuItem onHelpItemSelect={() => onHelpItemSelect()} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
|
||||||
mutation.mutate({ away: !user?.away });
|
|
||||||
utils.invalidateQueries("viewer.me");
|
|
||||||
}}
|
|
||||||
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
|
||||||
<MoonIcon
|
|
||||||
className={classNames(
|
|
||||||
user.away
|
|
||||||
? "text-purple-500 group-hover:text-purple-700"
|
|
||||||
: "text-gray-500 group-hover:text-gray-700",
|
|
||||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{user.away ? t("set_as_free") : t("set_as_away")}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
|
||||||
{user.username && (
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
|
|
||||||
className="flex items-center px-4 py-2 text-sm text-gray-700">
|
|
||||||
<ExternalLinkIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" />{" "}
|
|
||||||
{t("view_public_page")}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
href={JOIN_SLACK}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 2447.6 2452.5"
|
|
||||||
className={classNames(
|
|
||||||
"text-gray-500 group-hover:text-gray-700",
|
|
||||||
"mt-0.5 h-4 w-4 flex-shrink-0 ltr:mr-4 rtl:ml-4"
|
|
||||||
)}
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clipRule="evenodd" fillRule="evenodd">
|
|
||||||
<path
|
|
||||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
{t("join_our_slack")}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href={ROADMAP}
|
|
||||||
className="flex items-center px-4 py-2 text-sm text-gray-700">
|
|
||||||
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="flex w-full px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
onClick={() => setHelpOpen(true)}>
|
|
||||||
<QuestionMarkCircleIcon
|
|
||||||
className={classNames(
|
|
||||||
"text-gray-500 group-hover:text-neutral-500",
|
|
||||||
"h-5 w-5 flex-shrink-0 ltr:mr-3"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{t("help")}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<a
|
|
||||||
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
|
|
||||||
className="flex cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
|
||||||
<LogoutIcon
|
|
||||||
className={classNames(
|
|
||||||
"text-gray-500 group-hover:text-gray-700",
|
|
||||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{t("sign_out")}
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,16 +11,7 @@ export default function AllApps({ apps }: { apps: App[] }) {
|
|||||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
|
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
|
||||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
{apps.map((app) => (
|
{apps.map((app) => (
|
||||||
<AppCard
|
<AppCard key={app.name} app={app} />
|
||||||
key={app.name}
|
|
||||||
name={app.name}
|
|
||||||
slug={app.slug}
|
|
||||||
description={app.description}
|
|
||||||
logo={app.logo}
|
|
||||||
rating={app.rating}
|
|
||||||
reviews={app.reviews}
|
|
||||||
isProOnly={app.isProOnly}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { App } from "@calcom/types/App";
|
||||||
|
import Badge from "@calcom/ui/Badge";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import Badge from "@components/ui/Badge";
|
|
||||||
|
|
||||||
interface AppCardProps {
|
interface AppCardProps {
|
||||||
logo: string;
|
app: App;
|
||||||
name: string;
|
|
||||||
slug?: string;
|
|
||||||
category?: string;
|
|
||||||
description: string;
|
|
||||||
rating: number;
|
|
||||||
reviews?: number;
|
|
||||||
isProOnly?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AppCard(props: AppCardProps) {
|
export default function AppCard(props: AppCardProps) {
|
||||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||||
return (
|
return (
|
||||||
<Link href={"/apps/" + props.slug}>
|
<Link href={"/apps/" + props.app.slug}>
|
||||||
<a
|
<a
|
||||||
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
|
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
|
||||||
data-testid={`app-store-app-card-${props.slug}`}>
|
data-testid={`app-store-app-card-${props.app.slug}`}>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img src={props.logo} alt={props.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
<img src={props.app.logo} alt={props.app.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||||
}
|
}
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -38,18 +31,13 @@ export default function AppCard(props: AppCardProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h3 className="font-medium">{props.name}</h3>
|
<h3 className="font-medium">{props.app.name}</h3>
|
||||||
{props.isProOnly && user?.plan === "FREE" ? (
|
|
||||||
<Badge className="ml-2" variant="default">
|
|
||||||
PRO
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
||||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
||||||
</div> */}
|
</div> */}
|
||||||
<p className="mt-2 truncate text-sm text-gray-500">{props.description}</p>
|
<p className="mt-2 truncate text-sm text-gray-500">{props.app.description}</p>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Glide, { Options } from "@glidejs/glide";
|
import Glide, { Options } from "@glidejs/glide";
|
||||||
import "@glidejs/glide/dist/css/glide.core.min.css";
|
import "@glidejs/glide/dist/css/glide.core.min.css";
|
||||||
import "@glidejs/glide/dist/css/glide.theme.min.css";
|
import "@glidejs/glide/dist/css/glide.theme.min.css";
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
|
||||||
const Slider = <T extends string | unknown>({
|
const Slider = <T extends string | unknown>({
|
||||||
title = "",
|
title = "",
|
||||||
className = "",
|
className = "",
|
||||||
@@ -51,10 +52,10 @@ const Slider = <T extends string | unknown>({
|
|||||||
)}
|
)}
|
||||||
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
||||||
<button data-glide-dir="<" className="mr-4">
|
<button data-glide-dir="<" className="mr-4">
|
||||||
<ArrowLeftIcon className="h-5 w-5 text-gray-600 hover:text-black" />
|
<Icon.FiArrowLeft className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||||
</button>
|
</button>
|
||||||
<button data-glide-dir=">">
|
<button data-glide-dir=">">
|
||||||
<ArrowRightIcon className="h-5 w-5 text-gray-600 hover:text-black" />
|
<Icon.FiArrowRight className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,20 +21,10 @@ const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
renderItem={(app) => (
|
renderItem={(app) => <AppCard app={app} />}
|
||||||
<AppCard
|
|
||||||
key={app.name}
|
|
||||||
name={app.name}
|
|
||||||
slug={app.slug}
|
|
||||||
description={app.description}
|
|
||||||
logo={app.logo}
|
|
||||||
rating={app.rating}
|
|
||||||
reviews={app.reviews}
|
|
||||||
isProOnly={app.isProOnly}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated use v2 instead, located at packages/ui/v2/core/apps */
|
||||||
export default TrendingAppsSlider;
|
export default TrendingAppsSlider;
|
||||||
@@ -2,11 +2,11 @@ import { signIn } from "next-auth/react";
|
|||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import React, { useEffect, useState } from "react";
|
|||||||
import useDigitInput from "react-digit-input";
|
import useDigitInput from "react-digit-input";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { Input } from "@calcom/ui/form/fields";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Label, Input } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
export default function TwoFactor({ center = true }) {
|
||||||
|
|
||||||
export default function TwoFactor() {
|
|
||||||
const [value, onChange] = useState("");
|
const [value, onChange] = useState("");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const methods = useFormContext();
|
const methods = useFormContext();
|
||||||
@@ -26,7 +25,9 @@ export default function TwoFactor() {
|
|||||||
const className = "h-12 w-12 !text-xl text-center";
|
const className = "h-12 w-12 !text-xl text-center";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto !mt-0 max-w-sm">
|
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||||
|
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||||
|
|
||||||
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
|
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
|
||||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PlusIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@@ -7,6 +6,7 @@ import showToast from "@calcom/lib/notification";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
import { Form } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
@@ -41,7 +41,7 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
|||||||
return (
|
return (
|
||||||
<Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
|
<Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button data-testid={name} StartIcon={PlusIcon}>
|
<Button data-testid={name} StartIcon={Icon.FiPlus}>
|
||||||
{t("new_schedule_btn")}
|
{t("new_schedule_btn")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
/**
|
||||||
import { DuplicateIcon } from "@heroicons/react/solid";
|
* @deprecated
|
||||||
|
* use Component in "/packages/features/schedules/components/Schedule";
|
||||||
|
**/
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
@@ -8,7 +10,8 @@ import { GroupBase, Props } from "react-select";
|
|||||||
import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs";
|
import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import Dropdown, { DropdownMenuContent } from "@calcom/ui/Dropdown";
|
import Dropdown, { DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
|
|
||||||
import { defaultDayRange } from "@lib/availability";
|
import { defaultDayRange } from "@lib/availability";
|
||||||
@@ -193,6 +196,10 @@ const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (select
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use Component in "/packages/features/schedules/components/Schedule";
|
||||||
|
**/
|
||||||
export const DayRanges = ({
|
export const DayRanges = ({
|
||||||
name,
|
name,
|
||||||
defaultValue = [defaultDayRange],
|
defaultValue = [defaultDayRange],
|
||||||
@@ -231,13 +238,13 @@ export const DayRanges = ({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div key={field.id} className="flex items-center rtl:space-x-reverse">
|
<div key={field.id} className="flex items-center rtl:space-x-reverse">
|
||||||
<div className="flex flex-grow sm:flex-grow-0">
|
<div className="flex flex-grow space-x-1 sm:flex-grow-0">
|
||||||
<TimeRangeField name={`${name}.${index}`} />
|
<TimeRangeField name={`${name}.${index}`} />
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
StartIcon={TrashIcon}
|
StartIcon={Icon.FiTrash}
|
||||||
type="button"
|
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -249,19 +256,21 @@ export const DayRanges = ({
|
|||||||
type="button"
|
type="button"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
size="icon"
|
size="icon"
|
||||||
StartIcon={PlusIcon}
|
StartIcon={Icon.FiPlus}
|
||||||
onClick={handleAppend}
|
onClick={handleAppend}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<Tooltip content={t("duplicate") as string}>
|
<Tooltip content={t("duplicate") as string}>
|
||||||
<Button
|
<DropdownMenuTrigger asChild>
|
||||||
type="button"
|
<Button
|
||||||
color="minimal"
|
type="button"
|
||||||
size="icon"
|
color="minimal"
|
||||||
StartIcon={DuplicateIcon}
|
size="icon"
|
||||||
onClick={handleAppend}
|
StartIcon={Icon.FiCopy}
|
||||||
/>
|
onClick={handleAppend}
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<CopyTimes
|
<CopyTimes
|
||||||
@@ -323,6 +332,10 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use Component in "/packages/features/schedules/components/Schedule";
|
||||||
|
**/
|
||||||
const Schedule = ({ name }: { name: string }) => {
|
const Schedule = ({ name }: { name: string }) => {
|
||||||
const { i18n } = useLocale();
|
const { i18n } = useLocale();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
|
/**
|
||||||
|
* @deprecated modifications to this file should be v2 only
|
||||||
|
* Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead
|
||||||
|
*/
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
@@ -8,7 +11,12 @@ import { Availability } from "@calcom/prisma/client";
|
|||||||
import { inferQueryOutput } from "@calcom/trpc/react";
|
import { inferQueryOutput } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated modifications to this file should be v2 only
|
||||||
|
* Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead
|
||||||
|
*/
|
||||||
export function ScheduleListItem({
|
export function ScheduleListItem({
|
||||||
schedule,
|
schedule,
|
||||||
deleteFunction,
|
deleteFunction,
|
||||||
@@ -37,7 +45,7 @@ export function ScheduleListItem({
|
|||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-neutral-500">
|
||||||
{schedule.availability.map((availability: Availability) => (
|
{schedule.availability.map((availability: Availability) => (
|
||||||
<Fragment key={availability.id}>
|
<Fragment key={availability.id}>
|
||||||
{availabilityAsString(availability, i18n.language)}
|
{availabilityAsString(availability, { locale: i18n.language })}
|
||||||
<br />
|
<br />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
@@ -45,9 +53,10 @@ export function ScheduleListItem({
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
|
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
|
||||||
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
|
<Icon.FiMoreHorizontal className="h-5 w-5 group-hover:text-gray-800" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
@@ -61,7 +70,7 @@ export function ScheduleListItem({
|
|||||||
type="button"
|
type="button"
|
||||||
color="warn"
|
color="warn"
|
||||||
className="w-full font-normal"
|
className="w-full font-normal"
|
||||||
StartIcon={isDeleting ? undefined : TrashIcon}
|
StartIcon={isDeleting ? undefined : Icon.FiTrash}
|
||||||
loading={isDeleting}>
|
loading={isDeleting}>
|
||||||
{isDeleting ? t("deleting") : t("delete_schedule")}
|
{isDeleting ? t("deleting") : t("delete_schedule")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import React from "react";
|
|||||||
|
|
||||||
import { SkeletonText } from "@calcom/ui";
|
import { SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
|
/** @deprecated Use `apps/web/components/v2/availability/SkeletonLoader.tsx` */
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
|
||||||
|
import { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
|
return locations.length ? (
|
||||||
|
<div>
|
||||||
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||||
|
{locations.map((location) => {
|
||||||
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
|
if (!eventLocationType) {
|
||||||
|
// It's possible that the location app got uninstalled
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||||
|
<img
|
||||||
|
src={eventLocationType.iconUrl}
|
||||||
|
className={classNames(
|
||||||
|
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
||||||
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||||
|
)}
|
||||||
|
alt={`${eventLocationType.label} icon`}
|
||||||
|
/>
|
||||||
|
<span key={location.type}>{locationKeyToString(location)} </span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
@@ -18,13 +17,10 @@ type AvailableTimesProps = {
|
|||||||
recurringCount: number | undefined;
|
recurringCount: number | undefined;
|
||||||
eventTypeSlug: string;
|
eventTypeSlug: string;
|
||||||
date: Dayjs;
|
date: Dayjs;
|
||||||
users: {
|
|
||||||
username: string | null;
|
|
||||||
}[];
|
|
||||||
schedulingType: SchedulingType | null;
|
|
||||||
seatsPerTimeSlot?: number | null;
|
seatsPerTimeSlot?: number | null;
|
||||||
slots?: Slot[];
|
slots?: Slot[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
ethSignature?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
@@ -35,8 +31,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
eventTypeSlug,
|
eventTypeSlug,
|
||||||
recurringCount,
|
recurringCount,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
schedulingType,
|
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
|
ethSignature,
|
||||||
}) => {
|
}) => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -49,17 +45,17 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8 flex flex-col text-center sm:mt-0 sm:w-1/3 sm:pl-4 md:-mb-5">
|
<div className="dark:bg-darkgray-100 mt-8 flex flex-col px-4 text-center sm:mt-0 sm:w-1/3 sm:p-5 md:-mb-5">
|
||||||
<div className="mb-4 text-left text-lg font-light text-gray-600">
|
<div className="mb-4 text-left text-base">
|
||||||
<span className="text-bookingdarker w-1/2 dark:text-white">
|
<span className="text-bookingdarker dark:text-darkgray-800 mb-8 w-1/2 break-words font-semibold text-gray-900">
|
||||||
<strong>{nameOfDay(i18n.language, Number(date.format("d")))}</strong>
|
{nameOfDay(i18n.language, Number(date.format("d")))}
|
||||||
<span className="text-bookinglight">
|
</span>
|
||||||
{date.format(", D ")}
|
<span className="text-bookinglight font-medium">
|
||||||
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
|
{date.format(", D ")}
|
||||||
</span>
|
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid flex-grow grid-cols-2 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
<div className="grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||||
{slots.length > 0 &&
|
{slots.length > 0 &&
|
||||||
slots.map((slot) => {
|
slots.map((slot) => {
|
||||||
type BookingURL = {
|
type BookingURL = {
|
||||||
@@ -75,6 +71,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
slug: eventTypeSlug,
|
slug: eventTypeSlug,
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
|
ethSignature,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,10 +79,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schedulingType === SchedulingType.ROUND_ROBIN) {
|
|
||||||
bookingUrl.query.user = slot.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If event already has an attendee add booking id
|
// If event already has an attendee add booking id
|
||||||
if (slot.bookingUid) {
|
if (slot.bookingUid) {
|
||||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||||
@@ -97,8 +90,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-primary-500 mb-2 block rounded-sm border bg-white py-4 font-medium opacity-25 dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 ",
|
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
@@ -107,8 +100,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
<Link href={bookingUrl} prefetch={false}>
|
<Link href={bookingUrl} prefetch={false}>
|
||||||
<a
|
<a
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
||||||
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
data-testid="time">
|
data-testid="time">
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
|||||||
@@ -1,33 +1,22 @@
|
|||||||
import {
|
|
||||||
BanIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ClockIcon,
|
|
||||||
LocationMarkerIcon,
|
|
||||||
PaperAirplaneIcon,
|
|
||||||
PencilAltIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "@heroicons/react/outline";
|
|
||||||
import { RefreshIcon } from "@heroicons/react/solid";
|
|
||||||
import { BookingStatus } from "@prisma/client";
|
import { BookingStatus } from "@prisma/client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "react-query";
|
|
||||||
|
|
||||||
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
|
import Badge from "@calcom/ui/v2/core/Badge";
|
||||||
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
import { linkValueToString } from "@lib/linkValueToString";
|
|
||||||
import { LocationType } from "@lib/location";
|
|
||||||
import { extractRecurringDates } from "@lib/parseDate";
|
import { extractRecurringDates } from "@lib/parseDate";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
@@ -52,40 +41,31 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||||
const mutation = useMutation(
|
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
|
||||||
async (confirm: boolean) => {
|
onSuccess: () => {
|
||||||
let body = {
|
|
||||||
id: booking.id,
|
|
||||||
confirmed: confirm,
|
|
||||||
language: i18n.language,
|
|
||||||
reason: rejectionReason,
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Only pass down the recurring event id when we need to confirm the entire series, which happens in
|
|
||||||
* the "Recurring" tab, to support confirming discretionally in the "Recurring" tab.
|
|
||||||
*/
|
|
||||||
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
|
|
||||||
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
|
|
||||||
}
|
|
||||||
const res = await fetch("/api/book/confirm", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new HttpError({ statusCode: res.status });
|
|
||||||
}
|
|
||||||
setRejectionDialogIsOpen(false);
|
setRejectionDialogIsOpen(false);
|
||||||
|
utils.invalidateQueries("viewer.bookings");
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
async onSettled() {
|
|
||||||
await utils.invalidateQueries(["viewer.bookings"]);
|
const bookingConfirm = async (confirm: boolean) => {
|
||||||
},
|
let body = {
|
||||||
|
bookingId: booking.id,
|
||||||
|
confirmed: confirm,
|
||||||
|
reason: rejectionReason,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Only pass down the recurring event id when we need to confirm the entire series, which happens in
|
||||||
|
* the "Recurring" tab, to support confirming discretionally in the "Recurring" tab.
|
||||||
|
*/
|
||||||
|
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
|
||||||
|
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
|
||||||
}
|
}
|
||||||
);
|
mutation.mutate(body);
|
||||||
|
};
|
||||||
|
|
||||||
const isUpcoming = new Date(booking.endTime) >= new Date();
|
const isUpcoming = new Date(booking.endTime) >= new Date();
|
||||||
|
const isPast = new Date(booking.endTime) < new Date();
|
||||||
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
||||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||||
const isRejected = booking.status === BookingStatus.REJECTED;
|
const isRejected = booking.status === BookingStatus.REJECTED;
|
||||||
@@ -101,7 +81,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: BanIcon,
|
icon: Icon.FiSlash,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -111,9 +91,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
? t("confirm_all")
|
? t("confirm_all")
|
||||||
: t("confirm"),
|
: t("confirm"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate(true);
|
bookingConfirm(true);
|
||||||
},
|
},
|
||||||
icon: CheckIcon,
|
icon: Icon.FiCheck,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
color: "primary",
|
color: "primary",
|
||||||
},
|
},
|
||||||
@@ -133,23 +113,22 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
? "?allRemainingBookings=true"
|
? "?allRemainingBookings=true"
|
||||||
: ""
|
: ""
|
||||||
}`,
|
}`,
|
||||||
icon: XIcon,
|
icon: Icon.FiX,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "edit_booking",
|
id: "edit_booking",
|
||||||
label: t("edit_booking"),
|
label: t("edit"),
|
||||||
icon: PencilAltIcon,
|
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: "reschedule",
|
id: "reschedule",
|
||||||
icon: ClockIcon,
|
icon: Icon.FiClock,
|
||||||
label: t("reschedule_booking"),
|
label: t("reschedule_booking"),
|
||||||
href: `/reschedule/${booking.uid}`,
|
href: `/reschedule/${booking.uid}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule_request",
|
id: "reschedule_request",
|
||||||
icon: PaperAirplaneIcon,
|
icon: Icon.FiSend,
|
||||||
iconClassName: "rotate-45 w-[18px] -ml-[2px]",
|
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||||
label: t("send_reschedule_request"),
|
label: t("send_reschedule_request"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpenRescheduleDialog(true);
|
setIsOpenRescheduleDialog(true);
|
||||||
@@ -161,7 +140,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpenLocationDialog(true);
|
setIsOpenLocationDialog(true);
|
||||||
},
|
},
|
||||||
icon: LocationMarkerIcon,
|
icon: Icon.FiMapPin,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -174,7 +153,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||||
<PaperAirplaneIcon className="-mt-[1px] w-4 rotate-45" />
|
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -191,13 +170,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveLocation = (newLocationType: LocationType, details: { [key: string]: string }) => {
|
const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => {
|
||||||
let newLocation = newLocationType as string;
|
let newLocation = newLocationType as string;
|
||||||
if (
|
const eventLocationType = getEventLocationType(newLocationType);
|
||||||
newLocationType === LocationType.InPerson ||
|
if (eventLocationType?.organizerInputType) {
|
||||||
newLocationType === LocationType.Link ||
|
|
||||||
newLocationType === LocationType.UserPhone
|
|
||||||
) {
|
|
||||||
newLocation = details[Object.keys(details)[0]];
|
newLocation = details[Object.keys(details)[0]];
|
||||||
}
|
}
|
||||||
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
||||||
@@ -206,31 +182,16 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
// Calculate the booking date(s) and setup recurring event data to show
|
// Calculate the booking date(s) and setup recurring event data to show
|
||||||
let recurringStrings: string[] = [];
|
let recurringStrings: string[] = [];
|
||||||
let recurringDates: Date[] = [];
|
let recurringDates: Date[] = [];
|
||||||
const today = new Date();
|
|
||||||
if (booking.recurringBookings && booking.eventType.recurringEvent?.freq !== undefined) {
|
if (booking.recurringBookings && booking.eventType.recurringEvent?.freq !== undefined) {
|
||||||
[recurringStrings, recurringDates] = extractRecurringDates(
|
[recurringStrings, recurringDates] = extractRecurringDates(
|
||||||
booking.recurringBookings,
|
booking.recurringBookings,
|
||||||
user?.timeZone,
|
user?.timeZone,
|
||||||
i18n
|
i18n
|
||||||
);
|
);
|
||||||
if (booking.status === BookingStatus.PENDING) {
|
|
||||||
// Only take into consideration next up instances if booking is confirmed
|
|
||||||
recurringDates = recurringDates.filter((aDate) => aDate >= today);
|
|
||||||
recurringStrings = recurringDates.map((_, key) => recurringStrings[key]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = booking.location || "";
|
const location = booking.location || "";
|
||||||
|
|
||||||
if (location.includes("integration")) {
|
|
||||||
if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
|
|
||||||
location = t("web_conference");
|
|
||||||
} else if (isConfirmed) {
|
|
||||||
location = linkValueToString(booking.location, t);
|
|
||||||
} else {
|
|
||||||
location = t("web_conferencing_details_to_follow");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -292,7 +253,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutation.mutate(false);
|
bookingConfirm(false);
|
||||||
}}>
|
}}>
|
||||||
{t("rejection_confirmation")}
|
{t("rejection_confirmation")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -300,87 +261,103 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="flex hover:bg-neutral-50">
|
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||||
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-64" onClick={onClick}>
|
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-48" onClick={onClick}>
|
||||||
<div className="cursor-pointer py-4">
|
<div className="cursor-pointer py-4">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||||
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
{booking.recurringBookings &&
|
{isPending && (
|
||||||
booking.eventType?.recurringEvent?.freq &&
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||||
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
|
{t("unconfirmed")}
|
||||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
</Badge>
|
||||||
<div className="flex">
|
)}
|
||||||
<Tooltip
|
|
||||||
content={recurringStrings.map((aDate, key) => (
|
{booking.eventType?.team && (
|
||||||
<p key={key}>{aDate}</p>
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||||
))}>
|
{booking.eventType.team.name}
|
||||||
<div className="text-gray-600 dark:text-white">
|
</Badge>
|
||||||
<RefreshIcon className="float-left mr-1 mt-[2px] inline-block h-4 w-4 text-gray-400" />
|
)}
|
||||||
<p className="pl-[21px]">
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
{booking.status === BookingStatus.ACCEPTED
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||||
? `${t("event_remaining", {
|
{t("pending_payment")}
|
||||||
count: recurringDates.length,
|
</Badge>
|
||||||
})}`
|
)}
|
||||||
: getEveryFreqFor({
|
|
||||||
t,
|
<div className="mt-2 text-sm text-gray-400">
|
||||||
recurringEvent: booking.eventType.recurringEvent,
|
<RecurringBookingsTooltip
|
||||||
recurringCount: booking.recurringBookings.length,
|
booking={booking}
|
||||||
})}
|
recurringStrings={recurringStrings}
|
||||||
</p>
|
recurringDates={recurringDates}
|
||||||
</div>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={"flex-1 ltr:pl-4 rtl:pr-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
|
<td className={"flex-1 px-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
|
||||||
<div className="cursor-pointer py-4">
|
{/* Time and Badges for mobile */}
|
||||||
<div className="sm:hidden">
|
<div className="w-full pt-4 pb-2 sm:hidden">
|
||||||
{isPending && <Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>}
|
<div className="flex w-full items-center justify-between sm:hidden">
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
|
<div className="pr-2 text-sm text-gray-500">
|
||||||
)}
|
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||||
<div className="text-sm font-medium text-gray-900">
|
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||||
{startTime}:{" "}
|
|
||||||
<small className="text-sm text-gray-500">
|
|
||||||
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isPending && (
|
||||||
|
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
|
||||||
|
{t("unconfirmed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{booking.eventType?.team && (
|
||||||
|
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="gray">
|
||||||
|
{booking.eventType.team.name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
|
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
|
||||||
|
{t("pending_payment")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<div className="text-sm text-gray-400 sm:hidden">
|
||||||
|
<RecurringBookingsTooltip
|
||||||
|
booking={booking}
|
||||||
|
recurringStrings={recurringStrings}
|
||||||
|
recurringDates={recurringDates}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cursor-pointer py-4">
|
||||||
<div
|
<div
|
||||||
title={booking.title}
|
title={booking.title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
|
"max-w-10/12 sm:max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
|
||||||
isCancelled ? "line-through" : ""
|
isCancelled ? "line-through" : ""
|
||||||
)}>
|
)}>
|
||||||
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
|
|
||||||
{booking.title}
|
{booking.title}
|
||||||
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
||||||
)}
|
)}
|
||||||
{isPending && <Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>}
|
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<div
|
||||||
className="max-w-52 md:max-w-96 truncate text-sm text-gray-500"
|
className="max-w-52 md:max-w-96 truncate text-sm text-gray-600"
|
||||||
title={booking.description}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{booking.attendees.length !== 0 && (
|
{booking.attendees.length !== 0 && (
|
||||||
<a
|
<DisplayAttendees
|
||||||
className="text-sm text-gray-900 hover:text-blue-500"
|
attendees={booking.attendees}
|
||||||
href={"mailto:" + booking.attendees[0].email}
|
user={booking.user}
|
||||||
onClick={(e) => e.stopPropagation()}>
|
currentEmail={user?.email}
|
||||||
{booking.attendees[0].email}
|
/>
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
{isCancelled && booking.rescheduled && (
|
{isCancelled && booking.rescheduled && (
|
||||||
<div className="mt-2 inline-block text-left text-sm md:hidden">
|
<div className="mt-2 inline-block text-left text-sm md:hidden">
|
||||||
@@ -388,9 +365,20 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end sm:hidden">
|
||||||
|
<div className="whitespace-nowrap pt-2 pb-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
|
||||||
|
{isUpcoming && !isCancelled ? (
|
||||||
|
<>
|
||||||
|
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
||||||
|
{isConfirmed && <TableActions actions={bookedActions} />}
|
||||||
|
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="hidden whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:block">
|
||||||
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
|
|
||||||
{isUpcoming && !isCancelled ? (
|
{isUpcoming && !isCancelled ? (
|
||||||
<>
|
<>
|
||||||
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
||||||
@@ -398,6 +386,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
{isCancelled && booking.rescheduled && (
|
{isCancelled && booking.rescheduled && (
|
||||||
<div className="hidden h-full items-center md:flex">
|
<div className="hidden h-full items-center md:flex">
|
||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
@@ -409,6 +398,138 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RecurringBookingsTooltipProps {
|
||||||
|
booking: BookingItemProps;
|
||||||
|
recurringStrings: string[];
|
||||||
|
recurringDates: Date[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecurringBookingsTooltip = ({
|
||||||
|
booking,
|
||||||
|
recurringStrings,
|
||||||
|
recurringDates,
|
||||||
|
}: RecurringBookingsTooltipProps) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
(booking.recurringBookings &&
|
||||||
|
booking.eventType?.recurringEvent?.freq &&
|
||||||
|
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
|
||||||
|
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||||
|
<div className="flex">
|
||||||
|
<Tooltip
|
||||||
|
content={recurringStrings.map((aDate, key) => (
|
||||||
|
<p key={key}>{aDate}</p>
|
||||||
|
))}>
|
||||||
|
<div className="text-gray-600 dark:text-white">
|
||||||
|
<Icon.FiRefreshCcw
|
||||||
|
strokeWidth="3"
|
||||||
|
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 pl-5 text-xs">
|
||||||
|
{booking.status === BookingStatus.ACCEPTED
|
||||||
|
? `${t("event_remaining", {
|
||||||
|
count: recurringDates.length,
|
||||||
|
})}`
|
||||||
|
: getEveryFreqFor({
|
||||||
|
t,
|
||||||
|
recurringEvent: booking.eventType.recurringEvent,
|
||||||
|
recurringCount: booking.recurringBookings.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UserProps {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FirstAttendee = ({
|
||||||
|
user,
|
||||||
|
currentEmail,
|
||||||
|
}: {
|
||||||
|
user: UserProps;
|
||||||
|
currentEmail: string | null | undefined;
|
||||||
|
}) => {
|
||||||
|
return user.email === currentEmail ? (
|
||||||
|
<div className="inline-block">You</div>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
key={user.email}
|
||||||
|
className=" hover:text-blue-500"
|
||||||
|
href={"mailto:" + user.email}
|
||||||
|
onClick={(e) => e.stopPropagation()}>
|
||||||
|
{user.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Attendee: React.FC<{ email: string; children: React.ReactNode }> = ({ email, children }) => {
|
||||||
|
return (
|
||||||
|
<a className=" hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AttendeeProps {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayAttendees = ({
|
||||||
|
attendees,
|
||||||
|
user,
|
||||||
|
currentEmail,
|
||||||
|
}: {
|
||||||
|
attendees: AttendeeProps[];
|
||||||
|
user: UserProps | null;
|
||||||
|
currentEmail: string | null | undefined;
|
||||||
|
}) => {
|
||||||
|
if (attendees.length === 1) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||||
|
<span> and </span>
|
||||||
|
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (attendees.length === 2) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||||
|
<span>, </span>
|
||||||
|
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
||||||
|
<div className="inline-block text-sm text-gray-900"> and </div>
|
||||||
|
<Attendee email={attendees[1].email}>{attendees[1].name}</Attendee>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-gray-900">
|
||||||
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||||
|
<span>, </span>
|
||||||
|
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
||||||
|
<span> & </span>
|
||||||
|
<Tooltip
|
||||||
|
content={attendees.slice(1).map((attendee, key) => (
|
||||||
|
<p key={key}>{attendee.name}</p>
|
||||||
|
))}>
|
||||||
|
<div className="inline-block">{attendees.length - 1} more</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { XIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Button } from "@calcom/ui/Button";
|
import { Button } from "@calcom/ui/Button";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -39,7 +38,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
{error && (
|
{error && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||||
<XIcon className="h-6 w-6 text-red-600" />
|
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
@@ -58,19 +57,23 @@ export default function CancelBooking(props: Props) {
|
|||||||
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
|
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className="flex rtl:space-x-reverse">
|
<div className=" flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
|
||||||
{!props.recurringEvent && (
|
{!props.recurringEvent && (
|
||||||
<div className="w-full">
|
<div className=" border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
|
||||||
<Button color="secondary" onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="border-0 sm:border"
|
||||||
|
onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
||||||
{t("reschedule_this")}
|
{t("reschedule_this")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full space-x-2 text-right">
|
<div className="mb-2 flex w-full space-x-2 text-right sm:mb-0 ">
|
||||||
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
|
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
|
||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
className="flex grow justify-center"
|
||||||
data-testid="cancel"
|
data-testid="cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import React from "react";
|
|||||||
|
|
||||||
import { SkeletonText } from "@calcom/ui";
|
import { SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
|
/** @deprecated Use `apps/web/components/v2/bookings/SkeletonLoader.tsx` */
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="mt-6 animate-pulse divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
@@ -16,7 +17,7 @@ export default SkeletonLoader;
|
|||||||
|
|
||||||
function SkeletonItem() {
|
function SkeletonItem() {
|
||||||
return (
|
return (
|
||||||
<li className="group flex w-full items-center justify-between px-2 py-4 sm:px-6">
|
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
|
||||||
<div className="flex-grow truncate text-sm">
|
<div className="flex-grow truncate text-sm">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Switch from "@calcom/ui/Switch";
|
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/form/TimezoneSelect";
|
||||||
|
import { Switch } from "@calcom/ui/v2";
|
||||||
import TimezoneSelect, { ITimezoneOption } from "@components/ui/form/TimezoneSelect";
|
|
||||||
|
|
||||||
import { is24h, timeZone } from "../../lib/clock";
|
import { is24h, timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
@@ -34,9 +33,9 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
|
|||||||
};
|
};
|
||||||
|
|
||||||
return selectedTimeZone !== "" ? (
|
return selectedTimeZone !== "" ? (
|
||||||
<div className="max-w-80 absolute z-10 w-full rounded-sm border border-gray-200 bg-white px-4 py-2 dark:border-0 dark:bg-gray-700">
|
<div className="max-w-80 dark:border-darkgray-300 dark:bg-darkgray-200 absolute z-10 w-full rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
||||||
<div className="mb-4 flex">
|
<div className="mb-4 flex">
|
||||||
<div className="font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<label className="ltl:mr-3 mr-2 align-text-top text-sm font-medium text-neutral-700 ltr:ml-3 rtl:mr-3 dark:text-white">
|
<label className="ltl:mr-3 mr-2 align-text-top text-sm font-medium text-neutral-700 ltr:ml-3 rtl:mr-3 dark:text-white">
|
||||||
{t("am_pm")}
|
{t("am_pm")}
|
||||||
@@ -53,7 +52,7 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
|
|||||||
id="timeZone"
|
id="timeZone"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 focus:ring-black sm:text-sm"
|
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import AvatarGroup, { AvatarGroupProps } from "@components/ui/AvatarGroup";
|
||||||
|
|
||||||
|
export const UserAvatars = ({
|
||||||
|
profile,
|
||||||
|
users,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
profile: { image: string | null; name?: string | null };
|
||||||
|
showMembers: boolean;
|
||||||
|
users: { username: string | null; name?: string | null }[];
|
||||||
|
} & Pick<AvatarGroupProps, "size" | "truncateAfter">) => {
|
||||||
|
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
|
||||||
|
return (
|
||||||
|
<AvatarGroup
|
||||||
|
border="border-2 dark:border-darkgray-100 border-white"
|
||||||
|
items={
|
||||||
|
[
|
||||||
|
{ image: profile.image, alt: profile.name, title: profile.name },
|
||||||
|
...(showMembers
|
||||||
|
? users.map((user) => ({
|
||||||
|
title: user.name,
|
||||||
|
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||||
|
alt: user.name || undefined,
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||||
|
}
|
||||||
|
size={props.size}
|
||||||
|
truncateAfter={props.truncateAfter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,92 +1,52 @@
|
|||||||
// Get router variables
|
// Get router variables
|
||||||
import {
|
|
||||||
ArrowLeftIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
ClipboardCheckIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
LocationMarkerIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
VideoCameraIcon,
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
import { EventType } from "@prisma/client";
|
import { EventType } from "@prisma/client";
|
||||||
|
import { SchedulingType } from "@prisma/client";
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
import { useContracts } from "contexts/contractsContext";
|
|
||||||
import { TFunction } from "next-i18next";
|
import { TFunction } from "next-i18next";
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
|
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useIsEmbed,
|
|
||||||
useEmbedStyles,
|
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
|
useEmbedStyles,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { CAL_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
|
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import DatePicker from "@calcom/ui/booker/DatePicker";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
// import { timeZone } from "@lib/clock";
|
// import { timeZone } from "@lib/clock";
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
|
||||||
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
|
||||||
|
|
||||||
|
import Gates, { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||||
import TimeOptions from "@components/booking/TimeOptions";
|
import TimeOptions from "@components/booking/TimeOptions";
|
||||||
|
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||||
|
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
|
||||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||||
|
|
||||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
import { AvailableEventLocations } from "../AvailableEventLocations";
|
||||||
type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
|
||||||
|
|
||||||
export const locationKeyToString = (location: LocationObject, t: TFunction) => {
|
|
||||||
switch (location.type) {
|
|
||||||
case LocationType.InPerson:
|
|
||||||
return location.address || "In Person"; // If disabled address won't exist on the object
|
|
||||||
case LocationType.Link:
|
|
||||||
return location.link || "Link"; // If disabled link won't exist on the object
|
|
||||||
case LocationType.Phone:
|
|
||||||
return t("your_number");
|
|
||||||
case LocationType.UserPhone:
|
|
||||||
return t("phone_call");
|
|
||||||
case LocationType.GoogleMeet:
|
|
||||||
return "Google Meet";
|
|
||||||
case LocationType.Zoom:
|
|
||||||
return "Zoom";
|
|
||||||
case LocationType.Daily:
|
|
||||||
return "Cal Video";
|
|
||||||
case LocationType.Jitsi:
|
|
||||||
return "Jitsi";
|
|
||||||
case LocationType.Huddle01:
|
|
||||||
return "Huddle Video";
|
|
||||||
case LocationType.Tandem:
|
|
||||||
return "Tandem";
|
|
||||||
case LocationType.Teams:
|
|
||||||
return "Microsoft Teams";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -97,7 +57,7 @@ const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col justify-end">
|
<div className="flex h-full flex-col justify-end">
|
||||||
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
|
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
|
||||||
<ArrowLeftIcon className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white" />
|
<Icon.FiArrowLeft className="dark:text-darkgray-600 h-4 w-4 text-black transition-opacity hover:cursor-pointer" />
|
||||||
<p className="sr-only">Go Back</p>
|
<p className="sr-only">Go Back</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,13 +66,17 @@ const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
|||||||
|
|
||||||
const useSlots = ({
|
const useSlots = ({
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
|
usernameList,
|
||||||
timeZone,
|
timeZone,
|
||||||
}: {
|
}: {
|
||||||
eventTypeId: number;
|
eventTypeId: number;
|
||||||
|
eventTypeSlug: string;
|
||||||
startTime?: Dayjs;
|
startTime?: Dayjs;
|
||||||
endTime?: Dayjs;
|
endTime?: Dayjs;
|
||||||
|
usernameList: string[];
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading, isIdle } = trpc.useQuery(
|
const { data, isLoading, isIdle } = trpc.useQuery(
|
||||||
@@ -120,6 +84,8 @@ const useSlots = ({
|
|||||||
"viewer.public.slots.getSchedule",
|
"viewer.public.slots.getSchedule",
|
||||||
{
|
{
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
usernameList,
|
||||||
startTime: startTime?.toISOString() || "",
|
startTime: startTime?.toISOString() || "",
|
||||||
endTime: endTime?.toISOString() || "",
|
endTime: endTime?.toISOString() || "",
|
||||||
timeZone,
|
timeZone,
|
||||||
@@ -127,7 +93,6 @@ const useSlots = ({
|
|||||||
],
|
],
|
||||||
{ enabled: !!startTime && !!endTime }
|
{ enabled: !!startTime && !!endTime }
|
||||||
);
|
);
|
||||||
|
|
||||||
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -145,15 +110,19 @@ const SlotPicker = ({
|
|||||||
timeFormat,
|
timeFormat,
|
||||||
timeZone,
|
timeZone,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
|
users,
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
weekStart = 0,
|
weekStart = 0,
|
||||||
|
ethSignature,
|
||||||
}: {
|
}: {
|
||||||
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
||||||
timeFormat: string;
|
timeFormat: string;
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
seatsPerTimeSlot?: number;
|
seatsPerTimeSlot?: number;
|
||||||
recurringEventCount?: number;
|
recurringEventCount?: number;
|
||||||
|
users: string[];
|
||||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
ethSignature?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
@@ -185,26 +154,32 @@ const SlotPicker = ({
|
|||||||
const { i18n, isLocaleReady } = useLocale();
|
const { i18n, isLocaleReady } = useLocale();
|
||||||
const { slots: _1 } = useSlots({
|
const { slots: _1 } = useSlots({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
startTime: selectedDate?.startOf("day"),
|
startTime: selectedDate?.startOf("day"),
|
||||||
endTime: selectedDate?.endOf("day"),
|
endTime: selectedDate?.endOf("day"),
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
const { slots: _2, isLoading } = useSlots({
|
const { slots: _2, isLoading } = useSlots({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
startTime: browsingDate?.startOf("month"),
|
startTime: browsingDate?.startOf("month"),
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useMemo(() => ({ ..._1, ..._2 }), [_1, _2]);
|
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-8 w-full sm:mt-0 sm:min-w-[455px]",
|
"mt-8 w-full px-4 sm:mt-0 sm:min-w-[455px] md:px-5",
|
||||||
selectedDate ? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 " : "sm:pl-4"
|
selectedDate
|
||||||
|
? "sm:dark:border-darkgray-200 border-gray-200 sm:w-1/2 sm:border-r sm:p-4 sm:pr-6 md:w-1/3"
|
||||||
|
: "sm:p-4"
|
||||||
)}
|
)}
|
||||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
@@ -229,8 +204,7 @@ const SlotPicker = ({
|
|||||||
eventTypeSlug={eventType.slug}
|
eventTypeSlug={eventType.slug}
|
||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
recurringCount={recurringEventCount}
|
recurringCount={recurringEventCount}
|
||||||
schedulingType={eventType.schedulingType}
|
ethSignature={ethSignature}
|
||||||
users={[]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -267,15 +241,15 @@ function TimezoneDropdown({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen} className="flex">
|
||||||
<Collapsible.Trigger className="min-w-32 text-bookinglight mb-1 -ml-2 px-2 py-1 text-left dark:text-white">
|
<Collapsible.Trigger className="min-w-32 dark:text-darkgray-600 mb-2 -ml-2 px-2 text-left text-gray-600">
|
||||||
<p className="text-gray-600 dark:text-white">
|
<p className="text-sm font-medium">
|
||||||
<GlobeIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
|
||||||
{timeZone}
|
{timeZone}
|
||||||
{isTimeOptionsOpen ? (
|
{isTimeOptionsOpen ? (
|
||||||
<ChevronUpIcon className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiChevronUp className="ml-1 inline-block h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDownIcon className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
@@ -298,10 +272,14 @@ const dateQuerySchema = z.object({
|
|||||||
|
|
||||||
const useRouterQuery = <T extends string>(name: T) => {
|
const useRouterQuery = <T extends string>(name: T) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const query = z.object({ [name]: z.string().optional() }).parse(router.query);
|
const existingQueryParams = router.asPath.split("?")[1];
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(existingQueryParams);
|
||||||
|
const query = Object.fromEntries(urlParams);
|
||||||
|
|
||||||
const setQuery = (newValue: string | number | null | undefined) => {
|
const setQuery = (newValue: string | number | null | undefined) => {
|
||||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||||
|
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return { [name]: query[name], setQuery } as {
|
return { [name]: query[name], setQuery } as {
|
||||||
@@ -309,6 +287,8 @@ const useRouterQuery = <T extends string>(name: T) => {
|
|||||||
} & { setQuery: typeof setQuery };
|
} & { setQuery: typeof setQuery };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType }: Props) => {
|
const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
@@ -316,7 +296,6 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
const { rescheduleUid } = query;
|
const { rescheduleUid } = query;
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { contracts } = useContracts();
|
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
@@ -325,6 +304,13 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
const [timeZone, setTimeZone] = useState<string>();
|
const [timeZone, setTimeZone] = useState<string>();
|
||||||
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
|
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
|
||||||
const [isAvailableTimesVisible, setIsAvailableTimesVisible] = useState<boolean>();
|
const [isAvailableTimesVisible, setIsAvailableTimesVisible] = useState<boolean>();
|
||||||
|
const [gateState, gateDispatcher] = useReducer(
|
||||||
|
(state: GateState, newState: Partial<GateState>) => ({
|
||||||
|
...state,
|
||||||
|
...newState,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
@@ -337,15 +323,6 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
// TODO: Improve this;
|
// TODO: Improve this;
|
||||||
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
||||||
|
|
||||||
// TODO: this needs to be extracted elsewhere
|
|
||||||
useEffect(() => {
|
|
||||||
if (eventType.metadata.smartContractAddress) {
|
|
||||||
const eventOwner = eventType.users[0];
|
|
||||||
if (!contracts[(eventType.metadata.smartContractAddress || null) as number])
|
|
||||||
router.replace(`/${eventOwner.username}`);
|
|
||||||
}
|
|
||||||
}, [contracts, eventType.metadata.smartContractAddress, eventType.users, router]);
|
|
||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -359,6 +336,8 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
|
||||||
|
// get dynamic user list here
|
||||||
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
// Recurring event sidebar requires more space
|
// Recurring event sidebar requires more space
|
||||||
const maxWidth = isAvailableTimesVisible
|
const maxWidth = isAvailableTimesVisible
|
||||||
? recurringEventCount
|
? recurringEventCount
|
||||||
@@ -376,14 +355,22 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
onChangeTimeZone={setTimeZone}
|
onChangeTimeZone={setTimeZone}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[timeZone]
|
[timeZone, timeFormat]
|
||||||
);
|
);
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
const slug = rawSlug.join("/");
|
const slug = rawSlug.join("/");
|
||||||
|
|
||||||
|
// Define conditional gates here
|
||||||
|
const gates = [
|
||||||
|
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||||
|
eventType.metadata && eventType.metadata.blockchainId && eventType.metadata.smartContractAddress
|
||||||
|
? ("rainbow" as Gate)
|
||||||
|
: undefined,
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Gates gates={gates} metadata={eventType.metadata} dispatch={gateDispatcher}>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||||
@@ -406,88 +393,51 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
<div
|
<div
|
||||||
style={availabilityDatePickerEmbedStyles}
|
style={availabilityDatePickerEmbedStyles}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isBackgroundTransparent ? "" : "bg-white dark:bg-gray-800 sm:dark:border-gray-600",
|
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white",
|
||||||
"border-bookinglightest rounded-md md:border",
|
"border-bookinglightest overflow-hidden rounded-md md:border",
|
||||||
isEmbed ? "mx-auto" : maxWidth
|
isEmbed ? "mx-auto" : maxWidth
|
||||||
)}>
|
)}>
|
||||||
{/* mobile: details */}
|
{/* mobile: details */}
|
||||||
<div className="block px-4 pt-4 sm:p-8 md:hidden">
|
<div className="block px-4 pt-4 sm:p-8 md:hidden">
|
||||||
<div>
|
<div>
|
||||||
<AvatarGroup
|
<UserAvatars
|
||||||
border="border-2 dark:border-gray-800 border-white"
|
profile={profile}
|
||||||
items={
|
users={eventType.users}
|
||||||
[
|
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||||
{ image: profile.image, alt: profile.name, title: profile.name },
|
|
||||||
...eventType.users
|
|
||||||
.filter((user) => user.name !== profile.name)
|
|
||||||
.map((user) => ({
|
|
||||||
title: user.name,
|
|
||||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
|
||||||
alt: user.name || undefined,
|
|
||||||
})),
|
|
||||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
|
||||||
}
|
|
||||||
size={9}
|
size={9}
|
||||||
truncateAfter={5}
|
truncateAfter={5}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="break-words text-sm font-medium text-black dark:text-white">{profile.name}</p>
|
<div className="dark:text-darkgray-900 mt-2 gap-2">
|
||||||
<div className="mt-2 gap-2 dark:text-gray-100">
|
<p className="break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
<h1 className="text-bookingdark mb-4 break-words text-xl font-semibold dark:text-white">
|
{profile.name}
|
||||||
|
</p>
|
||||||
|
<h1 className="text-bookingdark dark:text-darkgray-900 mb-4 break-words text-xl font-semibold">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-3">
|
||||||
{eventType?.description && (
|
{eventType?.description && (
|
||||||
<p className="text-gray-600 dark:text-white">
|
<div className="dark:text-darkgray-600 flex py-1 text-sm font-medium text-gray-600">
|
||||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<div>
|
||||||
{eventType.description}
|
<Icon.FiInfo className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||||
</p>
|
</div>
|
||||||
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType?.requiresConfirmation && (
|
{eventType?.requiresConfirmation && (
|
||||||
<p className="text-gray-600 dark:text-white">
|
<p className="dark:text-darkgray-600 text-gray-600">
|
||||||
<ClipboardCheckIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
{t("requires_confirmation")}
|
{t("requires_confirmation")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{eventType.locations.length === 1 && (
|
<AvailableEventLocations locations={eventType.locations} />
|
||||||
<p className="text-gray-600 dark:text-white">
|
<p className="dark:text-darkgray-600 text-gray-600">
|
||||||
{Object.values(AppStoreLocationType).includes(
|
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
||||||
eventType.locations[0].type as unknown as AppStoreLocationType
|
|
||||||
) ? (
|
|
||||||
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
|
||||||
) : (
|
|
||||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{locationKeyToString(eventType.locations[0], t)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{eventType.locations.length > 1 && (
|
|
||||||
<div className="flex-warp flex text-gray-600 dark:text-white">
|
|
||||||
<div className="mr-[10px] ml-[2px] -mt-1 ">
|
|
||||||
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
{eventType.locations.map((el, i, arr) => {
|
|
||||||
return (
|
|
||||||
<span key={el.type}>
|
|
||||||
{locationKeyToString(el, t)}{" "}
|
|
||||||
{arr.length - 1 !== i && (
|
|
||||||
<span className="font-light"> {t("or_lowercase")} </span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-gray-600 dark:text-white">
|
|
||||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
|
||||||
{eventType.length} {t("minutes")}
|
{eventType.length} {t("minutes")}
|
||||||
</p>
|
</p>
|
||||||
{eventType.price > 0 && (
|
{eventType.price > 0 && (
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="dark:text-darkgray-600 text-gray-600">
|
||||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 dark:text-gray-400" />
|
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={eventType.price / 100.0}
|
value={eventType.price / 100.0}
|
||||||
@@ -498,9 +448,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="dark:text-darkgray-600 flex items-center text-gray-600">
|
||||||
<RefreshIcon className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 shrink-0 text-gray-500" />
|
||||||
<div className="ml-[27px]">
|
<div>
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
</p>
|
</p>
|
||||||
@@ -508,13 +458,13 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max={eventType.recurringEvent.count}
|
max={eventType.recurringEvent.count}
|
||||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
className="w-15 dark:text-darkgray-600 h-7 rounded-sm border-gray-300 bg-white text-sm text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600"
|
||||||
defaultValue={eventType.recurringEvent.count}
|
defaultValue={eventType.recurringEvent.count}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="inline text-gray-600 dark:text-white">
|
<p className="dark:text-darkgray-600 inline text-gray-600">
|
||||||
{t("occurrence", {
|
{t("occurrence", {
|
||||||
count: recurringEventCount,
|
count: recurringEventCount,
|
||||||
})}
|
})}
|
||||||
@@ -528,12 +478,12 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
{/* Temp disabled booking?.startTime && rescheduleUid && (
|
{/* Temp disabled booking?.startTime && rescheduleUid && (
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="mt-8 text-gray-600 dark:text-white"
|
className="mt-8 text-gray-600 dark:text-darkgray-600"
|
||||||
data-testid="former_time_p_mobile">
|
data-testid="former_time_p_mobile">
|
||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 line-through dark:text-white">
|
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
||||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||||
{typeof booking.startTime === "string" &&
|
{typeof booking.startTime === "string" &&
|
||||||
parseDate(dayjs(booking.startTime), i18n)}
|
parseDate(dayjs(booking.startTime), i18n)}
|
||||||
</p>
|
</p>
|
||||||
@@ -546,92 +496,51 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 sm:flex sm:py-5">
|
<div className="overflow-hidden sm:flex">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"hidden overflow-hidden pr-8 sm:border-r sm:dark:border-gray-700 md:flex md:flex-col " +
|
"sm:dark:border-darkgray-200 hidden overflow-hidden border-gray-200 p-5 sm:border-r md:flex md:flex-col " +
|
||||||
(isAvailableTimesVisible ? "sm:w-1/3" : recurringEventCount ? "sm:w-2/3" : "sm:w-1/2")
|
(isAvailableTimesVisible ? "sm:w-1/3" : recurringEventCount ? "sm:w-2/3" : "sm:w-1/2")
|
||||||
}>
|
}>
|
||||||
<AvatarGroup
|
<UserAvatars
|
||||||
border="border-2 dark:border-gray-800 border-white"
|
profile={profile}
|
||||||
items={
|
users={eventType.users}
|
||||||
[
|
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||||
{ image: profile.image, alt: profile.name, title: profile.name },
|
|
||||||
...eventType.users
|
|
||||||
.filter((user) => user.name !== profile.name)
|
|
||||||
.map((user) => ({
|
|
||||||
title: user.name,
|
|
||||||
alt: user.name,
|
|
||||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
|
||||||
})),
|
|
||||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
|
||||||
}
|
|
||||||
size={10}
|
size={10}
|
||||||
truncateAfter={3}
|
truncateAfter={3}
|
||||||
/>
|
/>
|
||||||
<h2 className="mt-3 break-words font-medium text-gray-500 dark:text-gray-300">
|
<h2 className="break-words text-sm font-medium text-gray-600 dark:text-gray-300 lg:mt-2">
|
||||||
{profile.name}
|
{profile.name}
|
||||||
</h2>
|
</h2>
|
||||||
<h1 className="font-cal mb-4 break-words text-xl font-semibold text-gray-900 dark:text-white">
|
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl text-gray-900">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="dark:text-darkgray-600 flex flex-col space-y-3 text-sm font-medium text-gray-600">
|
||||||
{eventType?.description && (
|
{eventType?.description && (
|
||||||
<div className="flex text-gray-600 dark:text-white">
|
<div className="flex">
|
||||||
<div>
|
<div>
|
||||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiInfo className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<p>{eventType.description}</p>
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType?.requiresConfirmation && (
|
{eventType?.requiresConfirmation && (
|
||||||
<div className="flex text-gray-600 dark:text-white">
|
<div className="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<ClipboardCheckIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
|
||||||
</div>
|
</div>
|
||||||
{t("requires_confirmation")}
|
{t("requires_confirmation")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType.locations.length === 1 && (
|
<AvailableEventLocations locations={eventType.locations} />
|
||||||
<p className="text-gray-600 dark:text-white">
|
<p className="text-sm font-medium">
|
||||||
{Object.values(AppStoreLocationType).includes(
|
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
||||||
eventType.locations[0].type as unknown as AppStoreLocationType
|
|
||||||
) ? (
|
|
||||||
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
|
||||||
) : (
|
|
||||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{locationKeyToString(eventType.locations[0], t)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{eventType.locations.length > 1 && (
|
|
||||||
<div className="flex-warp flex text-gray-600 dark:text-white">
|
|
||||||
<div className="mr-[10px] ml-[2px] -mt-1 ">
|
|
||||||
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
{eventType.locations.map((el, i, arr) => {
|
|
||||||
return (
|
|
||||||
<span key={el.type}>
|
|
||||||
{locationKeyToString(el, t)}{" "}
|
|
||||||
{arr.length - 1 !== i && (
|
|
||||||
<span className="font-light"> {t("or_lowercase")} </span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-gray-600 dark:text-white">
|
|
||||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
|
||||||
{eventType.length} {t("minutes")}
|
{eventType.length} {t("minutes")}
|
||||||
</p>
|
</p>
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="flex items-start text-sm font-medium">
|
||||||
<RefreshIcon className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
||||||
<div className="ml-[27px]">
|
<div>
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
</p>
|
</p>
|
||||||
@@ -639,13 +548,13 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max={eventType.recurringEvent.count}
|
max={eventType.recurringEvent.count}
|
||||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
||||||
defaultValue={eventType.recurringEvent.count}
|
defaultValue={eventType.recurringEvent.count}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="inline text-gray-600 dark:text-white">
|
<p className="inline">
|
||||||
{t("occurrence", {
|
{t("occurrence", {
|
||||||
count: recurringEventCount,
|
count: recurringEventCount,
|
||||||
})}
|
})}
|
||||||
@@ -654,8 +563,8 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType.price > 0 && (
|
{eventType.price > 0 && (
|
||||||
<p className="-ml-2 px-2 py-1 text-gray-600 dark:text-white">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={eventType.price / 100.0}
|
value={eventType.price / 100.0}
|
||||||
@@ -673,12 +582,12 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="mt-4 mb-3 text-gray-600 dark:text-white"
|
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
||||||
data-testid="former_time_p_desktop">
|
data-testid="former_time_p_desktop">
|
||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 line-through dark:text-white">
|
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
||||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -695,15 +604,18 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
users={userList}
|
||||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||||
recurringEventCount={recurringEventCount}
|
recurringEventCount={recurringEventCount}
|
||||||
|
ethSignature={gateState.rainbowToken}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
|
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<Toaster position="bottom-right" />
|
||||||
|
</Gates>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
import {
|
|
||||||
CalendarIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
ExclamationCircleIcon,
|
|
||||||
ExclamationIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
ClipboardCheckIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
|
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
|
||||||
import { useContracts } from "contexts/contractsContext";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState, useReducer } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { ReactMultiEmail } from "react-multi-email";
|
import { ReactMultiEmail } from "react-multi-email";
|
||||||
@@ -24,6 +13,14 @@ import { useMutation } from "react-query";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
locationKeyToString,
|
||||||
|
getEventLocationValue,
|
||||||
|
getEventLocationType,
|
||||||
|
EventLocationType,
|
||||||
|
} from "@calcom/app-store/locations";
|
||||||
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
|
import { LocationObject, LocationType } from "@calcom/core/location";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -33,54 +30,39 @@ import {
|
|||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { createPaymentLink } from "@calcom/stripe/client";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { Button } from "@calcom/ui/Button";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
|
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||||
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
||||||
|
import { Button } from "@calcom/ui/v2";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { ensureArray } from "@lib/ensureArray";
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
import useTheme from "@lib/hooks/useTheme";
|
|
||||||
import { LocationObject, LocationType } from "@lib/location";
|
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||||
import slugify from "@lib/slugify";
|
import slugify from "@lib/slugify";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
|
||||||
|
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import Gates, { Gate, GateState } from "@components/Gates";
|
||||||
import type PhoneInputType from "@components/ui/form/PhoneInput";
|
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||||
|
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
||||||
|
|
||||||
import { BookPageProps } from "../../../pages/[user]/book";
|
import { BookPageProps } from "../../../pages/[user]/book";
|
||||||
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||||
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||||
|
|
||||||
declare global {
|
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var web3: {
|
|
||||||
currentProvider: {
|
|
||||||
selectedAddress: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** These are like 40kb that not every user needs */
|
|
||||||
const PhoneInput = dynamic(
|
|
||||||
() => import("@components/ui/form/PhoneInput")
|
|
||||||
) as unknown as typeof PhoneInputType;
|
|
||||||
|
|
||||||
type BookingPageProps = (BookPageProps | TeamBookingPageProps | HashLinkPageProps) & {
|
|
||||||
locationLabels: Record<LocationType, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BookingFormValues = {
|
type BookingFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
locationType?: LocationType;
|
locationType?: EventLocationType["type"];
|
||||||
guests?: string[];
|
guests?: string[];
|
||||||
phone?: string;
|
phone?: string;
|
||||||
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
|
hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers
|
||||||
@@ -97,7 +79,6 @@ const BookingPage = ({
|
|||||||
profile,
|
profile,
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
locationLabels,
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
@@ -106,10 +87,16 @@ const BookingPage = ({
|
|||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { contracts } = useContracts();
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const [gateState, gateDispatcher] = useReducer(
|
||||||
|
(state: GateState, newState: Partial<GateState>) => ({
|
||||||
|
...state,
|
||||||
|
...newState,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
@@ -122,15 +109,6 @@ const BookingPage = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (eventType.metadata.smartContractAddress) {
|
|
||||||
const eventOwner = eventType.users[0];
|
|
||||||
|
|
||||||
if (!contracts[(eventType.metadata.smartContractAddress || null) as number])
|
|
||||||
router.replace(`/${eventOwner.username}`);
|
|
||||||
}
|
|
||||||
}, [contracts, eventType.metadata.smartContractAddress, eventType.users, router]);
|
|
||||||
|
|
||||||
const mutation = useMutation(createBooking, {
|
const mutation = useMutation(createBooking, {
|
||||||
onSuccess: async (responseData) => {
|
onSuccess: async (responseData) => {
|
||||||
const { id, attendees, paymentUid } = responseData;
|
const { id, attendees, paymentUid } = responseData;
|
||||||
@@ -146,16 +124,6 @@ const BookingPage = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const location = (function humanReadableLocation(location) {
|
|
||||||
if (!location) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (location.includes("integration")) {
|
|
||||||
return t("web_conferencing_details_to_follow");
|
|
||||||
}
|
|
||||||
return location;
|
|
||||||
})(responseData.location);
|
|
||||||
|
|
||||||
return router.push({
|
return router.push({
|
||||||
pathname: "/success",
|
pathname: "/success",
|
||||||
query: {
|
query: {
|
||||||
@@ -166,7 +134,7 @@ const BookingPage = ({
|
|||||||
reschedule: !!rescheduleUid,
|
reschedule: !!rescheduleUid,
|
||||||
name: attendees[0].name,
|
name: attendees[0].name,
|
||||||
email: attendees[0].email,
|
email: attendees[0].email,
|
||||||
location,
|
location: responseData.location,
|
||||||
eventName: profile.eventName || "",
|
eventName: profile.eventName || "",
|
||||||
bookingId: id,
|
bookingId: id,
|
||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
@@ -212,9 +180,6 @@ const BookingPage = ({
|
|||||||
const date = asStringOrNull(router.query.date);
|
const date = asStringOrNull(router.query.date);
|
||||||
|
|
||||||
const [guestToggle, setGuestToggle] = useState(booking && booking.attendees.length > 1);
|
const [guestToggle, setGuestToggle] = useState(booking && booking.attendees.length > 1);
|
||||||
|
|
||||||
const eventTypeDetail = { isWeb3Active: false, ...eventType };
|
|
||||||
|
|
||||||
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
|
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
|
||||||
const locations: LocationObject[] = useMemo(
|
const locations: LocationObject[] = useMemo(
|
||||||
() => (eventType.locations as LocationObject[]) || [],
|
() => (eventType.locations as LocationObject[]) || [],
|
||||||
@@ -227,17 +192,26 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
}, [router.query.guest]);
|
}, [router.query.guest]);
|
||||||
|
|
||||||
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
|
|
||||||
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
|
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
|
||||||
const guestListEmails = !isDynamicGroupBooking
|
const guestListEmails = !isDynamicGroupBooking
|
||||||
? booking?.attendees.slice(1).map((attendee) => attendee.email)
|
? booking?.attendees.slice(1).map((attendee) => attendee.email)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// There should only exists one default userData variable for primaryAttendee.
|
||||||
|
const defaultUserValues = {
|
||||||
|
email: rescheduleUid
|
||||||
|
? booking?.attendees[0].email
|
||||||
|
: router.query.email
|
||||||
|
? (router.query.email as string)
|
||||||
|
: "",
|
||||||
|
name: rescheduleUid ? booking?.attendees[0].name : router.query.name ? (router.query.name as string) : "",
|
||||||
|
};
|
||||||
|
|
||||||
const defaultValues = () => {
|
const defaultValues = () => {
|
||||||
if (!rescheduleUid) {
|
if (!rescheduleUid) {
|
||||||
return {
|
return {
|
||||||
name: (router.query.name as string) || (!loggedInIsOwner && session?.user?.name) || "",
|
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
|
||||||
email: (router.query.email as string) || (!loggedInIsOwner && session?.user?.email) || "",
|
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
|
||||||
notes: (router.query.notes as string) || "",
|
notes: (router.query.notes as string) || "",
|
||||||
guests: ensureArray(router.query.guest) as string[],
|
guests: ensureArray(router.query.guest) as string[],
|
||||||
customInputs: eventType.customInputs.reduce(
|
customInputs: eventType.customInputs.reduce(
|
||||||
@@ -259,11 +233,12 @@ const BookingPage = ({
|
|||||||
|
|
||||||
const customInputType = booking.customInputs;
|
const customInputType = booking.customInputs;
|
||||||
return {
|
return {
|
||||||
name: primaryAttendee.name || "",
|
name: defaultUserValues.name,
|
||||||
email: primaryAttendee.email || "",
|
email: defaultUserValues.email || "",
|
||||||
guests: guestListEmails,
|
guests: guestListEmails,
|
||||||
notes: booking.description || "",
|
notes: booking.description || "",
|
||||||
rescheduleReason: "",
|
rescheduleReason: "",
|
||||||
|
smsReminderNumber: booking.smsReminderNumber || undefined,
|
||||||
customInputs: eventType.customInputs.reduce(
|
customInputs: eventType.customInputs.reduce(
|
||||||
(customInputs, input) => ({
|
(customInputs, input) => ({
|
||||||
...customInputs,
|
...customInputs,
|
||||||
@@ -296,12 +271,12 @@ const BookingPage = ({
|
|||||||
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
|
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedLocation = useWatch({
|
const selectedLocationType = useWatch({
|
||||||
control: bookingForm.control,
|
control: bookingForm.control,
|
||||||
name: "locationType",
|
name: "locationType",
|
||||||
defaultValue: ((): LocationType | undefined => {
|
defaultValue: ((): EventLocationType["type"] | undefined => {
|
||||||
if (router.query.location) {
|
if (router.query.location) {
|
||||||
return router.query.location as LocationType;
|
return router.query.location as EventLocationType["type"];
|
||||||
}
|
}
|
||||||
if (locations.length === 1) {
|
if (locations.length === 1) {
|
||||||
return locations[0]?.type;
|
return locations[0]?.type;
|
||||||
@@ -309,37 +284,13 @@ const BookingPage = ({
|
|||||||
})(),
|
})(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getLocationValue = (
|
const selectedLocation = getEventLocationType(selectedLocationType);
|
||||||
booking: Pick<BookingFormValues, "locationType" | "phone" | "hostPhoneNumber">
|
const AttendeeInput =
|
||||||
) => {
|
selectedLocation?.attendeeInputType === "text"
|
||||||
const { locationType } = booking;
|
? "input"
|
||||||
switch (locationType) {
|
: selectedLocation?.attendeeInputType === "phone"
|
||||||
case LocationType.Phone: {
|
? PhoneInput
|
||||||
return booking.phone || "";
|
: null;
|
||||||
}
|
|
||||||
case LocationType.InPerson: {
|
|
||||||
return locationInfo(locationType)?.address || "";
|
|
||||||
}
|
|
||||||
case LocationType.Link: {
|
|
||||||
return locationInfo(locationType)?.link || "";
|
|
||||||
}
|
|
||||||
case LocationType.UserPhone: {
|
|
||||||
return locationInfo(locationType)?.hostPhoneNumber || "";
|
|
||||||
}
|
|
||||||
case LocationType.Around: {
|
|
||||||
return locationInfo(locationType)?.link || "";
|
|
||||||
}
|
|
||||||
case LocationType.Riverside: {
|
|
||||||
return locationInfo(locationType)?.link || "";
|
|
||||||
}
|
|
||||||
case LocationType.Whereby: {
|
|
||||||
return locationInfo(locationType)?.link || "";
|
|
||||||
}
|
|
||||||
// Catches all other location types, such as Google Meet, Zoom etc.
|
|
||||||
default:
|
|
||||||
return selectedLocation || "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate the booking date(s)
|
// Calculate the booking date(s)
|
||||||
let recurringStrings: string[] = [],
|
let recurringStrings: string[] = [],
|
||||||
@@ -375,20 +326,11 @@ const BookingPage = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
let web3Details: Record<"userWallet" | "userSignature", string> | undefined;
|
|
||||||
if (eventTypeDetail.metadata.smartContractAddress) {
|
|
||||||
web3Details = {
|
|
||||||
userWallet: window.web3.currentProvider.selectedAddress,
|
|
||||||
userSignature: contracts[(eventTypeDetail.metadata.smartContractAddress || null) as number],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recurringDates.length) {
|
if (recurringDates.length) {
|
||||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||||
const recurringEventId = uuidv4();
|
const recurringEventId = uuidv4();
|
||||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||||
...booking,
|
...booking,
|
||||||
web3Details,
|
|
||||||
start: dayjs(recurringDate).format(),
|
start: dayjs(recurringDate).format(),
|
||||||
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
|
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
@@ -400,9 +342,10 @@ const BookingPage = ({
|
|||||||
language: i18n.language,
|
language: i18n.language,
|
||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
user: router.query.user,
|
user: router.query.user,
|
||||||
location: getLocationValue(
|
location: getEventLocationValue(locations, {
|
||||||
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
|
type: booking.locationType ? booking.locationType : selectedLocationType || "",
|
||||||
),
|
phone: booking.phone,
|
||||||
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||||
@@ -411,13 +354,13 @@ const BookingPage = ({
|
|||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
||||||
|
ethSignature: gateState.rainbowToken,
|
||||||
}));
|
}));
|
||||||
recurringMutation.mutate(recurringBookings);
|
recurringMutation.mutate(recurringBookings);
|
||||||
} else {
|
} else {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
...booking,
|
...booking,
|
||||||
web3Details,
|
|
||||||
start: dayjs(date).format(),
|
start: dayjs(date).format(),
|
||||||
end: dayjs(date).add(eventType.length, "minute").format(),
|
end: dayjs(date).add(eventType.length, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
@@ -427,9 +370,10 @@ const BookingPage = ({
|
|||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
bookingUid: router.query.bookingUid as string,
|
bookingUid: router.query.bookingUid as string,
|
||||||
user: router.query.user,
|
user: router.query.user,
|
||||||
location: getLocationValue(
|
location: getEventLocationValue(locations, {
|
||||||
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
|
type: (booking.locationType ? booking.locationType : selectedLocationType) || "",
|
||||||
),
|
phone: booking.phone,
|
||||||
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||||
@@ -438,15 +382,18 @@ const BookingPage = ({
|
|||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
||||||
|
ethSignature: gateState.rainbowToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInput = !!rescheduleUid;
|
// Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields.
|
||||||
|
const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name;
|
||||||
|
const disableLocations = !!rescheduleUid;
|
||||||
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
|
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
|
||||||
const inputClassName =
|
const inputClassName =
|
||||||
"focus:border-brand block w-full rounded-sm border-gray-300 focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm";
|
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
|
||||||
|
|
||||||
let isSmsReminderNumberNeeded = false;
|
let isSmsReminderNumberNeeded = false;
|
||||||
|
|
||||||
@@ -463,8 +410,16 @@ const BookingPage = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define conditional gates here
|
||||||
|
const gates = [
|
||||||
|
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||||
|
eventType.metadata && eventType.metadata.blockchainId && eventType.metadata.smartContractAddress
|
||||||
|
? ("rainbow" as Gate)
|
||||||
|
: undefined,
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Gates gates={gates} metadata={eventType.metadata} dispatch={gateDispatcher}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
{rescheduleUid
|
{rescheduleUid
|
||||||
@@ -491,127 +446,123 @@ const BookingPage = ({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"main overflow-hidden",
|
"main overflow-hidden",
|
||||||
isEmbed ? "" : "border border-gray-200",
|
isEmbed ? "" : "border border-gray-200",
|
||||||
isBackgroundTransparent ? "" : "dark:border-1 bg-white dark:bg-gray-800",
|
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white",
|
||||||
"rounded-md dark:border-gray-600 sm:border"
|
"dark:border-darkgray-300 rounded-md sm:border"
|
||||||
)}>
|
)}>
|
||||||
<div className="px-4 py-5 sm:flex sm:p-4">
|
<div className="sm:flex">
|
||||||
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-700">
|
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<AvatarGroup
|
<UserAvatars
|
||||||
border="border-2 border-white dark:border-gray-800"
|
profile={profile}
|
||||||
size={14}
|
users={eventType.users}
|
||||||
items={[
|
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||||
{ image: profile.image || "", alt: profile.name || "", title: profile.name || "" },
|
size={10}
|
||||||
].concat(
|
truncateAfter={3}
|
||||||
eventType.users
|
|
||||||
.filter((user) => user.name !== profile.name)
|
|
||||||
.map((user) => ({
|
|
||||||
title: user.name || "",
|
|
||||||
image: user.avatar || "",
|
|
||||||
alt: user.name || "",
|
|
||||||
}))
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<h2 className="font-cal text-bookinglight mt-2 font-medium dark:text-gray-300">
|
<h2 className="mt-2 break-words text-sm font-medium text-gray-500 dark:text-gray-300">
|
||||||
{profile.name}
|
{profile.name}
|
||||||
</h2>
|
</h2>
|
||||||
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
|
<h1 className="font-cal dark:text-darkgray-900 break-words text-2xl text-gray-900 ">
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
<div className="mt-4 flex flex-col space-y-3 lg:mt-9">
|
||||||
<p
|
{eventType?.description && (
|
||||||
className={`${
|
<div className="dark:text-darkgray-600 flex text-sm font-medium text-gray-600">
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
<div>
|
||||||
? "text-rose-600"
|
<Icon.FiInfo className="dark:text-darkgray-600 mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
|
||||||
? "text-yellow-500"
|
|
||||||
: "text-emerald-400"
|
|
||||||
} mb-2`}>
|
|
||||||
{booking
|
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{eventType?.description && (
|
|
||||||
<p className="text-bookinglight mb-2 dark:text-white">
|
|
||||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
|
||||||
{eventType.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{eventType?.requiresConfirmation && (
|
|
||||||
<p className="text-bookinglight mb-2 dark:text-white">
|
|
||||||
<ClipboardCheckIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
|
||||||
{t("requires_confirmation")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="text-bookinglight mb-2 dark:text-white">
|
|
||||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
|
||||||
{eventType.length} {t("minutes")}
|
|
||||||
</p>
|
|
||||||
{eventType.price > 0 && (
|
|
||||||
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1 dark:text-white">
|
|
||||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<FormattedNumber
|
|
||||||
value={eventType.price / 100.0}
|
|
||||||
style="currency"
|
|
||||||
currency={eventType.currency.toUpperCase()}
|
|
||||||
/>
|
|
||||||
</IntlProvider>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
|
||||||
<div className="mb-3 text-gray-600 dark:text-white">
|
|
||||||
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
|
||||||
{getEveryFreqFor({
|
|
||||||
t,
|
|
||||||
recurringEvent: eventType.recurringEvent,
|
|
||||||
recurringCount: recurringEventCount,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-bookinghighlight mb-4 flex">
|
|
||||||
<CalendarIcon className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
|
||||||
<div className="-mt-1">
|
|
||||||
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
|
||||||
parseDate(dayjs(date).tz(timeZone()), i18n)}
|
|
||||||
{!rescheduleUid &&
|
|
||||||
eventType.recurringEvent?.freq &&
|
|
||||||
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
|
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
|
||||||
<div className="flex">
|
|
||||||
<Tooltip
|
|
||||||
content={recurringStrings.slice(5).map((aDate, key) => (
|
|
||||||
<p key={key}>{aDate}</p>
|
|
||||||
))}>
|
|
||||||
<p className="text-gray-600 dark:text-white">
|
|
||||||
{t("plus_more", { count: recurringStrings.length - 5 })}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (
|
{eventType?.requiresConfirmation && (
|
||||||
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1">
|
<div className="dark:text-darkgray-600 flex items-center text-sm font-medium text-gray-600">
|
||||||
{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}
|
<div>
|
||||||
|
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
|
||||||
|
</div>
|
||||||
|
{t("requires_confirmation")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="dark:text-darkgray-600 text-sm font-medium text-gray-600">
|
||||||
|
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
||||||
|
{eventType.length} {t("minutes")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
{eventType.price > 0 && (
|
||||||
{booking?.startTime && rescheduleUid && (
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<div>
|
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
<p className="mt-8 mb-2 text-gray-600 dark:text-white" data-testid="former_time_p">
|
<IntlProvider locale="en">
|
||||||
{t("former_time")}
|
<FormattedNumber
|
||||||
</p>
|
value={eventType.price / 100.0}
|
||||||
<p className="text-gray-500 line-through dark:text-white">
|
style="currency"
|
||||||
<CalendarIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
currency={eventType.currency.toUpperCase()}
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
/>
|
||||||
|
</IntlProvider>
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
|
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||||
|
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||||
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
|
{getEveryFreqFor({
|
||||||
|
t,
|
||||||
|
recurringEvent: eventType.recurringEvent,
|
||||||
|
recurringCount: recurringEventCount,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
|
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
|
parseDate(dayjs(date).tz(timeZone()), i18n)}
|
||||||
|
{!rescheduleUid &&
|
||||||
|
eventType.recurringEvent?.freq &&
|
||||||
|
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
|
||||||
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||||
|
<div className="flex">
|
||||||
|
<Tooltip
|
||||||
|
content={recurringStrings.slice(5).map((aDate, key) => (
|
||||||
|
<p key={key}>{aDate}</p>
|
||||||
|
))}>
|
||||||
|
<p className="dark:text-darkgray-600 text-sm">
|
||||||
|
{t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
|
</p>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{booking?.startTime && rescheduleUid && (
|
||||||
|
<div>
|
||||||
|
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||||
|
{t("former_time")}
|
||||||
|
</p>
|
||||||
|
<p className="line-through ">
|
||||||
|
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||||
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
|
<Icon.FiUser className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
|
? "text-rose-600"
|
||||||
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-bookinghighlight"
|
||||||
|
} mb-2 font-medium`}>
|
||||||
|
{booking
|
||||||
|
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||||
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
<div className="p-6 sm:w-1/2">
|
||||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
@@ -640,9 +591,7 @@ const BookingPage = ({
|
|||||||
required
|
required
|
||||||
className={classNames(
|
className={classNames(
|
||||||
inputClassName,
|
inputClassName,
|
||||||
bookingForm.formState.errors.email
|
bookingForm.formState.errors.email && "!focus:ring-red-700 !border-red-700"
|
||||||
? "border-red-700 focus:ring-red-700"
|
|
||||||
: " border-gray-300 dark:border-gray-900"
|
|
||||||
)}
|
)}
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
||||||
@@ -650,7 +599,7 @@ const BookingPage = ({
|
|||||||
/>
|
/>
|
||||||
{bookingForm.formState.errors.email && (
|
{bookingForm.formState.errors.email && (
|
||||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
|
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||||
<p>{t("email_validation_error")}</p>
|
<p>{t("email_validation_error")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -661,23 +610,34 @@ const BookingPage = ({
|
|||||||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
</span>
|
</span>
|
||||||
{locations.map((location, i) => (
|
{locations.map((location, i) => {
|
||||||
<label key={i} className="block">
|
const locationString = locationKeyToString(location);
|
||||||
<input
|
// TODO: Right now selectedLocationType isn't send by getSSP. Once that's available defaultChecked should work and show the location in the original booking
|
||||||
type="radio"
|
const defaultChecked = rescheduleUid ? selectedLocationType === location.type : i === 0;
|
||||||
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
if (typeof locationString !== "string") {
|
||||||
{...bookingForm.register("locationType", { required: true })}
|
// It's possible that location app got uninstalled
|
||||||
value={location.type}
|
return null;
|
||||||
defaultChecked={selectedLocation === location.type}
|
}
|
||||||
/>
|
return (
|
||||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
|
<label key={i} className="block">
|
||||||
{locationLabels[location.type]}
|
<input
|
||||||
</span>
|
type="radio"
|
||||||
</label>
|
disabled={!!disableLocations}
|
||||||
))}
|
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
||||||
|
{...bookingForm.register("locationType", { required: true })}
|
||||||
|
value={location.type}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
/>
|
||||||
|
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
|
||||||
|
{locationKeyToString(location)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedLocation === LocationType.Phone && (
|
{/* TODO: Change name and id ="phone" to something generic */}
|
||||||
|
{AttendeeInput && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor="phone"
|
htmlFor="phone"
|
||||||
@@ -685,10 +645,10 @@ const BookingPage = ({
|
|||||||
{t("phone_number")}
|
{t("phone_number")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<PhoneInput<BookingFormValues>
|
<AttendeeInput<BookingFormValues>
|
||||||
control={bookingForm.control}
|
control={bookingForm.control}
|
||||||
name="phone"
|
name="phone"
|
||||||
placeholder={t("enter_phone_number")}
|
placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")}
|
||||||
id="phone"
|
id="phone"
|
||||||
required
|
required
|
||||||
disabled={disableInput}
|
disabled={disableInput}
|
||||||
@@ -696,7 +656,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
{bookingForm.formState.errors.phone && (
|
{bookingForm.formState.errors.phone && (
|
||||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
|
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||||
<p>{t("invalid_number")}</p>
|
<p>{t("invalid_number")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -718,6 +678,7 @@ const BookingPage = ({
|
|||||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||||
required: input.required,
|
required: input.required,
|
||||||
})}
|
})}
|
||||||
|
required={input.required}
|
||||||
id={"custom_" + input.id}
|
id={"custom_" + input.id}
|
||||||
rows={3}
|
rows={3}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
@@ -731,6 +692,7 @@ const BookingPage = ({
|
|||||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||||
required: input.required,
|
required: input.required,
|
||||||
})}
|
})}
|
||||||
|
required={input.required}
|
||||||
id={"custom_" + input.id}
|
id={"custom_" + input.id}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
placeholder={input.placeholder}
|
placeholder={input.placeholder}
|
||||||
@@ -743,6 +705,7 @@ const BookingPage = ({
|
|||||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||||
required: input.required,
|
required: input.required,
|
||||||
})}
|
})}
|
||||||
|
required={input.required}
|
||||||
id={"custom_" + input.id}
|
id={"custom_" + input.id}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
@@ -756,6 +719,7 @@ const BookingPage = ({
|
|||||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||||
required: input.required,
|
required: input.required,
|
||||||
})}
|
})}
|
||||||
|
required={input.required}
|
||||||
id={"custom_" + input.id}
|
id={"custom_" + input.id}
|
||||||
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
|
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
@@ -770,72 +734,61 @@ const BookingPage = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!eventType.disableGuests && (
|
{!eventType.disableGuests && guestToggle && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{!guestToggle && (
|
<div>
|
||||||
<label
|
<label
|
||||||
onClick={() => setGuestToggle(!guestToggle)}
|
|
||||||
htmlFor="guests"
|
htmlFor="guests"
|
||||||
className="mb-1 block text-sm font-medium hover:cursor-pointer dark:text-white">
|
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
{/*<UserAddIcon className="inline-block w-5 h-5 mr-1 -mt-1" />*/}
|
{t("guests")}
|
||||||
{t("additional_guests")}
|
|
||||||
</label>
|
</label>
|
||||||
)}
|
{!disableInput && (
|
||||||
{guestToggle && (
|
<Controller
|
||||||
<div>
|
control={bookingForm.control}
|
||||||
<label
|
name="guests"
|
||||||
htmlFor="guests"
|
render={({ field: { onChange, value } }) => (
|
||||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
<ReactMultiEmail
|
||||||
{t("guests")}
|
className="relative"
|
||||||
</label>
|
placeholder={<span className="dark:text-darkgray-600">guest@example.com</span>}
|
||||||
{!disableInput && (
|
emails={value}
|
||||||
<Controller
|
onChange={onChange}
|
||||||
control={bookingForm.control}
|
getLabel={(
|
||||||
name="guests"
|
email: string,
|
||||||
render={({ field: { onChange, value } }) => (
|
index: number,
|
||||||
<ReactMultiEmail
|
removeEmail: (index: number) => void
|
||||||
className="relative"
|
) => {
|
||||||
placeholder="guest@example.com"
|
return (
|
||||||
emails={value}
|
<div data-tag key={index} className="cursor-pointer">
|
||||||
onChange={onChange}
|
{email}
|
||||||
getLabel={(
|
{!disableInput && (
|
||||||
email: string,
|
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||||
index: number,
|
×
|
||||||
removeEmail: (index: number) => void
|
</span>
|
||||||
) => {
|
)}
|
||||||
return (
|
</div>
|
||||||
<div data-tag key={index} className="cursor-pointer">
|
);
|
||||||
{email}
|
}}
|
||||||
{!disableInput && (
|
/>
|
||||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
)}
|
||||||
×
|
/>
|
||||||
</span>
|
)}
|
||||||
)}
|
{/* Custom code when guest emails should not be editable */}
|
||||||
</div>
|
{disableInput && guestListEmails && guestListEmails.length > 0 && (
|
||||||
);
|
<div data-tag className="react-multi-email">
|
||||||
}}
|
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
|
||||||
/>
|
{guestListEmails.map((email, index) => {
|
||||||
)}
|
return (
|
||||||
/>
|
<div key={index} className="cursor-pointer">
|
||||||
)}
|
<span data-tag>{email}</span>
|
||||||
{/* Custom code when guest emails should not be editable */}
|
</div>
|
||||||
{disableInput && guestListEmails && guestListEmails.length > 0 && (
|
);
|
||||||
<div data-tag className="react-multi-email">
|
})}
|
||||||
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
|
</div>
|
||||||
{guestListEmails.map((email, index) => {
|
)}
|
||||||
return (
|
</div>
|
||||||
<div key={index} className="cursor-pointer">
|
|
||||||
<span data-tag>{email}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isSmsReminderNumberNeeded && selectedLocation !== LocationType.Phone && (
|
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
htmlFor="smsReminderNumber"
|
htmlFor="smsReminderNumber"
|
||||||
@@ -849,12 +802,11 @@ const BookingPage = ({
|
|||||||
placeholder={t("enter_phone_number")}
|
placeholder={t("enter_phone_number")}
|
||||||
id="smsReminderNumber"
|
id="smsReminderNumber"
|
||||||
required
|
required
|
||||||
disabled={disableInput}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{bookingForm.formState.errors.smsReminderNumber && (
|
{bookingForm.formState.errors.smsReminderNumber && (
|
||||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
|
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||||
<p>{t("invalid_number")}</p>
|
<p>{t("invalid_number")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -874,7 +826,6 @@ const BookingPage = ({
|
|||||||
rows={3}
|
rows={3}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
placeholder={t("reschedule_placeholder")}
|
placeholder={t("reschedule_placeholder")}
|
||||||
disabled={disabledExceptForOwner}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
@@ -889,16 +840,33 @@ const BookingPage = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
<div className="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
|
{!eventType.disableGuests && !guestToggle && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="minimalSecondary"
|
||||||
|
size="icon"
|
||||||
|
tooltip={t("additional_guests")}
|
||||||
|
StartIcon={Icon.FiUserPlus}
|
||||||
|
onClick={() => setGuestToggle(!guestToggle)}
|
||||||
|
className="mr-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
// We override this for this component only for now - as we don't support darkmode everywhere in the app
|
||||||
|
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast mr-auto dark:border-transparent"
|
||||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" type="button" onClick={() => router.back()}>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
{(mutation.isError || recurringMutation.isError) && (
|
{(mutation.isError || recurringMutation.isError) && (
|
||||||
@@ -908,7 +876,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</Gates>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -922,7 +890,7 @@ function ErrorMessage({ error }: { error: unknown }) {
|
|||||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
<Icon.FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ltr:ml-3 rtl:mr-3">
|
<div className="ltr:ml-3 rtl:mr-3">
|
||||||
<p className="text-sm text-yellow-700">
|
<p className="text-sm text-yellow-700">
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { ExclamationIcon } from "@heroicons/react/outline";
|
|
||||||
import { CheckIcon } from "@heroicons/react/solid";
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import React, { PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui/Button";
|
import { Button } from "@calcom/ui/Button";
|
||||||
import { DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
import { DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
|
||||||
export type DeleteStripeDialogContentProps = {
|
export type DeleteStripeDialogContentProps = {
|
||||||
cancelAllBookingsBtnText?: string;
|
cancelAllBookingsBtnText?: string;
|
||||||
@@ -37,17 +36,17 @@ export default function DeleteStripeDialogContent(props: PropsWithChildren<Delet
|
|||||||
<div className="mt-0.5 ltr:mr-3">
|
<div className="mt-0.5 ltr:mr-3">
|
||||||
{variety === "danger" && (
|
{variety === "danger" && (
|
||||||
<div className="mx-auto rounded-full bg-red-100 p-2 text-center">
|
<div className="mx-auto rounded-full bg-red-100 p-2 text-center">
|
||||||
<ExclamationIcon className="h-5 w-5 text-red-600" />
|
<Icon.FiAlertTriangle className="h-5 w-5 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{variety === "warning" && (
|
{variety === "warning" && (
|
||||||
<div className="mx-auto rounded-full bg-orange-100 p-2 text-center">
|
<div className="mx-auto rounded-full bg-orange-100 p-2 text-center">
|
||||||
<ExclamationIcon className="h-5 w-5 text-orange-600" />
|
<Icon.FiAlertTriangle className="h-5 w-5 text-orange-600" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{variety === "success" && (
|
{variety === "success" && (
|
||||||
<div className="mx-auto rounded-full bg-green-100 p-2 text-center">
|
<div className="mx-auto rounded-full bg-green-100 p-2 text-center">
|
||||||
<CheckIcon className="h-5 w-5 text-green-600" />
|
<Icon.FiCheck className="h-5 w-5 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,62 +1,75 @@
|
|||||||
import { LocationMarkerIcon } from "@heroicons/react/solid";
|
import { ErrorMessage } from "@hookform/error-message";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import {
|
||||||
|
LocationType,
|
||||||
|
getEventLocationType,
|
||||||
|
EventLocationType,
|
||||||
|
LocationObject,
|
||||||
|
} from "@calcom/app-store/locations";
|
||||||
|
import { getMessageForOrganizer } from "@calcom/app-store/locations";
|
||||||
|
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
import { Form } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
import { linkValueToString } from "@lib/linkValueToString";
|
|
||||||
import { LocationType } from "@lib/location";
|
|
||||||
import { LocationOptionsToString } from "@lib/locationOptions";
|
|
||||||
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import type PhoneInputType from "@components/ui/form/PhoneInput";
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
const PhoneInput = dynamic(
|
|
||||||
() => import("@components/ui/form/PhoneInput")
|
|
||||||
) as unknown as typeof PhoneInputType;
|
|
||||||
|
|
||||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||||
|
|
||||||
type OptionTypeBase = {
|
type OptionTypeBase = {
|
||||||
label: string;
|
label: string;
|
||||||
value: LocationType;
|
value: EventLocationType["type"];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LocationFormValues = {
|
|
||||||
locationType: LocationType;
|
|
||||||
locationAddress?: string;
|
|
||||||
locationLink?: string;
|
|
||||||
locationPhoneNumber?: string;
|
|
||||||
displayLocationPublicly?: boolean;
|
|
||||||
};
|
|
||||||
interface ISetLocationDialog {
|
interface ISetLocationDialog {
|
||||||
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
|
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||||
selection?: OptionTypeBase;
|
selection?: OptionTypeBase;
|
||||||
booking?: BookingItem;
|
booking?: BookingItem;
|
||||||
defaultValues?: {
|
defaultValues?: LocationObject[];
|
||||||
type: LocationType;
|
|
||||||
address?: string | undefined;
|
|
||||||
link?: string | undefined;
|
|
||||||
hostPhoneNumber?: string | undefined;
|
|
||||||
displayLocationPublicly?: boolean | undefined;
|
|
||||||
}[];
|
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LocationInput = (props: {
|
||||||
|
eventLocationType: EventLocationType;
|
||||||
|
locationFormMethods: ReturnType<typeof useForm>;
|
||||||
|
id: string;
|
||||||
|
required: boolean;
|
||||||
|
placeholder: string;
|
||||||
|
className?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
}): JSX.Element | null => {
|
||||||
|
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
||||||
|
if (eventLocationType?.organizerInputType === "text") {
|
||||||
|
return (
|
||||||
|
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||||
|
);
|
||||||
|
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||||
|
return (
|
||||||
|
<PhoneInput
|
||||||
|
name={eventLocationType.variable}
|
||||||
|
control={locationFormMethods.control}
|
||||||
|
{...remainingProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
const {
|
const {
|
||||||
saveLocation,
|
saveLocation,
|
||||||
@@ -79,32 +92,52 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
const locationFormSchema = z.object({
|
const locationFormSchema = z.object({
|
||||||
locationType: z.string(),
|
locationType: z.string(),
|
||||||
|
phone: z.string().optional().nullable(),
|
||||||
locationAddress: z.string().optional(),
|
locationAddress: z.string().optional(),
|
||||||
locationLink:
|
locationLink: z
|
||||||
selection?.value === LocationType.Whereby
|
.string()
|
||||||
? z
|
.optional()
|
||||||
.string()
|
.superRefine((val, ctx) => {
|
||||||
.regex(/^http(s)?:\/\/(www\.)?whereby.com\/[a-zA-Z0-9]*/)
|
if (
|
||||||
.optional()
|
eventLocationType &&
|
||||||
: selection?.value === LocationType.Around
|
!eventLocationType.default &&
|
||||||
? z
|
eventLocationType.linkType === "static" &&
|
||||||
.string()
|
eventLocationType.urlRegExp
|
||||||
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
|
) {
|
||||||
.optional()
|
const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(val).success;
|
||||||
: selection?.value === LocationType.Riverside
|
if (!valid) {
|
||||||
? z
|
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
||||||
.string()
|
ctx.addIssue({
|
||||||
.regex(/^http(s)?:\/\/(www\.)?riverside.fm\/studio\/[a-zA-Z0-9]*/)
|
code: z.ZodIssueCode.custom,
|
||||||
.optional()
|
message: `Invalid URL for ${eventLocationType.label}. ${
|
||||||
: z.string().url().optional(),
|
sampleUrl ? "Sample URL: " + sampleUrl : ""
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = z.string().url().optional().safeParse(val).success;
|
||||||
|
if (!valid) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid URL`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}),
|
||||||
displayLocationPublicly: z.boolean().optional(),
|
displayLocationPublicly: z.boolean().optional(),
|
||||||
locationPhoneNumber: z
|
locationPhoneNumber: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
.nullable()
|
||||||
|
.refine((val) => {
|
||||||
|
if (val === null) return false;
|
||||||
|
return isValidPhoneNumber(val);
|
||||||
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const locationFormMethods = useForm<LocationFormValues>({
|
const locationFormMethods = useForm({
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
resolver: zodResolver(locationFormSchema),
|
resolver: zodResolver(locationFormSchema),
|
||||||
});
|
});
|
||||||
@@ -114,28 +147,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
name: "locationType",
|
name: "locationType",
|
||||||
});
|
});
|
||||||
|
|
||||||
const LocationOptions =
|
const eventLocationType = getEventLocationType(selectedLocation);
|
||||||
selectedLocation === LocationType.InPerson ? (
|
|
||||||
<>
|
const defaultLocation = defaultValues?.find(
|
||||||
|
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
||||||
|
);
|
||||||
|
|
||||||
|
const LocationOptions = (() => {
|
||||||
|
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
||||||
|
if (!eventLocationType.variable) {
|
||||||
|
console.error("eventLocationType.variable can't be undefined");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
|
||||||
{t("set_address_place")}
|
{t(eventLocationType.messageForOrganizer || "")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<input
|
<LocationInput
|
||||||
type="text"
|
locationFormMethods={locationFormMethods}
|
||||||
{...locationFormMethods.register("locationAddress")}
|
eventLocationType={eventLocationType}
|
||||||
id="address"
|
id="locationInput"
|
||||||
|
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||||
required
|
required
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
defaultValue={
|
defaultValue={
|
||||||
defaultValues
|
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
||||||
? defaultValues.find(
|
|
||||||
(location: { type: LocationType }) => location.type === LocationType.InPerson
|
|
||||||
)?.address
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ErrorMessage
|
||||||
|
errors={locationFormMethods.formState.errors}
|
||||||
|
name={eventLocationType.variable}
|
||||||
|
className="mt-1 text-sm text-red-500"
|
||||||
|
as="p"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!booking && (
|
{!booking && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
@@ -144,12 +192,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
control={locationFormMethods.control}
|
control={locationFormMethods.control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
defaultChecked={
|
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||||
defaultValues
|
|
||||||
? defaultValues.find((location) => location.type === LocationType.InPerson)
|
|
||||||
?.displayLocationPublicly
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
description={t("display_location_label")}
|
description={t("display_location_label")}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||||
@@ -161,226 +204,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
) : selectedLocation === LocationType.Link ? (
|
} else {
|
||||||
<div>
|
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
||||||
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
|
}
|
||||||
{t("set_link_meeting")}
|
})();
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...locationFormMethods.register("locationLink")}
|
|
||||||
required
|
|
||||||
id="link"
|
|
||||||
className="block w-full rounded-sm border-gray-300 sm:text-sm"
|
|
||||||
defaultValue={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find(
|
|
||||||
(location: { type: LocationType }) => location.type === LocationType.Link
|
|
||||||
)?.link
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{locationFormMethods.formState.errors.locationLink && (
|
|
||||||
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
description={t("display_location_label")}
|
|
||||||
defaultChecked={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find((location) => location.type === LocationType.Link)
|
|
||||||
?.displayLocationPublicly
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : selectedLocation === LocationType.UserPhone ? (
|
|
||||||
<div>
|
|
||||||
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_your_phone_number")}
|
|
||||||
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<PhoneInput<LocationFormValues>
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
name="locationPhoneNumber"
|
|
||||||
required
|
|
||||||
id="locationPhoneNumber"
|
|
||||||
placeholder={t("host_phone_number")}
|
|
||||||
defaultValue={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find(
|
|
||||||
(location: { type: LocationType }) => location.type === LocationType.UserPhone
|
|
||||||
)?.hostPhoneNumber
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{locationFormMethods.formState.errors.locationPhoneNumber && (
|
|
||||||
<p className="mt-1 text-sm text-red-500">{t("invalid_number")}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : selectedLocation === LocationType.Whereby ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_whereby_link")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...locationFormMethods.register("locationLink")}
|
|
||||||
id="wherebylink"
|
|
||||||
placeholder="https://www.whereby.com/cal"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find(
|
|
||||||
(location: { type: LocationType }) => location.type === LocationType.Whereby
|
|
||||||
)?.address
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
defaultChecked={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find((location) => location.type === LocationType.Whereby)
|
|
||||||
?.displayLocationPublicly
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) =>
|
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
|
||||||
}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : selectedLocation === LocationType.Around ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_around_link")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...locationFormMethods.register("locationLink")}
|
|
||||||
id="aroundlink"
|
|
||||||
placeholder="https://www.around.co/rick"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find(
|
|
||||||
(location: { type: LocationType }) => location.type === LocationType.Around
|
|
||||||
)?.address
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
defaultChecked={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find((location) => location.type === LocationType.Around)
|
|
||||||
?.displayLocationPublicly
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) =>
|
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
|
||||||
}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : selectedLocation === LocationType.Riverside ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_riverside_link")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...locationFormMethods.register("locationLink")}
|
|
||||||
id="aroundlink"
|
|
||||||
placeholder="https://www.riverside.fm/studio/rick"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find(
|
|
||||||
(location: { type: LocationType }) => location.type === LocationType.Riverside
|
|
||||||
)?.address
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
defaultChecked={
|
|
||||||
defaultValues
|
|
||||||
? defaultValues.find((location) => location.type === LocationType.Riverside)
|
|
||||||
?.displayLocationPublicly
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) =>
|
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
|
||||||
}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog}>
|
<Dialog open={isOpenDialog}>
|
||||||
@@ -388,7 +216,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<LocationMarkerIcon className="text-primary-600 h-6 w-6" />
|
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
@@ -403,7 +231,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
{booking && (
|
{booking && (
|
||||||
<>
|
<>
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
||||||
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
|
<p className="mb-2 ml-1 text-sm text-black">
|
||||||
|
{getHumanReadableLocationValue(booking.location, t)}
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Form
|
<Form
|
||||||
@@ -415,22 +245,29 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
if (newLocation === LocationType.InPerson) {
|
if (newLocation === LocationType.InPerson) {
|
||||||
details = {
|
details = {
|
||||||
address: values.locationAddress,
|
address: values.locationAddress,
|
||||||
displayLocationPublicly,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const eventLocationType = getEventLocationType(newLocation);
|
||||||
|
|
||||||
|
// TODO: There can be a property that tells if it is to be saved in `link`
|
||||||
if (
|
if (
|
||||||
newLocation === LocationType.Link ||
|
newLocation === LocationType.Link ||
|
||||||
newLocation === LocationType.Whereby ||
|
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
||||||
newLocation === LocationType.Around ||
|
|
||||||
newLocation === LocationType.Riverside
|
|
||||||
) {
|
) {
|
||||||
details = { link: values.locationLink, displayLocationPublicly };
|
details = { link: values.locationLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLocation === LocationType.UserPhone) {
|
if (newLocation === LocationType.UserPhone) {
|
||||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
details = { hostPhoneNumber: values.locationPhoneNumber };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventLocationType?.organizerInputType) {
|
||||||
|
details = {
|
||||||
|
...details,
|
||||||
|
displayLocationPublicly,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
saveLocation(newLocation, details);
|
saveLocation(newLocation, details);
|
||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
setSelectedLocation?.(undefined);
|
setSelectedLocation?.(undefined);
|
||||||
@@ -459,8 +296,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
? locationOptions.filter((location) => location.value !== "phone")
|
? locationOptions.filter((location) => location.value !== "phone")
|
||||||
: locationOptions
|
: locationOptions
|
||||||
}
|
}
|
||||||
isSearchable={false}
|
isSearchable
|
||||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
locationFormMethods.setValue("locationType", val.value);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ClockIcon } from "@heroicons/react/outline";
|
|
||||||
import { RescheduleResponse } from "pages/api/book/request-reschedule";
|
import { RescheduleResponse } from "pages/api/book/request-reschedule";
|
||||||
import React, { useState, Dispatch, SetStateAction } from "react";
|
import React, { useState, Dispatch, SetStateAction } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
@@ -6,9 +5,10 @@ import { useMutation } from "react-query";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
|
||||||
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
|
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
<ClockIcon className="m-auto h-6 w-6" />
|
<Icon.FiClock className="m-auto h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DialogHeader title={t("send_reschedule_request")} />
|
<DialogHeader title={t("send_reschedule_request")} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
@@ -23,6 +22,7 @@ import Dropdown, {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@calcom/ui/Dropdown";
|
} from "@calcom/ui/Dropdown";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
|
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
@@ -49,9 +49,8 @@ interface CreateEventTypeBtnProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||||
const { t } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// URL encoded params
|
// URL encoded params
|
||||||
const teamId: number | undefined =
|
const teamId: number | undefined =
|
||||||
typeof router.query.teamId === "string" && router.query.teamId
|
typeof router.query.teamId === "string" && router.query.teamId
|
||||||
@@ -139,7 +138,9 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|||||||
{ shallow: true }
|
{ shallow: true }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
if (!isLocaleReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
name="new-eventtype"
|
name="new-eventtype"
|
||||||
@@ -152,7 +153,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|||||||
options={props.options}
|
options={props.options}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogContent className="overflow-y-auto">
|
<DialogContent>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||||
@@ -286,14 +287,14 @@ export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => props.openModal(props.options[0])}
|
onClick={() => props.openModal(props.options[0])}
|
||||||
data-testid="new-event-type"
|
data-testid="new-event-type"
|
||||||
StartIcon={PlusIcon}
|
StartIcon={Icon.FiPlus}
|
||||||
disabled={!props.canAddEvents}>
|
disabled={!props.canAddEvents}>
|
||||||
{t("new_event_type_btn")}
|
{t("new_event_type_btn")}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
|
<Button EndIcon={Icon.FiChevronDown}>{t("new_event_type_btn")}</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
||||||
@@ -338,7 +339,7 @@ function CreateEventTeamsItem(props: {
|
|||||||
onSelect={() => props.openModal(props.option)}>
|
onSelect={() => props.openModal(props.option)}>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={props.option.name || ""}
|
alt={props.option.name || ""}
|
||||||
imageSrc={props.option.image}
|
imageSrc={props.option.image || WEBAPP_URL + "/" + props.option.slug + "/avatar.png"}
|
||||||
size={6}
|
size={6}
|
||||||
className="inline ltr:mr-2 rtl:ml-2"
|
className="inline ltr:mr-2 rtl:ml-2"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
import {
|
|
||||||
ClipboardCheckIcon,
|
|
||||||
ClockIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
import { Prisma, SchedulingType } from "@prisma/client";
|
import { Prisma, SchedulingType } from "@prisma/client";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
@@ -13,6 +5,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
|||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -37,41 +30,41 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classNames("text-neutral-500 dark:text-white", className)}>
|
<div className={classNames("text-gray-600 dark:text-white", className)}>
|
||||||
{eventType.description && (
|
{eventType.description && (
|
||||||
<h2 className="max-w-[280px] overflow-hidden text-ellipsis opacity-60 sm:max-w-[500px]">
|
<h2 className="max-w-[280px] overflow-hidden text-ellipsis sm:max-w-[500px]">
|
||||||
{eventType.description.substring(0, 100)}
|
{eventType.description.substring(0, 100)}
|
||||||
{eventType.description.length > 100 && "..."}
|
{eventType.description.length > 100 && "..."}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<ul className="mt-2 flex flex-wrap sm:flex-nowrap">
|
<ul className="mt-2 flex flex-wrap space-x-1 sm:flex-nowrap ">
|
||||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||||
<ClockIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<Icon.FiClock className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
{eventType.length}m
|
{eventType.length} {t("minutes")}
|
||||||
</li>
|
</li>
|
||||||
{eventType.schedulingType ? (
|
{eventType.schedulingType ? (
|
||||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||||
<UsersIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<Icon.FiUsers className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
||||||
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
||||||
</li>
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||||
<UserIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<Icon.FiUser className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
{t("1_on_1")}
|
{t("1_on_1")}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{recurringEvent?.count && recurringEvent.count > 0 && (
|
{recurringEvent?.count && recurringEvent.count > 0 && (
|
||||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||||
<RefreshIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<Icon.FiRefreshCw className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
{t("repeats_up_to", {
|
{t("repeats_up_to", {
|
||||||
count: recurringEvent.count,
|
count: recurringEvent.count,
|
||||||
})}
|
})}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{eventType.price > 0 && (
|
{eventType.price > 0 && (
|
||||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||||
<CreditCardIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<Icon.FiCreditCard className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={eventType.price / 100.0}
|
value={eventType.price / 100.0}
|
||||||
@@ -82,8 +75,8 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
|||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{eventType.requiresConfirmation && (
|
{eventType.requiresConfirmation && (
|
||||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||||
<ClipboardCheckIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
<Icon.FiCheckSquare className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
{t("requires_confirmation")}
|
{t("requires_confirmation")}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export type EventTypeDescriptionSafeProps = {
|
||||||
|
eventType: { description: string | null };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||||
|
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||||
|
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
||||||
|
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
||||||
|
return <div {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventTypeDescriptionSafeHTML;
|
||||||
@@ -4,7 +4,7 @@ import { UseFormReturn } from "react-hook-form";
|
|||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
@@ -16,6 +16,10 @@ type RecurringEventControllerProps = {
|
|||||||
onRecurringEventDefined: (value: boolean) => void;
|
onRecurringEventDefined: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* use component from `/apps/web/components/v2/eventtype/RecurringEventController` instead
|
||||||
|
**/
|
||||||
export default function RecurringEventController({
|
export default function RecurringEventController({
|
||||||
recurringEvent,
|
recurringEvent,
|
||||||
formMethods,
|
formMethods,
|
||||||
@@ -43,8 +47,8 @@ export default function RecurringEventController({
|
|||||||
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
|
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex items-start">
|
<div className="flex items-start">
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-start">
|
||||||
<input
|
<input
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onRecurringEventDefined(event?.target.checked);
|
onRecurringEventDefined(event?.target.checked);
|
||||||
@@ -82,7 +86,7 @@ export default function RecurringEventController({
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="block w-16 rounded-sm border-gray-300 [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
className="block w-16 rounded-sm border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
defaultValue={recurringEventState.interval}
|
defaultValue={recurringEventState.interval}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
@@ -97,7 +101,7 @@ export default function RecurringEventController({
|
|||||||
options={recurringEventFreqOptions}
|
options={recurringEventFreqOptions}
|
||||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="w-18 block min-w-0 rounded-sm sm:text-sm"
|
className="w-18 block min-w-0 rounded-sm text-sm"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -114,7 +118,7 @@ export default function RecurringEventController({
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="block w-16 rounded-sm border-gray-300 [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
className="block w-16 rounded-sm border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
defaultValue={recurringEventState.count}
|
defaultValue={recurringEventState.count}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { LinkIcon } from "@heroicons/react/outline";
|
|
||||||
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon, UserIcon } from "@heroicons/react/solid";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
|
||||||
|
/** @deprecated Use `apps/web/components/v2/eventtype/SkeletonLoader.tsx` */
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
<div className="mb-4 flex items-center">
|
<div className="mb-4 flex items-center">
|
||||||
<SkeletonAvatar width="8" height="8" />
|
<SkeletonAvatar width="8" height="8" />
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<SkeletonText height="4" width="16" />
|
<SkeletonText className="h-4 w-16" />
|
||||||
<SkeletonText height="4" width="24" />
|
<SkeletonText className="h-4 w-24" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
@@ -35,29 +33,16 @@ function SkeletonItem() {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<ClockIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||||
<SkeletonText width="12" height="4" />
|
<SkeletonText width="12" height="4" />
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<UserIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||||
<SkeletonText width="16" height="4" />
|
<SkeletonText width="16" height="4" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
|
|
||||||
<div className="flex justify-between rtl:space-x-reverse">
|
|
||||||
<div className="btn-icon appearance-none">
|
|
||||||
<ExternalLinkIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="btn-icon appearance-none">
|
|
||||||
<LinkIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="btn-icon appearance-none">
|
|
||||||
<DotsHorizontalIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { InstallAppButtonWithoutPlanCheck } from "@calcom/app-store/components";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { App } from "@calcom/types/App";
|
||||||
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
|
||||||
|
interface ICalendarItem {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
imageSrc: string;
|
||||||
|
type: App["type"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarItem = (props: ICalendarItem) => {
|
||||||
|
const { title, imageSrc, type } = props;
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center p-5">
|
||||||
|
<img src={imageSrc} alt={title} className="h-8 w-8" />
|
||||||
|
<p className="mx-3 text-sm font-bold">{title}</p>
|
||||||
|
|
||||||
|
<InstallAppButtonWithoutPlanCheck
|
||||||
|
type={type}
|
||||||
|
render={(buttonProps) => (
|
||||||
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
|
color="secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={(event) => {
|
||||||
|
// Save cookie key to return url step
|
||||||
|
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||||
|
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
||||||
|
}}
|
||||||
|
className="ml-auto rounded-md border border-gray-200 py-[10px] px-4 text-sm font-bold">
|
||||||
|
{t("connect")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CalendarItem };
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useMutation } from "react-query";
|
||||||
|
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Switch } from "@calcom/ui/v2";
|
||||||
|
|
||||||
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
interface ICalendarSwitchProps {
|
||||||
|
title: string;
|
||||||
|
externalId: string;
|
||||||
|
type: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
name: string;
|
||||||
|
isLastItemInList?: boolean;
|
||||||
|
}
|
||||||
|
const CalendarSwitch = (props: ICalendarSwitchProps) => {
|
||||||
|
const { title, externalId, type, isChecked, name, isLastItemInList = false } = props;
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const mutation = useMutation<
|
||||||
|
unknown,
|
||||||
|
unknown,
|
||||||
|
{
|
||||||
|
isOn: boolean;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
async ({ isOn }: { isOn: boolean }) => {
|
||||||
|
const body = {
|
||||||
|
integration: type,
|
||||||
|
externalId: externalId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOn) {
|
||||||
|
const res = await fetch("/api/availability/calendar", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Something went wrong");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await fetch("/api/availability/calendar", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Something went wrong");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
async onSettled() {
|
||||||
|
await utils.invalidateQueries(["viewer.integrations"]);
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
showToast(`Something went wrong when toggling "${title}""`, "error");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className={classNames("flex flex-row items-center", !isLastItemInList ? "mb-4" : "")}>
|
||||||
|
<div className="flex pl-2">
|
||||||
|
<Switch
|
||||||
|
id={externalId}
|
||||||
|
defaultChecked={isChecked}
|
||||||
|
onCheckedChange={(isOn: boolean) => {
|
||||||
|
mutation.mutate({ isOn });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label className="ml-3 text-sm font-medium leading-5" htmlFor={externalId}>
|
||||||
|
{name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { CalendarSwitch };
|
||||||
Loaded 100 of 1244 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user