@meetelise/chat 1.13.9 → 1.14.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.
@@ -11,7 +11,8 @@ import "./time-picker.ts";
11
11
  import "./me-select.ts";
12
12
  import {
13
13
  DateWithTimeZoneOffset,
14
- getAvailabilitiesGroupedByDayCached,
14
+ getAvailabilitiesGroupedByDay,
15
+ getExistenceOfAvailabilitiesByTourType,
15
16
  } from "../../getAvailabilities";
16
17
  import { TourAvailabilityResponseRankOrderedSupportedTourTypesEnum } from "@meetelise/rest-sdk";
17
18
  import { format } from "date-fns";
@@ -37,6 +38,12 @@ export class TourScheduler extends LitElement {
37
38
  @state()
38
39
  private tourType = TourType.Guided;
39
40
  @state()
41
+ private shouldShowTourType = {
42
+ [TourType.Guided]: true,
43
+ [TourType.Self]: true,
44
+ [TourType.Virtual]: true,
45
+ };
46
+ @state()
40
47
  private email = "";
41
48
  @state()
42
49
  private phoneNumber = "";
@@ -53,8 +60,10 @@ export class TourScheduler extends LitElement {
53
60
  @state()
54
61
  private tourIsBooked = false;
55
62
 
56
- @query("input#name")
57
- nameInput!: HTMLInputElement;
63
+ @query("input#firstName")
64
+ firstNameInput!: HTMLInputElement;
65
+ @query("input#lastName")
66
+ lastNameInput!: HTMLInputElement;
58
67
  @query("input#email")
59
68
  emailInput!: HTMLInputElement;
60
69
  @query("input#phone")
@@ -63,9 +72,25 @@ export class TourScheduler extends LitElement {
63
72
  unitTypeSelect!: MESelect;
64
73
 
65
74
  firstUpdated = async (): Promise<void> => {
66
- this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDayCached(
75
+ this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDay(
67
76
  tourTypeMap[this.tourType]
68
77
  );
78
+
79
+ // Show a tour type only if it is supported by the building and has
80
+ // time slots available.
81
+ const availabilitiesExistForTourType =
82
+ await getExistenceOfAvailabilitiesByTourType();
83
+ this.shouldShowTourType = {
84
+ [TourType.Guided]:
85
+ this.tourTypeOptions.map((o) => o.value).includes("WITH_AGENT") &&
86
+ availabilitiesExistForTourType[TourType.Guided],
87
+ [TourType.Self]:
88
+ this.tourTypeOptions.map((o) => o.value).includes("SELF_GUIDED") &&
89
+ availabilitiesExistForTourType[TourType.Self],
90
+ [TourType.Virtual]:
91
+ this.tourTypeOptions.map((o) => o.value).includes("VIRTUAL_SHOWING") &&
92
+ availabilitiesExistForTourType[TourType.Virtual],
93
+ };
69
94
  };
70
95
 
71
96
  protected willUpdate = async (
@@ -74,8 +99,9 @@ export class TourScheduler extends LitElement {
74
99
  | Map<PropertyKey, unknown>
75
100
  ): Promise<void> => {
76
101
  if (_changedProperties.has("tourType")) {
77
- this.availabilitiesGroupedByDay =
78
- await getAvailabilitiesGroupedByDayCached(tourTypeMap[this.tourType]);
102
+ this.availabilitiesGroupedByDay = await getAvailabilitiesGroupedByDay(
103
+ tourTypeMap[this.tourType]
104
+ );
79
105
  }
80
106
  };
81
107
 
@@ -235,7 +261,7 @@ export class TourScheduler extends LitElement {
235
261
  dateAndTime: (): boolean => !!this.selectedDate && !!this.selectedTime,
236
262
  leadInfo: (): boolean => {
237
263
  return (
238
- !!this.nameInput?.value &&
264
+ (!!this.firstNameInput?.value || !!this.lastNameInput?.value) &&
239
265
  this.emailInput?.value.includes("@") &&
240
266
  // TODO: deleting phone number doesn't cause validation to fail, at least on mobile
241
267
  !!this.phoneNumber &&
@@ -272,9 +298,8 @@ export class TourScheduler extends LitElement {
272
298
  email_address: this.email,
273
299
  phone_number: `+1${this.phoneNumber.match(/\d/g)?.join("")}`, // e.g. +12125555555
274
300
  building_id: this.buildingId,
275
- // TODO: this is very bad dumb name-splitting logic! I'm only doing it because the design had one name field and the backend expects two
276
- first_name: this.nameInput.value.split(" ")[0],
277
- last_name: this.nameInput.value.split(" ").slice(1).join(" "),
301
+ first_name: this.firstNameInput.value,
302
+ last_name: this.lastNameInput.value,
278
303
  tour_type: tourTypeForSubmission[this.tourType],
279
304
  tour_time: `${this.selectedTime.datetime}${this.selectedTime.offset}`, // e.g., "2022-06-27T09:00:00-07:00"
280
305
  };
@@ -474,6 +499,15 @@ export class TourScheduler extends LitElement {
474
499
  box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
475
500
  }
476
501
 
502
+ button#schedule:not(:disabled):hover {
503
+ opacity: 0.7;
504
+ }
505
+
506
+ button#schedule:not(:disabled):active {
507
+ box-shadow: none;
508
+ opacity: 1;
509
+ }
510
+
477
511
  button#schedule:disabled {
478
512
  background: #e7e7e7;
479
513
  box-shadow: none;
@@ -604,9 +638,8 @@ export class TourScheduler extends LitElement {
604
638
  tourTypeMenu(): TemplateResult {
605
639
  return html`<h2 id="tourType">Tour Type</h2>
606
640
  <div id="tourTypeMenu">
607
- ${this.tourTypeOptions.map((o) => o.value).includes("WITH_AGENT")
641
+ ${this.shouldShowTourType[TourType.Guided]
608
642
  ? html` <tour-type-option
609
- tourtype="guided"
610
643
  heading="Guided tour"
611
644
  subtitle="with an agent"
612
645
  @click="${() => {
@@ -637,9 +670,8 @@ export class TourScheduler extends LitElement {
637
670
  </svg>
638
671
  </tour-type-option>`
639
672
  : ""}
640
- ${this.tourTypeOptions.map((o) => o.value).includes("SELF_GUIDED")
673
+ ${this.shouldShowTourType[TourType.Self]
641
674
  ? html`<tour-type-option
642
- tourtype="self"
643
675
  heading="Take a tour"
644
676
  subtitle="on your own"
645
677
  @click="${() => {
@@ -670,9 +702,8 @@ export class TourScheduler extends LitElement {
670
702
  </svg>
671
703
  </tour-type-option>`
672
704
  : ""}
673
- ${this.tourTypeOptions.map((o) => o.value).includes("SELF_GUIDED")
705
+ ${this.shouldShowTourType[TourType.Virtual]
674
706
  ? html`<tour-type-option
675
- tourtype="guided"
676
707
  heading="Virtual tour"
677
708
  subtitle="over video"
678
709
  @click="${() => {
@@ -741,7 +772,8 @@ export class TourScheduler extends LitElement {
741
772
  .map((date) => format(new Date(date.datetime), "h:mmaaa"))
742
773
  .indexOf(e.target.selectedTime)
743
774
  : null;
744
- this.selectedTime = index ? daysAvailabilities[index] : null; // this.selectedAvailabilityString ?
775
+ this.selectedTime =
776
+ index !== null ? daysAvailabilities[index] : null;
745
777
  }
746
778
  }}
747
779
  ></time-picker>
@@ -811,8 +843,14 @@ export class TourScheduler extends LitElement {
811
843
  <div id="yourInformationMenu">
812
844
  <input
813
845
  type="text"
814
- placeholder="Name"
815
- id="name"
846
+ placeholder="First name"
847
+ id="firstName"
848
+ @input=${() => this.requestUpdate()}
849
+ />
850
+ <input
851
+ type="text"
852
+ placeholder="Last name"
853
+ id="lastName"
816
854
  @input=${() => this.requestUpdate()}
817
855
  />
818
856
  <input
@@ -842,25 +880,11 @@ export class TourScheduler extends LitElement {
842
880
  }}
843
881
  />
844
882
  </div>
845
-
846
- ${this.layoutOptions.length > 0
847
- ? html`<div id="unitChoiceMenu">
848
- <h2 id="unitChoice">What would you like to view?</h2>
849
- <div id="unitOptions">
850
- <me-select
851
- id="unitType"
852
- placeholder="Select type"
853
- .options="${this.layoutOptions.map((o) => o.label)}"
854
- defaultOption="Studio"
855
- @change=${() => {
856
- // to revalidate the form
857
- this.requestUpdate();
858
- }}
859
- >Studio
860
- </me-select>
861
- </div>
862
- </div>`
863
- : ""} `;
883
+ <!--
884
+ Layout dropdown would go here, but has been removed pending backend support.
885
+ Here is the code to add it back:
886
+ https://github.com/MeetElise/chat-ui/blob/e17aca8b39a0eed9430f22c182f2ebcdfb796417/src/WebComponent/Scheduler/tour-scheduler.ts#L846-L863
887
+ --> `;
864
888
  }
865
889
 
866
890
  confirmationMessage(): TemplateResult {
@@ -887,7 +911,13 @@ export class TourScheduler extends LitElement {
887
911
  <p>
888
912
  Thank you!
889
913
  <br />
890
- Your guided tour is scheduled for ${readableDateAndTime}.
914
+ Your
915
+ ${{
916
+ [TourType.Guided]: "guided",
917
+ [TourType.Self]: "self-guided",
918
+ [TourType.Virtual]: "virtual",
919
+ }[this.tourType]}
920
+ tour is scheduled for ${readableDateAndTime}.
891
921
  </p>
892
922
  <p>
893
923
  Look for an email confirmation along with instructions and directions.
@@ -955,8 +985,6 @@ export enum TourType {
955
985
  Virtual,
956
986
  }
957
987
 
958
- // TODO: we have three UI options and five TourAvailabilityResponseRankOrderedSupportedTourTypesEnum values
959
- // how should they map?
960
988
  const tourTypeMap = {
961
989
  [TourType.Guided]:
962
990
  TourAvailabilityResponseRankOrderedSupportedTourTypesEnum.WithAgent,
@@ -1,20 +1,16 @@
1
1
  import { css, html, LitElement, TemplateResult } from "lit";
2
2
  import { customElement, property } from "lit/decorators.js";
3
- import { TourType } from "./tour-scheduler";
4
3
 
5
4
  @customElement("tour-type-option")
6
5
  export class TourTypeOption extends LitElement {
7
- @property({ type: String })
8
- tourtype = "";
9
6
  @property({ type: String })
10
7
  heading = "";
11
8
  @property({ type: String })
12
9
  subtitle = "";
13
10
  @property({ type: Boolean })
14
11
  selected = false;
15
-
16
12
  @property({ attribute: false })
17
- onClick?: (tourType: TourType) => void;
13
+ onClick?: () => void;
18
14
 
19
15
  static styles = [
20
16
  css`
@@ -46,7 +46,7 @@ export default function createConversation(
46
46
  building.avatarType === "image" && building.avatarSrc
47
47
  ? avatarSrc
48
48
  : defaultAvatarUrl,
49
- // uncomment this to test changes to the default avatar if your test building has its own avatar
49
+ // uncomment the following line to test changes to the default avatar if your test building has its own avatar
50
50
  // avatarUrl: defaultAvatarUrl,
51
51
  };
52
52
  return conversation;
@@ -10,31 +10,49 @@ 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
- [buildingId: number]: TourAvailabilityResponse;
16
- } = {};
16
+ buildingId?: number | null;
17
+ availabilities: { [buildingId: number]: TourAvailabilityResponse };
18
+ } = { buildingId: null, availabilities: {} };
17
19
 
20
+ /**
21
+ * Returns the raw availabilities.
22
+ *
23
+ * If no `buildingId` is provided, it will use a cached buildingId from the previous call.
24
+ * If there is none cached, it will throw an error.
25
+ */
18
26
  export const getRawAvailabilities = async (
19
- buildingId: number
27
+ buildingId?: number
20
28
  ): Promise<TourAvailabilityResponse> => {
21
- if (availabilitiesCache[buildingId]) {
22
- return availabilitiesCache[buildingId];
29
+ if (buildingId) {
30
+ availabilitiesCache.buildingId = buildingId;
31
+ }
32
+ const buildingIdToUse = availabilitiesCache.buildingId;
33
+ if (!buildingIdToUse) {
34
+ throw new Error(
35
+ "No buildingId was provided to getRawAvailabilities and there is no buildingId cached."
36
+ );
37
+ }
38
+ const availabilities = availabilitiesCache.availabilities;
39
+ if (availabilities[buildingIdToUse]) {
40
+ return availabilities[buildingIdToUse];
23
41
  }
24
42
  const startTime = startOfToday();
25
43
  const endTime = formatISO(endOfDay(addDays(startTime, 30)));
26
- const url = `https://app.meetelise.com/api/pub/v1/buildings/${buildingId}/tour/availabilities?startTime=${formatISO(
44
+ const url = `https://app.meetelise.com/api/pub/v1/buildings/${buildingIdToUse}/tour/availabilities?startTime=${formatISO(
27
45
  startTime
28
46
  )}&endTime=${endTime}`;
29
47
  const result = await axios.get<TourAvailabilityResponse>(url);
30
- availabilitiesCache[buildingId] = result.data;
48
+ availabilitiesCache.availabilities[buildingIdToUse] = result.data;
31
49
  // The endpoint INCORRECTLY states that these are returned as dates. They are, in fact, strings.
32
50
  return result.data;
33
51
  };
34
52
 
35
53
  export const getAvailabilitiesForTourType = async (
36
- buildingId: number,
37
- tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum
54
+ tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
55
+ buildingId?: number
38
56
  ): Promise<{
39
57
  /**
40
58
  *
@@ -68,13 +86,46 @@ export interface DateWithTimeZoneOffset {
68
86
  offset: string;
69
87
  }
70
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
+
71
122
  export const getAvailabilitiesGroupedByDay = async (
72
- buildingId: number,
73
- tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum
123
+ tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum,
124
+ buildingId?: number
74
125
  ): Promise<{ [day: string]: DateWithTimeZoneOffset[] }> => {
75
126
  const availabilitiesForTourTypeRaw = await getAvailabilitiesForTourType(
76
- buildingId,
77
- tourType
127
+ tourType,
128
+ buildingId
78
129
  );
79
130
  if (!availabilitiesForTourTypeRaw) {
80
131
  return {};
@@ -97,16 +148,6 @@ export const getAvailabilitiesGroupedByDay = async (
97
148
  );
98
149
  };
99
150
 
100
- // TODO: if cache is empty, observe it and wait for it to be filled in.
101
- // TODO: alternative to this: cache the building id when getRawAvailabilities is called. Then I can call the normal methods.
102
- export const getAvailabilitiesGroupedByDayCached = async (
103
- tourType: TourAvailabilityResponseRankOrderedSupportedTourTypesEnum
104
- ): Promise<{ [day: string]: DateWithTimeZoneOffset[] }> =>
105
- getAvailabilitiesGroupedByDay(
106
- Object.keys(availabilitiesCache).map(Number)[0],
107
- tourType
108
- );
109
-
110
151
  /**
111
152
  * Takes an ISO 8601 date string with time zone offset and returns
112
153
  * an object of our custom type DateWithTimeZoneOffset.