@meetelise/chat 1.13.10 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,7 +50,7 @@ export class Launcher extends LitElement {
50
50
  onChatTapped: () => void = () => {
51
51
  return;
52
52
  };
53
- @property()
53
+ @property({ attribute: false })
54
54
  launcherStyles: StyleInfo = {};
55
55
 
56
56
  @state()
@@ -85,7 +85,13 @@ export class Launcher extends LitElement {
85
85
  );
86
86
  this.hasTextUsEnabled =
87
87
  registeredPhoneNumbers.length > 0 && this.buildingId !== 4895;
88
- if (this.buildingId === 3660) {
88
+ // TODO: replace this with a real API call once the endpoint exists
89
+ const schedulingIsEnabled = await (async function putApiCallHere(
90
+ buildingId: number
91
+ ) {
92
+ return !!buildingId;
93
+ })(this.buildingId);
94
+ if (this.buildingId === 3660 && schedulingIsEnabled) {
89
95
  this.hasSSTEnabled = true;
90
96
  }
91
97
  }
@@ -12,6 +12,7 @@ import "./me-select.ts";
12
12
  import {
13
13
  DateWithTimeZoneOffset,
14
14
  getAvailabilitiesGroupedByDay,
15
+ getExistenceOfAvailabilitiesByTourType,
15
16
  } from "../../getAvailabilities";
16
17
  import { TourAvailabilityResponseRankOrderedSupportedTourTypesEnum } from "@meetelise/rest-sdk";
17
18
  import { format } from "date-fns";
@@ -22,6 +23,7 @@ import { LabeledOption } from "../../fetchBuildingInfo";
22
23
  import { isMobile } from "../../utils";
23
24
  import axios from "axios";
24
25
  import { mapValues } from "lodash";
26
+ import classnames from "classnames";
25
27
 
26
28
  @customElement("tour-scheduler")
27
29
  export class TourScheduler extends LitElement {
@@ -37,6 +39,12 @@ export class TourScheduler extends LitElement {
37
39
  @state()
38
40
  private tourType = TourType.Guided;
39
41
  @state()
42
+ private shouldShowTourType = {
43
+ [TourType.Guided]: true,
44
+ [TourType.Self]: true,
45
+ [TourType.Virtual]: true,
46
+ };
47
+ @state()
40
48
  private email = "";
41
49
  @state()
42
50
  private phoneNumber = "";
@@ -45,6 +53,8 @@ export class TourScheduler extends LitElement {
45
53
  [day: string]: DateWithTimeZoneOffset[];
46
54
  } = {};
47
55
  @state()
56
+ private waitingForAvailabilities = true;
57
+ @state()
48
58
  private selectedDate?: Date;
49
59
  @state()
50
60
  private selectedTime?: DateWithTimeZoneOffset | null;
@@ -53,11 +63,13 @@ export class TourScheduler extends LitElement {
53
63
  @state()
54
64
  private tourIsBooked = false;
55
65
 
56
- @query("input#name")
57
- nameInput!: HTMLInputElement;
58
- @query("input#email")
66
+ @query(".inputContainer#firstName input")
67
+ firstNameInput!: HTMLInputElement;
68
+ @query(".inputContainer#lastName input")
69
+ lastNameInput!: HTMLInputElement;
70
+ @query(".inputContainer#email input")
59
71
  emailInput!: HTMLInputElement;
60
- @query("input#phone")
72
+ @query(".inputContainer#phone input")
61
73
  phoneInput!: HTMLInputElement;
62
74
  @query("me-select#unitType")
63
75
  unitTypeSelect!: MESelect;
@@ -66,6 +78,23 @@ export class TourScheduler extends LitElement {
66
78
  this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDay(
67
79
  tourTypeMap[this.tourType]
68
80
  );
81
+ this.waitingForAvailabilities = false;
82
+
83
+ // Show a tour type only if it is supported by the building and has
84
+ // time slots available.
85
+ const availabilitiesExistForTourType =
86
+ await getExistenceOfAvailabilitiesByTourType();
87
+ this.shouldShowTourType = {
88
+ [TourType.Guided]:
89
+ this.tourTypeOptions.map((o) => o.value).includes("WITH_AGENT") &&
90
+ availabilitiesExistForTourType[TourType.Guided],
91
+ [TourType.Self]:
92
+ this.tourTypeOptions.map((o) => o.value).includes("SELF_GUIDED") &&
93
+ availabilitiesExistForTourType[TourType.Self],
94
+ [TourType.Virtual]:
95
+ this.tourTypeOptions.map((o) => o.value).includes("VIRTUAL_SHOWING") &&
96
+ availabilitiesExistForTourType[TourType.Virtual],
97
+ };
69
98
  };
70
99
 
71
100
  protected willUpdate = async (
@@ -236,7 +265,7 @@ export class TourScheduler extends LitElement {
236
265
  dateAndTime: (): boolean => !!this.selectedDate && !!this.selectedTime,
237
266
  leadInfo: (): boolean => {
238
267
  return (
239
- !!this.nameInput?.value &&
268
+ (!!this.firstNameInput?.value || !!this.lastNameInput?.value) &&
240
269
  this.emailInput?.value.includes("@") &&
241
270
  // TODO: deleting phone number doesn't cause validation to fail, at least on mobile
242
271
  !!this.phoneNumber &&
@@ -273,9 +302,8 @@ export class TourScheduler extends LitElement {
273
302
  email_address: this.email,
274
303
  phone_number: `+1${this.phoneNumber.match(/\d/g)?.join("")}`, // e.g. +12125555555
275
304
  building_id: this.buildingId,
276
- // TODO: this is very bad dumb name-splitting logic! Instead, split the name input into first and last name.
277
- first_name: this.nameInput.value.split(" ")[0],
278
- last_name: this.nameInput.value.split(" ").slice(1).join(" "),
305
+ first_name: this.firstNameInput.value,
306
+ last_name: this.lastNameInput.value,
279
307
  tour_type: tourTypeForSubmission[this.tourType],
280
308
  tour_time: `${this.selectedTime.datetime}${this.selectedTime.offset}`, // e.g., "2022-06-27T09:00:00-07:00"
281
309
  };
@@ -403,7 +431,7 @@ export class TourScheduler extends LitElement {
403
431
  gap: 12px;
404
432
  }
405
433
 
406
- #yourInformationMenu > input {
434
+ #yourInformationMenu input {
407
435
  width: 305px;
408
436
  height: 49px;
409
437
  border: 1px solid #83818e;
@@ -415,7 +443,7 @@ export class TourScheduler extends LitElement {
415
443
  color: #202020;
416
444
  }
417
445
 
418
- #yourInformationMenu > input::placeholder {
446
+ #yourInformationMenu input::placeholder {
419
447
  color: #202020;
420
448
  }
421
449
 
@@ -475,6 +503,15 @@ export class TourScheduler extends LitElement {
475
503
  box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
476
504
  }
477
505
 
506
+ button#schedule:not(:disabled):hover {
507
+ opacity: 0.7;
508
+ }
509
+
510
+ button#schedule:not(:disabled):active {
511
+ box-shadow: none;
512
+ opacity: 1;
513
+ }
514
+
478
515
  button#schedule:disabled {
479
516
  background: #e7e7e7;
480
517
  box-shadow: none;
@@ -489,6 +526,81 @@ export class TourScheduler extends LitElement {
489
526
  font-size: 18px;
490
527
  }
491
528
 
529
+ /* Loading styles: pulsing gray overlay on all the form elements */
530
+
531
+ @keyframes spin {
532
+ 0% {
533
+ transform: none;
534
+ }
535
+ 50% {
536
+ transform: rotateZ(180deg);
537
+ }
538
+ 100% {
539
+ transform: rotateZ(360deg);
540
+ }
541
+ }
542
+
543
+ svg#loadingIcon {
544
+ animation: spin 2s infinite linear;
545
+ }
546
+
547
+ .tour-scheduler.loading #scheduleATour {
548
+ display: flex;
549
+ gap: 10px;
550
+ }
551
+
552
+ @keyframes loadingPulse {
553
+ 0% {
554
+ background-color: #e7e7e7;
555
+ }
556
+ 50% {
557
+ background-color: white;
558
+ }
559
+ 100% {
560
+ background-color: #e7e7e7;
561
+ }
562
+ }
563
+
564
+ tour-type-option,
565
+ date-picker,
566
+ #yourInformationMenu .inputContainer {
567
+ position: relative;
568
+ }
569
+
570
+ .tour-scheduler.loading
571
+ :is(tour-type-option, date-picker, #yourInformationMenu
572
+ .inputContainer)::after {
573
+ content: "";
574
+ position: absolute;
575
+ top: 0;
576
+ left: 0;
577
+ height: 100%;
578
+ z-index: 1;
579
+ animation: loadingPulse 2s infinite;
580
+ }
581
+
582
+ .tour-scheduler.loading tour-type-option::after {
583
+ border-radius: 10px;
584
+ width: 200px;
585
+ }
586
+
587
+ .tour-scheduler.loading date-picker::after {
588
+ border-radius: 10px;
589
+ width: 205px;
590
+ }
591
+
592
+ .tour-scheduler.loading #yourInformationMenu .inputContainer::after {
593
+ width: 100%;
594
+ }
595
+
596
+ .tour-scheduler.loading #yourInformationMenu .inputContainer input {
597
+ visibility: hidden;
598
+ }
599
+
600
+ .tour-scheduler.loading time-picker {
601
+ display: none;
602
+ }
603
+
492
604
  @media (max-width: 767px) {
493
605
  /* TODO: separate styles into general, desktop-specific, and mobile-specific.
494
606
  basically everything I have "unset" or "initial" on should become desktop-specific. the grid layout is only for desktop.
@@ -605,7 +717,7 @@ export class TourScheduler extends LitElement {
605
717
  tourTypeMenu(): TemplateResult {
606
718
  return html`<h2 id="tourType">Tour Type</h2>
607
719
  <div id="tourTypeMenu">
608
- ${this.tourTypeOptions.map((o) => o.value).includes("WITH_AGENT")
720
+ ${this.shouldShowTourType[TourType.Guided]
609
721
  ? html` <tour-type-option
610
722
  heading="Guided tour"
611
723
  subtitle="with an agent"
@@ -637,7 +749,7 @@ export class TourScheduler extends LitElement {
637
749
  </svg>
638
750
  </tour-type-option>`
639
751
  : ""}
640
- ${this.tourTypeOptions.map((o) => o.value).includes("SELF_GUIDED")
752
+ ${this.shouldShowTourType[TourType.Self]
641
753
  ? html`<tour-type-option
642
754
  heading="Take a tour"
643
755
  subtitle="on your own"
@@ -669,7 +781,7 @@ export class TourScheduler extends LitElement {
669
781
  </svg>
670
782
  </tour-type-option>`
671
783
  : ""}
672
- ${this.tourTypeOptions.map((o) => o.value).includes("VIRTUAL_SHOWING")
784
+ ${this.shouldShowTourType[TourType.Virtual]
673
785
  ? html`<tour-type-option
674
786
  heading="Virtual tour"
675
787
  subtitle="over video"
@@ -739,7 +851,8 @@ export class TourScheduler extends LitElement {
739
851
  .map((date) => format(new Date(date.datetime), "h:mmaaa"))
740
852
  .indexOf(e.target.selectedTime)
741
853
  : null;
742
- this.selectedTime = index ? daysAvailabilities[index] : null; // this.selectedAvailabilityString ?
854
+ this.selectedTime =
855
+ index !== null ? daysAvailabilities[index] : null;
743
856
  }
744
857
  }}
745
858
  ></time-picker>
@@ -807,38 +920,56 @@ export class TourScheduler extends LitElement {
807
920
  userInfoAndLayoutMenu(): TemplateResult {
808
921
  return html`<h2 id="yourInformation">Your information</h2>
809
922
  <div id="yourInformationMenu">
810
- <input
811
- type="text"
812
- placeholder="Name"
813
- id="name"
814
- @input=${() => this.requestUpdate()}
815
- />
816
- <input
817
- type="email"
818
- inputmode="email"
819
- placeholder="Email"
820
- id="email"
821
- .value=${this.email}
822
- @input=${this.onChangeEmail}
823
- />
824
- <input
825
- type="tel"
826
- inputmode="tel"
827
- placeholder="Phone"
828
- id="phone"
829
- maxlength="14"
830
- .value=${this.phoneNumber}
831
- @keydown=${this.handlePhoneKeydown}
832
- @keyup=${this.handlePhoneKeyup}
833
- @input=${(e: Event) => {
834
- if (!e.target) {
835
- return;
836
- }
837
- this.phoneNumber = formatToPhone(
838
- (e.target as HTMLInputElement).value
839
- );
840
- }}
841
- />
923
+ <div class="inputContainer" id="firstName">
924
+ <input
925
+ type="text"
926
+ placeholder="First name"
927
+ name="firstName"
928
+ autocomplete="given-name"
929
+ @input=${() => this.requestUpdate()}
930
+ />
931
+ </div>
932
+ <div class="inputContainer" id="lastName">
933
+ <input
934
+ type="text"
935
+ placeholder="Last name"
936
+ name="lastName"
937
+ autocomplete="family-name"
938
+ @input=${() => this.requestUpdate()}
939
+ />
940
+ </div>
941
+ <div class="inputContainer" id="email">
942
+ <input
943
+ type="email"
944
+ inputmode="email"
945
+ placeholder="Email"
946
+ name="email"
947
+ autocomplete="email"
948
+ .value=${this.email}
949
+ @input=${this.onChangeEmail}
950
+ />
951
+ </div>
952
+ <div class="inputContainer" id="phone">
953
+ <input
954
+ type="tel"
955
+ inputmode="tel"
956
+ placeholder="Phone"
957
+ name="phone"
958
+ autocomplete="tel-national"
959
+ maxlength="14"
960
+ .value=${this.phoneNumber}
961
+ @keydown=${this.handlePhoneKeydown}
962
+ @keyup=${this.handlePhoneKeyup}
963
+ @input=${(e: Event) => {
964
+ if (!e.target) {
965
+ return;
966
+ }
967
+ this.phoneNumber = formatToPhone(
968
+ (e.target as HTMLInputElement).value
969
+ );
970
+ }}
971
+ />
972
+ </div>
842
973
  </div>
843
974
  <!--
844
975
  Layout dropdown would go here, but has been removed pending backend support.
@@ -871,7 +1002,13 @@ export class TourScheduler extends LitElement {
871
1002
  <p>
872
1003
  Thank you!
873
1004
  <br />
874
- Your guided tour is scheduled for ${readableDateAndTime}.
1005
+ Your
1006
+ ${{
1007
+ [TourType.Guided]: "guided",
1008
+ [TourType.Self]: "self-guided",
1009
+ [TourType.Virtual]: "virtual",
1010
+ }[this.tourType]}
1011
+ tour is scheduled for ${readableDateAndTime}.
875
1012
  </p>
876
1013
  <p>
877
1014
  Look for an email confirmation along with instructions and directions.
@@ -881,11 +1018,35 @@ export class TourScheduler extends LitElement {
881
1018
  `;
882
1019
  }
883
1020
 
1021
+ loadingIcon(): TemplateResult {
1022
+ return html`<svg
1023
+ id="loadingIcon"
1024
+ width="21"
1025
+ height="21"
1026
+ viewBox="0 0 21 21"
1027
+ fill="none"
1028
+ xmlns="http://www.w3.org/2000/svg"
1029
+ >
1030
+ <path
1031
+ d="M17.835 13.1045C18.4839 11.5628 18.6332 9.85647 18.2621 8.22548C17.8909 6.5945 17.018 5.12084 15.7659 4.0117C14.5139 2.90256 12.9457 2.21372 11.2818 2.04201C9.618 1.87031 7.94218 2.22438 6.49 3.05445L5.498 1.31745C7.01563 0.450066 8.73419 -0.00418222 10.4822 2.90165e-05C12.2302 0.00424025 13.9466 0.466764 15.46 1.34145C19.95 3.93345 21.67 9.48345 19.577 14.1115L20.919 14.8855L16.754 17.0995L16.589 12.3855L17.835 13.1045ZM3.085 6.89845C2.43614 8.44015 2.28678 10.1464 2.65792 11.7774C3.02905 13.4084 3.90201 14.8821 5.15407 15.9912C6.40613 17.1003 7.97432 17.7892 9.63816 17.9609C11.302 18.1326 12.9778 17.7785 14.43 16.9485L15.422 18.6855C13.9044 19.5528 12.1858 20.0071 10.4378 20.0029C8.68979 19.9987 6.97344 19.5361 5.46 18.6615C0.97 16.0695 -0.75 10.5195 1.343 5.89145L0 5.11845L4.165 2.90445L4.33 7.61845L3.084 6.89945L3.085 6.89845Z"
1032
+ fill="#1E1E1E"
1033
+ />
1034
+ </svg>`;
1035
+ }
1036
+
884
1037
  render(): TemplateResult {
885
1038
  if (!isMobile()) {
886
1039
  return html`
887
- <div class="tour-scheduler">
888
- <h1 id="scheduleATour">Schedule a tour</h1>
1040
+ <div
1041
+ class="${classnames("tour-scheduler", {
1042
+ loading: this.waitingForAvailabilities,
1043
+ })}"
1044
+ >
1045
+ <h1 id="scheduleATour">
1046
+ ${this.waitingForAvailabilities
1047
+ ? html`${this.loadingIcon()} Searching availabilities...`
1048
+ : "Schedule a tour"}
1049
+ </h1>
889
1050
  ${this.closeButton()}
890
1051
  ${this.tourIsBooked
891
1052
  ? html`
@@ -910,9 +1071,17 @@ export class TourScheduler extends LitElement {
910
1071
  } else {
911
1072
  const currentPage = this.mobilePages[this.mobilePageIndex];
912
1073
  return html`
913
- <div class="tour-scheduler">
1074
+ <div
1075
+ class="${classnames("tour-scheduler", {
1076
+ loading: this.waitingForAvailabilities,
1077
+ })}"
1078
+ >
914
1079
  <div id="topControls">
915
- <h1 id="scheduleATour">Schedule a tour</h1>
1080
+ <h1 id="scheduleATour">
1081
+ ${this.waitingForAvailabilities
1082
+ ? html`${this.loadingIcon()} Searching availabilities...`
1083
+ : "Schedule a tour"}
1084
+ </h1>
916
1085
  ${this.closeButton()}
917
1086
  </div>
918
1087
  ${this.tourIsBooked
@@ -922,7 +1091,9 @@ export class TourScheduler extends LitElement {
922
1091
  id="next"
923
1092
  @click=${currentPage.nextButtonAction}
924
1093
  ?disabled=${(() => {
925
- return !currentPage.validate();
1094
+ return (
1095
+ !currentPage.validate() || this.waitingForAvailabilities
1096
+ );
926
1097
  })()}
927
1098
  >
928
1099
  ${currentPage.nextButtonText}
@@ -939,8 +1110,6 @@ export enum TourType {
939
1110
  Virtual,
940
1111
  }
941
1112
 
942
- // TODO: we have three UI options and five TourAvailabilityResponseRankOrderedSupportedTourTypesEnum values
943
- // how should they map?
944
1113
  const tourTypeMap = {
945
1114
  [TourType.Guided]:
946
1115
  TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.WithAgent,
@@ -10,6 +10,7 @@ import {
10
10
  TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
11
11
  } from "@meetelise/rest-sdk";
12
12
  import groupBy from "lodash/groupBy";
13
+ import { TourType } from "./WebComponent/Scheduler/tour-scheduler";
13
14
 
14
15
  const availabilitiesCache: {
15
16
  buildingId?: number | null;
@@ -85,6 +86,39 @@ export interface DateWithTimeZoneOffset {
85
86
  offset: string;
86
87
  }
87
88
 
89
+ /**
90
+ * Returns an object that reveals whether each tour type supported by
91
+ * `tour-scheduler` has availabilities (time slots available for scheduling) in
92
+ * the time window of interest.
93
+ *
94
+ * Note that the existence of current availabilities is distinct from the
95
+ * question of whether the community supports the tour type at all. The first
96
+ * implies the second but not vice versa.
97
+ */
98
+ export const getExistenceOfAvailabilitiesByTourType = async (): Promise<{
99
+ [TourType.Guided]: boolean;
100
+ [TourType.Self]: boolean;
101
+ [TourType.Virtual]: boolean;
102
+ }> => {
103
+ return {
104
+ [TourType.Guided]: !!(
105
+ await getAvailabilitiesForTourType(
106
+ TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.WithAgent
107
+ )
108
+ )?.availableTourStartTimes?.length,
109
+ [TourType.Self]: !!(
110
+ await getAvailabilitiesForTourType(
111
+ TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.SelfGuided
112
+ )
113
+ )?.availableTourStartTimes?.length,
114
+ [TourType.Virtual]: !!(
115
+ await getAvailabilitiesForTourType(
116
+ TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.VirtualShowing
117
+ )
118
+ )?.availableTourStartTimes?.length,
119
+ };
120
+ };
121
+
88
122
  export const getAvailabilitiesGroupedByDay = async (
89
123
  tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
90
124
  buildingId?: number