@meetelise/chat 1.9.1 → 1.12.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.
Files changed (57) hide show
  1. package/.eslintrc.cjs +1 -0
  2. package/.github/workflows/release.yml +1 -0
  3. package/.vscode/settings.json +6 -1
  4. package/CONTRIBUTING.md +8 -0
  5. package/package.json +9 -10
  6. package/public/demo/index.html +78 -11
  7. package/public/dist/index.js +1714 -1
  8. package/public/dist/index.js.LICENSE.txt +26 -14
  9. package/public/index.html +2 -1
  10. package/src/MEChat.test.ts +5 -5
  11. package/src/MEChat.ts +53 -0
  12. package/src/WebComponent/InHouseLauncher.ts +446 -0
  13. package/src/WebComponent/MEChat.css +5 -0
  14. package/src/WebComponent/MEChat.ts +282 -0
  15. package/src/WebComponent/OfficeHours.ts +73 -0
  16. package/src/WebComponent/Scheduler/date-picker.ts +320 -0
  17. package/src/WebComponent/Scheduler/me-select.ts +244 -0
  18. package/src/WebComponent/Scheduler/time-picker.ts +101 -0
  19. package/src/WebComponent/Scheduler/tour-scheduler.ts +383 -0
  20. package/src/WebComponent/Scheduler/tour-type-option.ts +92 -0
  21. package/src/WebComponent/actions/ActionConfirmButton.ts +94 -0
  22. package/src/WebComponent/actions/CallUsWindow.ts +110 -0
  23. package/src/WebComponent/actions/DetailsWindow.ts +109 -0
  24. package/src/WebComponent/actions/EmailUsWindow.ts +432 -0
  25. package/src/WebComponent/actions/InputStyles.ts +31 -0
  26. package/src/WebComponent/actions/TextUsWindow.ts +226 -0
  27. package/src/WebComponent/actions/formatPhoneNumber.ts +42 -0
  28. package/src/WebComponent/inHouseLauncherStyles.ts +300 -0
  29. package/src/WebComponent/index.ts +2 -0
  30. package/src/WebComponent/utils.ts +82 -0
  31. package/src/analytics.ts +48 -15
  32. package/src/assetUrls.ts +4 -0
  33. package/src/fetchBuildingInfo.ts +1 -0
  34. package/src/getAvailabilities.ts +71 -0
  35. package/src/themes.ts +5 -3
  36. package/tsconfig.json +9 -3
  37. package/web-test-runner.config.js +0 -6
  38. package/webpack.config.cjs +8 -25
  39. package/public/dist/index.d.ts +0 -1
  40. package/public/dist/src/ChatButton.d.ts +0 -9
  41. package/public/dist/src/ChatIcon.d.ts +0 -6
  42. package/public/dist/src/InHouseLauncher.d.ts +0 -11
  43. package/public/dist/src/MEChat.d.ts +0 -73
  44. package/public/dist/src/analytics.d.ts +0 -18
  45. package/public/dist/src/chatID.d.ts +0 -11
  46. package/public/dist/src/createConversation.d.ts +0 -4
  47. package/public/dist/src/fetchBuildingInfo.d.ts +0 -25
  48. package/public/dist/src/themes.d.ts +0 -52
  49. package/public/dist/src/utils.d.ts +0 -2
  50. package/src/ChatButton.module.scss +0 -52
  51. package/src/ChatButton.tsx +0 -26
  52. package/src/ChatIcon.tsx +0 -26
  53. package/src/DemoApp.tsx +0 -113
  54. package/src/InHouseLauncher.module.scss +0 -140
  55. package/src/InHouseLauncher.tsx +0 -65
  56. package/src/MEChat.module.scss +0 -22
  57. package/src/MEChat.tsx +0 -293
@@ -0,0 +1,226 @@
1
+ import { css, html, LitElement, TemplateResult } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import { createRef, ref, Ref } from "lit/directives/ref.js";
5
+ import { installActionConfirmButton } from "./ActionConfirmButton";
6
+ import { installDetailsWindow } from "./DetailsWindow";
7
+ import {
8
+ formatToPhone,
9
+ isModifierKey,
10
+ isNumericInput,
11
+ } from "./formatPhoneNumber";
12
+ import { InputStyles } from "./InputStyles";
13
+ import axios from "axios";
14
+
15
+ @customElement("text-us-window")
16
+ export class TextUsWindow extends LitElement {
17
+ static styles = [
18
+ css`
19
+ @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;900&display=swap");
20
+ .text-us-wrapper {
21
+ font-family: "Poppins";
22
+ color: #202020;
23
+ }
24
+
25
+ .text-us-window__description {
26
+ font-size: 18px;
27
+ line-height: 24px;
28
+ margin-top: 32px;
29
+ }
30
+
31
+ .text-us-window__phone-input {
32
+ width: -webkit-fill-available;
33
+ height: 49px;
34
+ }
35
+
36
+ .text-us-window__vertical-spacer {
37
+ height: 20px;
38
+ }
39
+
40
+ .text-us-window__subtext {
41
+ font-size: 12px;
42
+ line-height: 22px;
43
+ }
44
+
45
+ .text-us-window__error {
46
+ font-size: 10px;
47
+ line-height: 22px;
48
+ margin-top: 8px;
49
+ }
50
+ `,
51
+ InputStyles,
52
+ ];
53
+
54
+ @property({ attribute: false })
55
+ onCloseClicked?: (e: MouseEvent) => void;
56
+
57
+ @property({ attribute: false })
58
+ buildingId = 0;
59
+
60
+ phoneNumberInputRef: Ref<HTMLInputElement> = createRef();
61
+
62
+ @state()
63
+ phoneNumber = "";
64
+ @state()
65
+ hasSubmittedForm = false;
66
+ @state()
67
+ hasPhoneNumberError = false;
68
+ @state()
69
+ hasSubmissionError = false;
70
+ @state()
71
+ isSubmitting = false;
72
+
73
+ onChangePhoneNumber = (e: Event): void => {
74
+ if (!e.target || !this.phoneNumberInputRef.value) {
75
+ return;
76
+ }
77
+ if (isModifierKey(e as KeyboardEvent)) {
78
+ return;
79
+ }
80
+ const inputElement = e.target as HTMLInputElement;
81
+
82
+ this.phoneNumber = formatToPhone(inputElement.value);
83
+
84
+ this.phoneNumberInputRef.value.value = this.phoneNumber;
85
+ };
86
+
87
+ enforceFormat = (e: KeyboardEvent): void => {
88
+ if (!isNumericInput(e) && !isModifierKey(e)) {
89
+ e.preventDefault();
90
+ }
91
+ };
92
+
93
+ validateFormFields = (): void => {
94
+ this.hasPhoneNumberError = false;
95
+ this.hasSubmissionError = false;
96
+ if (!this.phoneNumber || this.phoneNumber.length !== 14) {
97
+ this.hasPhoneNumberError = true;
98
+ }
99
+ };
100
+
101
+ onClick = async (): Promise<void> => {
102
+ this.validateFormFields();
103
+ if (this.hasPhoneNumberError) {
104
+ return;
105
+ }
106
+ try {
107
+ this.isSubmitting = true;
108
+ await createTextWithUs(this.phoneNumber, this.buildingId);
109
+ this.hasSubmittedForm = true;
110
+ this.isSubmitting = false;
111
+ } catch (e) {
112
+ this.isSubmitting = false;
113
+ this.hasSubmissionError = true;
114
+ }
115
+ };
116
+
117
+ render = (): TemplateResult => {
118
+ installDetailsWindow();
119
+ installActionConfirmButton();
120
+ if (this.hasSubmittedForm) {
121
+ return html`
122
+ <details-window
123
+ headerText="Text us"
124
+ .onCloseClick=${this.onCloseClicked}
125
+ >
126
+ <div class="text-us-wrapper">
127
+ <div class="text-us-window__vertical-spacer"></div>
128
+ <svg
129
+ width="20"
130
+ height="20"
131
+ viewBox="0 0 20 20"
132
+ fill="none"
133
+ xmlns="http://www.w3.org/2000/svg"
134
+ >
135
+ <path
136
+ d="M4.455 16L0 19.5V1C0 0.734784 0.105357 0.48043 0.292893 0.292893C0.48043 0.105357 0.734784 0 1 0H19C19.2652 0 19.5196 0.105357 19.7071 0.292893C19.8946 0.48043 20 0.734784 20 1V15C20 15.2652 19.8946 15.5196 19.7071 15.7071C19.5196 15.8946 19.2652 16 19 16H4.455ZM2 15.385L3.763 14H18V2H2V15.385ZM10 7V4L14 8L10 12V9H6V7H10Z"
137
+ fill="#202020"
138
+ />
139
+ </svg>
140
+ <div class="text-us-window__description">
141
+ Thank you!<br />Look for a text message from our team. We can
142
+ answer questions and help you book a tour through text.
143
+ </div>
144
+ <div class="text-us-window__vertical-spacer"></div>
145
+ <div class="text-us-window__subtext">
146
+ Opt out at anytime by texting “Stop”
147
+ </div>
148
+ </div>
149
+ </details-window>
150
+ `;
151
+ }
152
+
153
+ return html`
154
+ <details-window headerText="Text us" .onCloseClick=${this.onCloseClicked}>
155
+ <div class="text-us-wrapper">
156
+ <div class="text-us-window__description">
157
+ Have questions? <br />
158
+ Our team can answer via <br />text message.
159
+ </div>
160
+ <div class="text-us-window__vertical-spacer"></div>
161
+ <input
162
+ ${ref(this.phoneNumberInputRef)}
163
+ maxlength="14"
164
+ type="text"
165
+ placeholder="Phone number"
166
+ inputmode="tel"
167
+ class=${classMap({
168
+ ["webchat-input"]: true,
169
+ ["text-us-window__phone-input"]: true,
170
+ })}
171
+ .value=${this.phoneNumber}
172
+ @keydown=${this.enforceFormat}
173
+ @keyup=${this.onChangePhoneNumber}
174
+ />
175
+ ${this.hasPhoneNumberError
176
+ ? html`
177
+ <div class="text-us-window__error">
178
+ Enter a valid phone number
179
+ </div>
180
+ `
181
+ : ""}
182
+ <div class="text-us-window__vertical-spacer"></div>
183
+ <action-confirm-button
184
+ .onClick=${this.onClick}
185
+ .isLoading=${this.isSubmitting}
186
+ text="Send"
187
+ ></action-confirm-button>
188
+ <div class="text-us-window__vertical-spacer"></div>
189
+ <div class="text-us-window__subtext">
190
+ By entering your number and checking the box, you consent to be
191
+ contacted by our AI Leasing Assistant. Your consent to this process
192
+ is not a requirement for leasing at our property. Message and data
193
+ rates may apply.
194
+ </div>
195
+ </div>
196
+ </details-window>
197
+ `;
198
+ };
199
+ }
200
+
201
+ export const installTextUsWindow = (): void => {
202
+ if (!window.customElements.get("text-us-window")) {
203
+ window.customElements.define("text-us-window", TextUsWindow);
204
+ }
205
+ };
206
+
207
+ const createTextWithUs = async (rawPhoneNumber: string, buildingId: number) => {
208
+ const formattedPhoneNumber =
209
+ "+1" +
210
+ rawPhoneNumber
211
+ .replace("(", "")
212
+ .replace(")", "")
213
+ .replace(" ", "")
214
+ .replace("-", "");
215
+ const requestBody = {
216
+ building_id: buildingId,
217
+ lead_source: "property-website",
218
+ phone_number: formattedPhoneNumber,
219
+ };
220
+
221
+ await axios.post(
222
+ "https://app.meetelise.com/platformApi/state/create/textMe",
223
+ requestBody,
224
+ { headers: { ["X-SecurityKey"]: "JRL8jV4VcSCwOSir5gWkpgNLfKghmhBG" } }
225
+ );
226
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * For now, only handles the US phone number case.....
3
+ */
4
+ export const formatToPhone = (phoneNumber: string): string => {
5
+ const input = phoneNumber.replace(/\D/g, "").substring(0, 10);
6
+ const zip = input.substring(0, 3);
7
+ const middle = input.substring(3, 6);
8
+ const last = input.substring(6, 10);
9
+
10
+ if (input.length > 6) {
11
+ return `(${zip}) ${middle}-${last}`;
12
+ }
13
+ if (input.length > 3) {
14
+ return `(${zip}) ${middle}`;
15
+ }
16
+ if (input.length > 0) {
17
+ return `(${zip}`;
18
+ }
19
+ return "";
20
+ };
21
+
22
+ export const isNumericInput = (event: KeyboardEvent): boolean => {
23
+ const key = event.keyCode;
24
+ return (key >= 48 && key <= 57) || (key >= 96 && key <= 105);
25
+ };
26
+
27
+ export const isModifierKey = (event: KeyboardEvent): boolean => {
28
+ const key = event.keyCode;
29
+ // Allow left, up, right, down, Backspace, Tab, Enter, Delete, Ctrl/Command + A,C,V,X,Z, Shift, Home, End
30
+ return (
31
+ event.shiftKey === true ||
32
+ key === 35 ||
33
+ key === 36 || // Allow
34
+ key === 8 ||
35
+ key === 9 ||
36
+ key === 13 ||
37
+ key === 46 ||
38
+ (key > 36 && key < 41) ||
39
+ ((event.ctrlKey === true || event.metaKey === true) &&
40
+ (key === 65 || key === 67 || key === 86 || key === 88 || key === 90))
41
+ );
42
+ };
@@ -0,0 +1,300 @@
1
+ import { css } from "lit";
2
+
3
+ export const inHousLauncherStyles = css`
4
+ @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700;900&display=swap");
5
+ :host {
6
+ --glowBarHeight: 11.2px;
7
+ --enterAnimationDuration: 0.5s;
8
+ --desktopZIndex: 100000;
9
+ }
10
+
11
+ @keyframes slideInFromRight {
12
+ from {
13
+ transform: translateX(100%);
14
+ }
15
+ to {
16
+ transform: translateX(0);
17
+ }
18
+ }
19
+ .launcher {
20
+ font-family: Poppins;
21
+ user-select: none;
22
+ position: fixed;
23
+ }
24
+ .launcher:not(.miniLauncher) {
25
+ display: flex;
26
+ justify-content: space-evenly;
27
+ align-items: center;
28
+ background-color: rgba(255, 255, 255, 0.8);
29
+ color: #202020;
30
+ backdrop-filter: blur(10px);
31
+ box-shadow: 0px 8px 8px 4px rgba(0, 0, 0, 0.25);
32
+ }
33
+ .launcher:not(.miniLauncher).in-house-launcher__mobile {
34
+ width: 100%;
35
+ bottom: 0px;
36
+ left: 0px;
37
+ padding: 5px;
38
+ }
39
+ .launcher:not(.miniLauncher).in-house-launcher__desktop {
40
+ width: 245px;
41
+ height: 112px;
42
+ padding-left: 10px;
43
+ }
44
+ .launcher.in-house-launcher__desktop {
45
+ right: 0px;
46
+ overflow: hidden;
47
+ border-radius: 10px 0px 0px 10px;
48
+ bottom: 40px;
49
+ z-index: 100000;
50
+ }
51
+ .launcher.in-house-launcher__desktop.firstMount {
52
+ animation: slideInFromRight var(--enterAnimationDuration);
53
+ }
54
+ .launcher .glowBar {
55
+ overflow: hidden;
56
+ background-position: center;
57
+ position: absolute;
58
+ top: 3px;
59
+ left: 3px;
60
+ height: 11.2px;
61
+ width: 100%;
62
+ object-fit: fill;
63
+ border-top-left-radius: 10px;
64
+ }
65
+
66
+ .launcher .glowBar + * {
67
+ margin-top: calc(var(--glowBarHeight) + 8px);
68
+ }
69
+ .launcher .content {
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: center;
73
+ gap: 10px;
74
+ margin-bottom: 6px;
75
+ }
76
+ .launcher .content .header {
77
+ display: flex;
78
+ align-items: center;
79
+ }
80
+ .launcher .content .header .headerText {
81
+ font-weight: 600;
82
+ font-size: 20px;
83
+ }
84
+ .launcher .content .subtitle {
85
+ font-size: 12px;
86
+ font-weight: 600;
87
+ }
88
+ .miniLauncher {
89
+ display: flex;
90
+ align-items: center;
91
+ background-color: #202020;
92
+ position: fixed;
93
+ }
94
+ .miniLauncher:hover {
95
+ background: radial-gradient(
96
+ 36.85% 65.32% at 50% 106.31%,
97
+ #03ecc4 0%,
98
+ rgba(131, 129, 142, 1) 100%
99
+ );
100
+ }
101
+ .miniLauncher.firstMount {
102
+ animation: slideInFromRight 0.5s;
103
+ }
104
+ .miniLauncher.in-house-launcher__desktop {
105
+ padding-right: 20px;
106
+ right: 0px;
107
+ overflow: hidden;
108
+ bottom: 40px;
109
+ z-index: var(--desktopZIndex);
110
+ }
111
+ .miniLauncher.in-house-launcher__mobile {
112
+ right: 10px;
113
+ bottom: 20px;
114
+ }
115
+
116
+ .in-house-launcher__primary-action-text {
117
+ font-family: "Poppins";
118
+ font-weight: 700;
119
+ font-size: 24px;
120
+ line-height: 22px;
121
+ cursor: pointer;
122
+ }
123
+
124
+ .in-house-launcher__primary-action:hover {
125
+ color: #350da6;
126
+ }
127
+
128
+ .in-house-launcher__primary-action {
129
+ transition: color 0.5s cubic-bezier(0.2, 0.19, 0.27, 0.98),
130
+ fill 0.5s cubic-bezier(0.2, 0.19, 0.27, 0.98);
131
+ cursor: pointer;
132
+ }
133
+
134
+ .in-house-launcher__primary-action:hover path {
135
+ fill: #350da6;
136
+ }
137
+
138
+ .in-house-launcher__primary-action:hover .in-house-launcher__ask-underline {
139
+ background: #350da6;
140
+ }
141
+
142
+ .in-house-launcher__ask-underline {
143
+ width: 47px;
144
+ height: 3px;
145
+ background: #1e1e1e;
146
+ transition: background 0.5s cubic-bezier(0.2, 0.19, 0.27, 0.98);
147
+ }
148
+
149
+ .in-house-launcher__filler-text {
150
+ font-size: 10px;
151
+ line-height: 22px;
152
+ color: #1e1e1e;
153
+ }
154
+
155
+ .in-house-launcher__call-to-action-option {
156
+ font-weight: 700;
157
+ font-size: 12px;
158
+ line-height: 22px;
159
+
160
+ color: #1e1e1e;
161
+ border-bottom: 2px solid #1e1e1e;
162
+ width: fit-content;
163
+ height: fit-content;
164
+ cursor: pointer;
165
+ transition: color 0.2s cubic-bezier(0.2, 0.19, 0.27, 0.98);
166
+ }
167
+
168
+ .in-house-launcher__call-to-action-option:hover {
169
+ color: #350da6;
170
+ border-bottom: 2px solid #350da6;
171
+ }
172
+
173
+ .in-house-launcher__call-to-actions-wrapper {
174
+ width: 100%;
175
+ display: flex;
176
+ }
177
+
178
+ .in-house-launcher__call-to-actions-wrapper
179
+ > .in-house-launcher__call-to-action-option:not(:last-child) {
180
+ margin-right: 15px;
181
+ }
182
+
183
+ .in-house-launcher__window-wrapper {
184
+ position: fixed;
185
+ right: 0;
186
+ bottom: 172px;
187
+ }
188
+
189
+ .in-house-launcher__mini-launcher-wrapper {
190
+ position: fixed;
191
+ right: 0px;
192
+ bottom: 40px;
193
+ z-index: 100000;
194
+ display: flex;
195
+ align-items: center;
196
+ }
197
+
198
+ .in-house-launcher__mini-option {
199
+ position: relative;
200
+ // This is so that the pseudo elements we created will be visible despite being beneath the element it is a pseudo element for
201
+ border: 4px solid transparent;
202
+ height: 48px;
203
+ width: 48px;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ border-radius: 50%;
208
+ background: rgba(240, 240, 240, 0.9);
209
+ background-clip: padding-box;
210
+ padding: 4px;
211
+ box-shadow: 0px 8px 8px 4px rgba(0, 0, 0, 0.25);
212
+ cursor: pointer;
213
+ }
214
+
215
+ .in-house-launcher__mini-option-wrapper {
216
+ margin-right: 12px;
217
+ }
218
+
219
+ .in-house-launcher__mini-option:not(:hover)::before {
220
+ position: absolute;
221
+ top: -4px;
222
+ bottom: -4px;
223
+ left: -4px;
224
+ right: -4px;
225
+ background: #ffffff;
226
+ content: "";
227
+ z-index: -1;
228
+ border-radius: 50%;
229
+ }
230
+
231
+ .in-house-launcher__secondary-option {
232
+ width: 30px;
233
+ height: 30px;
234
+ }
235
+
236
+ .in-house-launcher__mini-option-wrapper {
237
+ background-clip: padding-box;
238
+ }
239
+
240
+ .in-house-launcher__mini-option:hover {
241
+ border: none;
242
+ }
243
+
244
+ .in-house-launcher__mini-option:hover::before {
245
+ position: absolute;
246
+ top: -4px;
247
+ bottom: -4px;
248
+ left: -4px;
249
+ right: -4px;
250
+ background: linear-gradient(to right, #350da6, #8c58e5, #e66933);
251
+ background-size: cover;
252
+ content: "";
253
+ z-index: -1;
254
+ border-radius: 50%;
255
+ }
256
+
257
+ @media screen and (max-width: 767px) {
258
+ .launcher:not(.miniLauncher).in-house-launcher__desktop {
259
+ width: 100%;
260
+ }
261
+
262
+ .launcher.in-house-launcher__desktop {
263
+ bottom: 0px;
264
+ }
265
+
266
+ .launcher.in-house-launcher__desktop {
267
+ border-radius: 0px;
268
+ }
269
+
270
+ .launcher .glowBar {
271
+ top: 3px;
272
+ left: 0;
273
+ border-top-left-radius: 0px;
274
+ }
275
+
276
+ .launcher .content {
277
+ flex-direction: row;
278
+ padding-top: 15px;
279
+ padding-bottom: 15px;
280
+ }
281
+
282
+ .in-house-launcher__primary-action-text {
283
+ font-size: 18px;
284
+ flex-grow: 1;
285
+ }
286
+
287
+ .in-house-launcher__call-to-actions-wrapper {
288
+ width: fit-content;
289
+ }
290
+
291
+ .in-house-launcher__ask-underline {
292
+ width: 35px;
293
+ }
294
+
295
+ .in-house-launcher__window-wrapper {
296
+ left: 0;
297
+ bottom: 0;
298
+ }
299
+ }
300
+ `;
@@ -0,0 +1,2 @@
1
+ export { InHouseLauncher } from "./InHouseLauncher";
2
+ import "./MEChat";
@@ -0,0 +1,82 @@
1
+ /** Pass in the year and the 0-indexed number of the month. E.g., for January 2022,
2
+ * `getDaysInMonth(2022, 0)`.
3
+ */
4
+ export const getDaysInMonth = (year: number, month: number): number => {
5
+ // In the Date constructor, day 0 is the last day of the
6
+ // previous month. We get the date of next month's day 0
7
+ // to find out how many days this month has.
8
+ return new Date(year, month + 1, 0).getDate();
9
+ };
10
+
11
+ /** Pass in the year and the 0-indexed number of the month. E.g., for January 2022,
12
+ * `getDaysInMonth(2022, 0)`. Returns the 0-indexed day of the week of the first
13
+ * day of the month, e.g. `0` for Sunday.
14
+ */
15
+ export const getMonthStartDay = (year: number, month: number): number =>
16
+ new Date(year, month, 1).getDay();
17
+
18
+ /** Pass in the year and the 0-indexed number of the month. E.g., for January 2022,
19
+ * `getDaysInMonth(2022, 0)`. Returns the 0-indexed day of the week of the last day
20
+ * of the month, e.g. `0` for Sunday.
21
+ */
22
+ export const getMonthEndDay = (year: number, month: number): number => {
23
+ const monthStartDay = getMonthStartDay(year, month);
24
+ const daysInMonth = getDaysInMonth(year, month);
25
+ return (monthStartDay + (daysInMonth % 7) - 1) % 7;
26
+ };
27
+
28
+ /** Takes a 0-indexed month and returns the previous month.
29
+ *
30
+ * @example
31
+ * getPreviousMonth(5) -> 4 // June -> May
32
+ *
33
+ * @example
34
+ * getPreviousMonth(0) -> 11 // January -> December
35
+ *
36
+ */
37
+ export const getPreviousMonth = (month: number): number =>
38
+ month > 0 ? month - 1 : 11;
39
+
40
+ /** Pass in the year and the 0-indexed number of the month. E.g., for January 2022,
41
+ * `getDaysInMonth(2022, 0)`. Returns the 0-indexed day of the week, e.g. `0` for Sunday.
42
+ */
43
+ export const getDaysInPreviousMonth = (year: number, month: number): number =>
44
+ getDaysInMonth(month > 0 ? year : year - 1, getPreviousMonth(month));
45
+
46
+ export const dayNames = [
47
+ "Sunday",
48
+ "Monday",
49
+ "Tuesday",
50
+ "Wednesday",
51
+ "Thursday",
52
+ "Friday",
53
+ "Saturday",
54
+ ];
55
+ export const monthNames = [
56
+ "January",
57
+ "February",
58
+ "March",
59
+ "April",
60
+ "May",
61
+ "June",
62
+ "July",
63
+ "August",
64
+ "September",
65
+ "October",
66
+ "November",
67
+ "December",
68
+ ];
69
+
70
+ export type Month =
71
+ | "January"
72
+ | "February"
73
+ | "March"
74
+ | "April"
75
+ | "May"
76
+ | "June"
77
+ | "July"
78
+ | "August"
79
+ | "September"
80
+ | "October"
81
+ | "November"
82
+ | "December";