@meetelise/chat 1.20.2 → 1.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ import { classMap } from "lit/directives/class-map.js";
11
11
  import { installCallUsWindow } from "./actions/call-us-window";
12
12
  import { getRegisteredPhoneNumbers } from "../getRegisteredPhoneNumbers";
13
13
  import { TourScheduler } from "./Scheduler/tour-scheduler";
14
- import { LabeledOption } from "../fetchBuildingInfo";
14
+ import { LabeledOption, UnitV2 } from "../fetchBuildingInfo";
15
15
 
16
16
  @customElement("meetelise-launcher")
17
17
  export class Launcher extends LitElement {
@@ -53,7 +53,9 @@ export class Launcher extends LitElement {
53
53
  @property({ type: Boolean })
54
54
  hasTextUsEnabled = false;
55
55
  @property({ attribute: false })
56
- layoutOptions: LabeledOption[] = [];
56
+ layoutOptions: string[] = [];
57
+ @property({ attribute: false })
58
+ unitOptions: UnitV2[] = [];
57
59
  @property({ attribute: false })
58
60
  tourTypeOptions: LabeledOption[] = [];
59
61
  @property({ attribute: false })
@@ -294,6 +296,7 @@ export class Launcher extends LitElement {
294
296
  orgSlug="${this.orgSlug}"
295
297
  buildingSlug="${this.buildingSlug}"
296
298
  .layoutOptions=${this.layoutOptions}
299
+ .unitOptions=${this.unitOptions}
297
300
  .tourTypeOptions=${this.tourTypeOptions}
298
301
  buildingId=${this.buildingId}
299
302
  ${ref(this.tourSchedulerRef)}
@@ -2,10 +2,15 @@ import { LitElement, html, TemplateResult, css } from "lit";
2
2
  import { property, state, query, customElement } from "lit/decorators.js";
3
3
  import { classMap } from "lit/directives/class-map.js";
4
4
 
5
+ type MeSelectOption = {
6
+ label: string;
7
+ value: string;
8
+ };
9
+
5
10
  @customElement("me-select")
6
11
  export class MESelect extends LitElement {
7
12
  @property({ attribute: false })
8
- options: string[] = [];
13
+ options: MeSelectOption[] = [];
9
14
 
10
15
  @property({ type: String })
11
16
  placeholder?: string = "Select";
@@ -14,7 +19,7 @@ export class MESelect extends LitElement {
14
19
  value?: string;
15
20
 
16
21
  @state()
17
- private activeOption: string | null = null;
22
+ private activeOption: MeSelectOption | null = null;
18
23
 
19
24
  @state()
20
25
  private isOpen?: boolean = false;
@@ -29,8 +34,8 @@ export class MESelect extends LitElement {
29
34
  this.isOpen = !this.isOpen;
30
35
  };
31
36
 
32
- setSelectedOption = (option: string, closeSelect = true): void => {
33
- this.value = option;
37
+ setSelectedOption = (option: MeSelectOption, closeSelect = true): void => {
38
+ this.value = option.value;
34
39
  if (closeSelect) {
35
40
  this.isOpen = !this.isOpen;
36
41
  this.activeOption = null;
@@ -103,6 +108,7 @@ export class MESelect extends LitElement {
103
108
  overflow-y: scroll;
104
109
  position: absolute;
105
110
  min-height: 40px;
111
+ min-width: 144px;
106
112
  max-width: 400px;
107
113
  width: max-content;
108
114
  background-color: white;
@@ -112,7 +118,17 @@ export class MESelect extends LitElement {
112
118
  box-sizing: border-box;
113
119
  box-shadow: 0px 4px 14px rgba(0, 0, 0, 0.15);
114
120
  border-radius: 10px;
115
- overflow: hidden;
121
+ }
122
+
123
+ ::-webkit-scrollbar {
124
+ -webkit-appearance: none;
125
+ width: 8px;
126
+ }
127
+
128
+ ::-webkit-scrollbar-thumb {
129
+ border-radius: 10px;
130
+ background-color: rgba(0, 0, 0, 0.4);
131
+ -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
116
132
  }
117
133
 
118
134
  .option {
@@ -228,7 +244,7 @@ export class MESelect extends LitElement {
228
244
  active: this.activeOption === option,
229
245
  })}"
230
246
  >
231
- ${option}
247
+ ${option.label}
232
248
  </li>`
233
249
  )}
234
250
  </ul>`
@@ -1,11 +1,27 @@
1
1
  import { LitElement, html, TemplateResult, css } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
  import { classMap } from "lit/directives/class-map.js";
4
+ import { DateWithTimeZoneOffset } from "../../getAvailabilities";
5
+
6
+ export type TimePickerOption = {
7
+ displayTime: string;
8
+ dateWithTimeZoneOffset: DateWithTimeZoneOffset;
9
+ };
10
+
11
+ const getDateWithTimezoneOffset = (
12
+ options: TimePickerOption[],
13
+ displayTime: string
14
+ ) => {
15
+ return options.reduce(function (map: Record<string, TimePickerOption>, obj) {
16
+ map[obj.displayTime] = obj;
17
+ return map;
18
+ }, {})[displayTime];
19
+ };
4
20
 
5
21
  @customElement("time-picker")
6
22
  export class TimePicker extends LitElement {
7
23
  @property({ attribute: false })
8
- options: string[] = [];
24
+ options: TimePickerOption[] = [];
9
25
 
10
26
  @property({ type: Boolean })
11
27
  selectedDateExists = false;
@@ -14,10 +30,10 @@ export class TimePicker extends LitElement {
14
30
  horizontal = false;
15
31
 
16
32
  @state()
17
- private selected?: string;
33
+ private selected?: TimePickerOption;
18
34
 
19
35
  @property({ attribute: false })
20
- get selectedTime(): undefined | string {
36
+ get selectedTime(): undefined | TimePickerOption {
21
37
  return this.selected;
22
38
  }
23
39
 
@@ -100,8 +116,12 @@ export class TimePicker extends LitElement {
100
116
  class=${this.horizontal ? "horizontal" : ""}
101
117
  @click="${(e: MouseEvent) => {
102
118
  const target = e.target as HTMLElement | undefined;
119
+
103
120
  if (target?.closest(".option:not(.selected)"))
104
- this.selected = target.innerText;
121
+ this.selected = getDateWithTimezoneOffset(
122
+ this.options,
123
+ target.innerText
124
+ );
105
125
  this.dispatchEvent(
106
126
  new Event("change", { bubbles: true, composed: true })
107
127
  );
@@ -114,8 +134,8 @@ export class TimePicker extends LitElement {
114
134
  ) {
115
135
  e.preventDefault();
116
136
  this.selected =
117
- target.innerText !== this.selected
118
- ? target.innerText
137
+ target.innerText !== this.selected?.displayTime
138
+ ? getDateWithTimezoneOffset(this.options, target.innerText)
119
139
  : undefined;
120
140
  this.dispatchEvent(
121
141
  new Event("change", { bubbles: true, composed: true })
@@ -127,11 +147,11 @@ export class TimePicker extends LitElement {
127
147
  (option) =>
128
148
  html`<div
129
149
  class="option ${classMap({
130
- selected: this.selected === option,
150
+ selected: this.selected?.displayTime === option.displayTime,
131
151
  })}"
132
152
  tabindex="0"
133
153
  >
134
- <span>${option}</span>
154
+ <span>${option.displayTime}</span>
135
155
  </div>`
136
156
  )}
137
157
  </div>
@@ -19,17 +19,35 @@ import { format } from "date-fns";
19
19
  import { DatePicker } from "./date-picker";
20
20
  import { MESelect } from "./me-select";
21
21
  import { TimePicker } from "./time-picker";
22
- import { LabeledOption } from "../../fetchBuildingInfo";
22
+ import { LabeledOption, UnitV2 } from "../../fetchBuildingInfo";
23
23
  import { isMobile } from "../../utils";
24
24
  import axios from "axios";
25
25
  import { mapValues } from "lodash";
26
26
  import classnames from "classnames";
27
27
  import parseISO from "date-fns/parseISO";
28
28
 
29
+ const getHumanReadableLayout = (layout: string) => {
30
+ if (layout == "studio") return "Studio";
31
+ return {
32
+ "1br": "1 bedroom",
33
+ "2br": "2 bedrooms",
34
+ "3br": "3 bedrooms",
35
+ "4br": "4 bedrooms",
36
+ "5br": "5 bedrooms",
37
+ "6br": "6 bedrooms",
38
+ "7br": "7 bedrooms",
39
+ "8br": "8 bedrooms",
40
+ "9br": "9 bedrooms",
41
+ "10br": "10 bedroom",
42
+ }[layout];
43
+ };
44
+
29
45
  @customElement("tour-scheduler")
30
46
  export class TourScheduler extends LitElement {
31
47
  @property({ attribute: false })
32
- layoutOptions: LabeledOption[] = [];
48
+ layoutOptions: string[] = [];
49
+ @property({ attribute: false })
50
+ unitOptions: UnitV2[] = [];
33
51
  @property({ attribute: false })
34
52
  tourTypeOptions: LabeledOption[] = [];
35
53
  @property({ type: Number })
@@ -69,9 +87,9 @@ export class TourScheduler extends LitElement {
69
87
  @state()
70
88
  private tourIsBooked = false;
71
89
 
72
- @query(".inputContainer#firstName input")
90
+ @query(".nameContainer#firstName input")
73
91
  firstNameInput!: HTMLInputElement;
74
- @query(".inputContainer#lastName input")
92
+ @query(".nameContainer#lastName input")
75
93
  lastNameInput!: HTMLInputElement;
76
94
  @query(".inputContainer#email input")
77
95
  emailInput!: HTMLInputElement;
@@ -79,6 +97,8 @@ export class TourScheduler extends LitElement {
79
97
  phoneInput!: HTMLInputElement;
80
98
  @query("me-select#unitType")
81
99
  unitTypeSelect!: MESelect;
100
+ @query("me-select#layoutType")
101
+ layoutTypeSelect!: MESelect;
82
102
 
83
103
  firstUpdated = async (): Promise<void> => {
84
104
  this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDay(
@@ -312,21 +332,25 @@ export class TourScheduler extends LitElement {
312
332
  last_name: this.lastNameInput.value,
313
333
  tour_type: tourTypeForSubmission[this.tourType],
314
334
  tour_time: `${this.selectedTime.datetime}${this.selectedTime.offset}`, // e.g., "2022-06-27T09:00:00-07:00"
335
+ layouts: [this.layoutTypeSelect.value],
336
+ unit_numbers: [this.unitTypeSelect.value],
315
337
  };
316
338
  const url = `https://app.meetelise.com/platformApi/state/create/scheduleMe`;
317
339
  this.isSubmitting = true;
318
- const response = await axios.post(url, data, {
319
- headers: {
320
- ["building-slug"]: this.buildingSlug,
321
- ["X-SecurityKey"]: "JRL8jV4VcSCwOSir5gWkpgNLfKghmhBG",
322
- ["org-slug"]: this.orgSlug,
323
- },
324
- });
325
- if (response.status === 200) {
340
+
341
+ try {
342
+ await axios.post(url, data, {
343
+ headers: {
344
+ ["building-slug"]: this.buildingSlug,
345
+ ["X-SecurityKey"]: "JRL8jV4VcSCwOSir5gWkpgNLfKghmhBG",
346
+ ["org-slug"]: this.orgSlug,
347
+ },
348
+ });
326
349
  this.isSubmitting = false;
327
350
  this.tourIsBooked = true;
328
- } else {
329
- const message = response.data["detail"] || "Failed to book tour";
351
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
352
+ } catch (e: any) {
353
+ const message = e.response.data["detail"] || "Failed to book tour";
330
354
  alert(message);
331
355
  this.isSubmitting = false;
332
356
  this.tourIsBooked = false;
@@ -355,7 +379,7 @@ export class TourScheduler extends LitElement {
355
379
  /* grid stuff */
356
380
  display: grid;
357
381
  grid-template-columns: 229px 432px 305px;
358
- grid-template-rows: 44px 54px 32px 195px 152px 1px;
382
+ grid-template-rows: 44px 54px 32px 195px 167px 1px;
359
383
  }
360
384
 
361
385
  h1,
@@ -449,7 +473,7 @@ export class TourScheduler extends LitElement {
449
473
  }
450
474
 
451
475
  #yourInformationMenu input {
452
- width: 305px;
476
+ width: 100%;
453
477
  height: 49px;
454
478
  border: 1px solid #83818e;
455
479
  padding: 13px 11px 14px 11px;
@@ -464,7 +488,7 @@ export class TourScheduler extends LitElement {
464
488
  color: #202020;
465
489
  }
466
490
 
467
- #unitChoiceMenu {
491
+ .unitLayoutChoices {
468
492
  grid-row: 5 / 6;
469
493
  grid-column: 3;
470
494
  align-self: start;
@@ -472,11 +496,15 @@ export class TourScheduler extends LitElement {
472
496
  flex-direction: column;
473
497
  }
474
498
 
475
- h2#unitChoice {
499
+ .unitLayoutChoice {
500
+ margin-bottom: 12px;
501
+ }
502
+
503
+ h2.unitLayoutChoice {
476
504
  margin-bottom: 7px;
477
505
  }
478
506
 
479
- #unitOptions {
507
+ .unitLayoutOptions {
480
508
  display: flex;
481
509
  flex-direction: column;
482
510
  gap: 8px;
@@ -584,10 +612,6 @@ export class TourScheduler extends LitElement {
584
612
  width: 205px;
585
613
  }
586
614
 
587
- .tour-scheduler.loading #yourInformationMenu .inputContainer::after {
588
- width: 100%;
589
- }
590
-
591
615
  .tour-scheduler.loading #yourInformationMenu .inputContainer input {
592
616
  visibility: hidden;
593
617
  }
@@ -596,6 +620,19 @@ export class TourScheduler extends LitElement {
596
620
  display: none;
597
621
  }
598
622
 
623
+ #namesWrapper {
624
+ display: flex;
625
+ justify-content: space-between;
626
+ }
627
+
628
+ .nameContainer {
629
+ width: 48%;
630
+ }
631
+
632
+ .nameInput {
633
+ width: 100%;
634
+ }
635
+
599
636
  @media (max-width: 767px) {
600
637
  /* TODO: separate styles into general, desktop-specific, and mobile-specific.
601
638
  basically everything I have "unset" or "initial" on should become desktop-specific. the grid layout is only for desktop.
@@ -832,9 +869,15 @@ export class TourScheduler extends LitElement {
832
869
  .options=${this.selectedDate
833
870
  ? this.availabilitiesGroupedByDay[
834
871
  format(this.selectedDate, "y-MM-dd")
835
- ]?.map((date) =>
836
- format(parseISO(`${date.datetime}${date.offset}`), "h:mmaaa")
837
- )
872
+ ]?.map((date) => {
873
+ return {
874
+ dateWithTimeZoneOffset: date,
875
+ displayTime: format(
876
+ parseISO(`${date.datetime}${date.offset}`),
877
+ "h:mmaaa"
878
+ ),
879
+ };
880
+ })
838
881
  : []}
839
882
  @change=${(e: Event) => {
840
883
  if (e.target instanceof TimePicker) {
@@ -844,9 +887,9 @@ export class TourScheduler extends LitElement {
844
887
  format(new Date(this.selectedDate), "y-MM-dd")
845
888
  ];
846
889
  const index = e.target.selectedTime
847
- ? daysAvailabilities
848
- .map((date) => format(new Date(date.datetime), "h:mmaaa"))
849
- .indexOf(e.target.selectedTime)
890
+ ? daysAvailabilities.indexOf(
891
+ e.target.selectedTime.dateWithTimeZoneOffset
892
+ )
850
893
  : null;
851
894
  this.selectedTime =
852
895
  index !== null ? daysAvailabilities[index] : null;
@@ -927,24 +970,29 @@ export class TourScheduler extends LitElement {
927
970
  userInfoAndLayoutMenu(): TemplateResult {
928
971
  return html`<h2 id="yourInformation">Your information</h2>
929
972
  <div id="yourInformationMenu">
930
- <div class="inputContainer" id="firstName">
931
- <input
932
- type="text"
933
- placeholder="First name"
934
- name="firstName"
935
- autocomplete="given-name"
936
- @input=${() => this.requestUpdate()}
937
- />
938
- </div>
939
- <div class="inputContainer" id="lastName">
940
- <input
941
- type="text"
942
- placeholder="Last name"
943
- name="lastName"
944
- autocomplete="family-name"
945
- @input=${() => this.requestUpdate()}
946
- />
973
+ <div id="namesWrapper">
974
+ <div class="nameContainer" id="firstName">
975
+ <input
976
+ class="nameInput"
977
+ type="text"
978
+ placeholder="First name"
979
+ name="firstName"
980
+ autocomplete="given-name"
981
+ @input=${() => this.requestUpdate()}
982
+ />
983
+ </div>
984
+ <div class="nameContainer" id="lastName">
985
+ <input
986
+ class="nameInput"
987
+ type="text"
988
+ placeholder="Last name"
989
+ name="lastName"
990
+ autocomplete="family-name"
991
+ @input=${() => this.requestUpdate()}
992
+ />
993
+ </div>
947
994
  </div>
995
+
948
996
  <div class="inputContainer" id="email">
949
997
  <input
950
998
  type="email"
@@ -978,20 +1026,64 @@ export class TourScheduler extends LitElement {
978
1026
  />
979
1027
  </div>
980
1028
  </div>
981
- <!--
982
- Layout dropdown would go here, but has been removed pending backend support.
983
- Here is the code to add it back:
984
- https://github.com/MeetElise/chat-ui/blob/e17aca8b39a0eed9430f22c182f2ebcdfb796417/src/WebComponent/Scheduler/tour-scheduler.ts#L846-L863
985
- --> `;
1029
+ <div class="unitLayoutChoices">
1030
+ ${this.layoutOptions.length > 0
1031
+ ? html`<div class="unitLayoutChoice">
1032
+ <h2 class="unitLayoutChoice">What would you like to view?</h2>
1033
+ <div class="unitLayoutOptions">
1034
+ <me-select
1035
+ id="layoutType"
1036
+ placeholder="Select type"
1037
+ .options="${this.layoutOptions.map((i) => ({
1038
+ label: getHumanReadableLayout(i),
1039
+ value: i,
1040
+ }))}"
1041
+ defaultOption="Studio"
1042
+ @change=${() => {
1043
+ // to revalidate the form
1044
+ this.requestUpdate();
1045
+ }}
1046
+ >Studio
1047
+ </me-select>
1048
+ </div>
1049
+ </div>`
1050
+ : ""}
1051
+ ${this.unitOptions.length > 0
1052
+ ? html`<div class="unitLayoutChoice">
1053
+ <div class="unitLayoutOptions">
1054
+ <me-select
1055
+ id="unitType"
1056
+ placeholder="Select type"
1057
+ .options="${this.unitOptions
1058
+ .filter(
1059
+ (i) =>
1060
+ !this.layoutTypeSelect ||
1061
+ i.layout === this.layoutTypeSelect.value
1062
+ )
1063
+ .map((i) => ({
1064
+ label: i.name,
1065
+ value: i.name,
1066
+ }))}"
1067
+ defaultOption="Studio"
1068
+ @change=${() => {
1069
+ // to revalidate the form
1070
+ this.requestUpdate();
1071
+ }}
1072
+ >Studio
1073
+ </me-select>
1074
+ </div>
1075
+ </div>`
1076
+ : ""}
1077
+ </div> `;
986
1078
  }
987
1079
 
988
1080
  confirmationMessage(): TemplateResult {
989
1081
  if (!this.selectedDate || !this.selectedTime) return html``;
990
1082
  // format example: "November 9th, 2022 at 11:00am"
991
- const readableDateAndTime = `${format(
992
- new Date(this.selectedDate),
993
- "MMMM do, y"
994
- )} at ${format(new Date(this.selectedTime.datetime), "h:mmaaa")}`;
1083
+ const readableDateAndTime = format(
1084
+ parseISO(`${this.selectedTime.datetime}${this.selectedTime.offset}`),
1085
+ "h:mmaaa"
1086
+ );
995
1087
  return html`
996
1088
  <div id="confirmationMessage">
997
1089
  <svg
@@ -285,7 +285,8 @@ export class MEChat extends LitElement {
285
285
  .isFirstMount=${!this.hasMounted}
286
286
  .isMini=${this.useMiniWidget}
287
287
  .buildingId=${this.building?.id ?? 0}
288
- .layoutOptions=${this.building?.layoutOptions ?? []}
288
+ .layoutOptions=${this.building?.layoutOptionsV2 ?? []}
289
+ .unitOptions=${this.building?.unitOptionsV2 ?? []}
289
290
  .tourTypeOptions=${this.building?.tourTypeOptions ?? []}
290
291
  .launcherStyles=${this.launcherStyles}
291
292
  chatCallUsHeader=${this.building?.chatCallUsHeader ?? "Call us"}
@@ -21,12 +21,18 @@ export interface Building {
21
21
  conversationMaintenanceMode: boolean;
22
22
  orgId: number;
23
23
  phoneNumber: string;
24
- layoutOptions: LabeledOption[];
25
24
  tourTypeOptions: LabeledOption[];
26
25
  chatWidgets?: string[] | null;
27
26
  chatCallUsHeader?: string;
27
+ unitOptionsV2: UnitV2[];
28
+ layoutOptionsV2: string[];
28
29
  }
29
30
 
31
+ export type UnitV2 = {
32
+ name: string;
33
+ layout: string;
34
+ };
35
+
30
36
  /**
31
37
  * Load the publicly-available info for a building.
32
38
  *
@@ -42,5 +48,19 @@ export default async function fetchBuildingInfo(
42
48
  const url = `${host}/api/pub/v1/organization/${orgSlug}/building/${buildingSlug}`;
43
49
  const response = await fetch(url);
44
50
  const building: Building = await response.json();
51
+
52
+ // HACK
53
+ // We will fetch these units/layouts from elise-crm-api and supplement the DTO
54
+ const unitsResponse = await fetch(
55
+ `${host}/eliseCrmApi/pub/building/${buildingSlug}/units`
56
+ );
57
+ const units: UnitV2[] = await unitsResponse.json();
58
+ const layoutsResponse = await fetch(
59
+ `${host}/eliseCrmApi/pub/building/${buildingSlug}/layouts`
60
+ );
61
+ const layouts: string[] = await layoutsResponse.json();
62
+
63
+ building.unitOptionsV2 = units;
64
+ building.layoutOptionsV2 = layouts;
45
65
  return building;
46
66
  }