@bash-app/bash-common 1.0.14 → 1.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bash-app/bash-common",
3
- "version": "1.0.14",
3
+ "version": "1.2.0",
4
4
  "description": "Common data and scripts to use on the frontend and backend",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -21,7 +21,9 @@
21
21
  "typescript": "^5.2.2"
22
22
  },
23
23
  "peerDependencies": {
24
- "tsx": "^4.10.3"
24
+ "tsx": "^4.10.3",
25
+ "dayjs": "^1.11.10",
26
+ "react-tailwindcss-datepicker": "^1.6.6"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@prisma/client": "^5.13.0",
@@ -767,22 +767,48 @@ model AssociatedBash {
767
767
  }
768
768
 
769
769
  model Service {
770
- id String @id @default(cuid())
771
- ownerId String
772
- owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
773
- name String?
774
- coverPhoto String?
775
- media Media[]
776
- serviceType ServiceType
777
- subServiceType String?
778
- serviceLinks ServiceLink[]
779
- address String
780
- description String?
781
- canContact Boolean?
782
- genre GenreType?
783
- }
784
-
785
- enum GenreType {
770
+ id String @id @default(cuid())
771
+ ownerId String
772
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
773
+ name String?
774
+ coverPhoto String?
775
+ media Media[]
776
+ serviceType ServiceType[]
777
+ subServiceType String?
778
+ serviceLinks ServiceLink[]
779
+ address String
780
+ description String?
781
+ canContact Boolean?
782
+ musicGenre MusicGenreType?
783
+ visibility VisibilityPreference @default(Public)
784
+ bashesInterestedIn BashEventType[]
785
+ crowdSizeId String @unique
786
+ crowdSize ServiceRange @relation("CrowdSize", fields: [crowdSizeId], references: [id], onDelete: Cascade)
787
+ serviceRangeId String @unique
788
+ serviceRange ServiceRange @relation("ServiceRange", fields: [serviceRangeId], references: [id], onDelete: Cascade)
789
+ availableDateTimes DateTime?
790
+ }
791
+
792
+ model ServiceRange {
793
+ id String @id @default(cuid())
794
+ minimum Int?
795
+ maximum Int?
796
+ crowdSize Service? @relation("CrowdSize")
797
+ serviceRange Service? @relation("ServiceRange")
798
+ }
799
+
800
+ model Availability {
801
+ id String @id @default(cuid())
802
+ startDateTime String
803
+ endDateTime String
804
+ }
805
+
806
+ enum VisibilityPreference {
807
+ Public
808
+ Private
809
+ }
810
+
811
+ enum MusicGenreType {
786
812
  Classical
787
813
  Rock
788
814
  HipHop
@@ -873,8 +899,8 @@ model VendorLink {
873
899
 
874
900
  model ServiceLink {
875
901
  id String @id @default(cuid())
876
- service Service @relation(fields: [servicesId], references: [id], onDelete: Cascade)
877
- link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
878
902
  servicesId String
903
+ service Service @relation(fields: [servicesId], references: [id], onDelete: Cascade)
879
904
  linkId String
905
+ link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
880
906
  }
package/src/index.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  export * from "./extendedSchemas";
2
2
  export * from "./definitions";
3
+ export * from "./utils/ticketListUtils";
4
+ export * from "./utils/dateTimeUtils";
5
+ export * from "./utils/recurrenceUtils";
6
+ export * from "./utils/addressUtils";
@@ -0,0 +1,173 @@
1
+
2
+ const ADDRESS_DELIM = '|';
3
+
4
+ const googleMapsApiKey = process.env.REACT_APP_GOOGLE_MAP_API_KEY as string;
5
+
6
+
7
+ export interface IAddress {
8
+ place: string;
9
+ street: string;
10
+ city: string;
11
+ state: string;
12
+ zipCode: string;
13
+ country: string;
14
+ }
15
+
16
+ export function addressHasEnoughDataForGeolocation(address: IAddress): boolean {
17
+ return !!((address.place && address.city && address.state) || (address.street && address.city && address.state));
18
+ }
19
+
20
+ export function addressValuesToDatabaseAddressString(addressValues: IAddress): string {
21
+ const { place = '', street, city, state, zipCode, country } = addressValues;
22
+ return [place, street, city, state, zipCode, country].join(ADDRESS_DELIM);
23
+ }
24
+
25
+ export function databaseAddressStringToAddressValues(addressString: string | undefined | null): IAddress {
26
+ if (addressString) {
27
+ const addressArray = addressString.split(ADDRESS_DELIM);
28
+ return {
29
+ place: addressArray[0],
30
+ street: addressArray[1],
31
+ city: addressArray[2],
32
+ state: addressArray[3],
33
+ zipCode: addressArray[4],
34
+ country: 'USA'
35
+ }
36
+ }
37
+ return { place: '', street: '', city: '', state: '', zipCode: '', country: '' };
38
+ }
39
+
40
+
41
+ export function databaseAddressStringToOneLineString(addressString: string | undefined | null): string {
42
+ if (addressString) {
43
+ const address = databaseAddressStringToAddressValues(addressString);
44
+ let addressArr = address.place ? [address.place] : [];
45
+ addressArr = [...addressArr, address.street, address.city, address.state];
46
+ const addressStr = addressArr.filter((str) => !!str).join(', ');
47
+ return `${addressStr} ${address.zipCode}`;
48
+ }
49
+ return '';
50
+ }
51
+
52
+ export function databaseAddressStringToDisplayString(addressString: string | undefined | null): string {
53
+ if (addressString) {
54
+ const oneLineString = databaseAddressStringToOneLineString(addressString);
55
+ const formatted = oneLineString.replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ") // Add space between a single uppercase letter and a capitalized word
56
+ .replace(/(\d)(?=[A-Z])/g, "$1 ") // Add space between numbers and letters
57
+ .replace(/,/g, ", "); // Add space after commas
58
+ return formatted;
59
+ }
60
+ return '';
61
+ }
62
+
63
+
64
+ export async function getAddressFromCoordinates( lat: number, lng: number ): Promise<IAddress> {
65
+ const apiUrl = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${googleMapsApiKey}&loading=async`;
66
+ try {
67
+ const response = await fetch(apiUrl);
68
+ const data = await response.json();
69
+ if (data.results.length > 0) {
70
+ const addressComponents = data.results[0].address_components;
71
+
72
+ let street = "";
73
+ let city = "";
74
+ let state = "";
75
+ let zipCode = "";
76
+
77
+ addressComponents.forEach((component: any) => {
78
+ if (component.types.includes("route")) {
79
+ street = component.long_name;
80
+ } else if (component.types.includes("locality")) {
81
+ city = component.long_name;
82
+ } else if (component.types.includes("administrative_area_level_1")) {
83
+ state = component.short_name;
84
+ } else if (component.types.includes("postal_code")) {
85
+ zipCode = component.long_name;
86
+ }
87
+ });
88
+
89
+ return { place: '', street, city, state, zipCode, country: 'USA' };
90
+ } else {
91
+ throw new Error("No address found");
92
+ }
93
+ } catch (error) {
94
+ console.error(error);
95
+ return {
96
+ place: '',
97
+ street: "",
98
+ city: "",
99
+ state: "",
100
+ zipCode: "",
101
+ country: 'USA'
102
+ };
103
+ }
104
+ }
105
+
106
+ export function extractAddressComponents(place: any): IAddress {
107
+ const addressComponents = place.address_components;
108
+ let streetNumber = '';
109
+ let streetName = '';
110
+ let city = '';
111
+ let state = '';
112
+ let zipCode = '';
113
+ let country = 'USA';
114
+
115
+ addressComponents.forEach((component: any) => {
116
+ const types = component.types;
117
+ if (types.includes('street_number')) {
118
+ streetNumber = component.long_name;
119
+ }
120
+ if (types.includes('route')) {
121
+ streetName = component.long_name;
122
+ }
123
+ if (types.includes('locality')) {
124
+ city = component.long_name;
125
+ }
126
+ if (types.includes('administrative_area_level_1')) {
127
+ state = component.short_name;
128
+ }
129
+ if (types.includes('postal_code')) {
130
+ zipCode = component.long_name;
131
+ }
132
+ if (types.includes('country')) {
133
+ country = component.long_name;
134
+ }
135
+ });
136
+
137
+ const street = `${streetNumber} ${streetName}`.trim();
138
+
139
+ return {
140
+ place: place.name || '',
141
+ street,
142
+ city,
143
+ state,
144
+ zipCode,
145
+ country
146
+ };
147
+ }
148
+
149
+
150
+ export async function getGeoCoordinatesFromAddress(address: IAddress): Promise<{lat: number, lng: number} | undefined> {
151
+ const addressStr = `${address.street}, ${address.city}, ${address.state} ${address.zipCode}, ${address.country}`;
152
+
153
+ const response = await fetch(
154
+ `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(addressStr)}&key=${googleMapsApiKey}`
155
+ );
156
+ if (!response.ok) {
157
+ console.error('Geocode response was not ok for address: ' + addressStr);
158
+ }
159
+
160
+ const data = await response.json();
161
+
162
+ if (data.status === 'OK') {
163
+ if (data.results?.length) {
164
+ return data.results[0].geometry.location;
165
+ }
166
+ else {
167
+ console.error('Geocode results were empty with address: ' + addressStr);
168
+ }
169
+ }
170
+ else {
171
+ console.error(`Geocode was not successful with address: ${addressStr}\nfor the following reason: ${data.status}`);
172
+ }
173
+ }
@@ -0,0 +1,177 @@
1
+ import {DateType, DateValueType} from "react-tailwindcss-datepicker";
2
+ import dayjs from "dayjs";
3
+ import dayjsUtc from "dayjs/plugin/utc";
4
+ import dayjsTimeZone from "dayjs/plugin/timezone";
5
+ import {DateTimeArgType} from "../definitions";
6
+
7
+ dayjs.extend(dayjsUtc);
8
+ dayjs.extend(dayjsTimeZone);
9
+
10
+
11
+ const PARSE_TIME_REG = /^(\d{1,2}):(\d{2}) ?([APM]{0,2})$/i;
12
+
13
+ export const DATETIME_FORMAT_STANDARD = "MMM D, YYYY - h:mm A";
14
+ export const DATETIME_FORMAT_ISO_LIKE = "YYYY-MM-DD HH:mm"
15
+ export const DATE_FORMAT_STANDARD = "MM/DD/YYYY";
16
+ export const DATE_FORMAT_ISO = "YYYY-MM-DD";
17
+ export const TIME_FORMAT_AM_PM = "hh:mm A";
18
+
19
+ export interface ITime {
20
+ hours: number;
21
+ minutes: number;
22
+ ampm: string;
23
+ }
24
+
25
+ export function ensureIsDateTime(possiblyADate: DateTimeArgType): Date | undefined {
26
+ if (!possiblyADate) {
27
+ return undefined;
28
+ }
29
+ return typeof possiblyADate === 'string' ? new Date(possiblyADate) : possiblyADate;
30
+ }
31
+
32
+ export function normalizeDate(validDate: Date | string | undefined | null): Date | undefined {
33
+ if (validDate) {
34
+ if (typeof validDate === 'string') {
35
+ validDate = new Date(validDate);
36
+ }
37
+ validDate.setHours(validDate.getHours(), validDate.getMinutes(), 0);
38
+ return validDate;
39
+ }
40
+ }
41
+
42
+ export function normalizeDates(dates: Date[]): Date[] {
43
+ return dates
44
+ .map((date): Date | undefined => {
45
+ return normalizeDate(date);
46
+ })
47
+ .filter((dateTime: Date | undefined): boolean => {
48
+ return !!dateTime
49
+ }) as Date[];
50
+ }
51
+
52
+ export function formatDateTimeToISODateTimeString(date: DateTimeArgType): string | undefined {
53
+ if (!date) {
54
+ return undefined;
55
+ }
56
+ date = new Date(date);
57
+ return date.toISOString();
58
+ }
59
+
60
+ export function dateWithinDateRange(date: DateTimeArgType, dateRange: DateValueType | undefined): boolean {
61
+ const startDate = dayjs(dateRange?.startDate).startOf('day').toDate();
62
+ const endDate = dayjs(dateRange?.endDate).endOf('day').toDate();
63
+
64
+ return compareDateTime(date, startDate) >= 0 &&
65
+ compareDateTime(date, endDate) < 0;
66
+ }
67
+
68
+ export function findLatestDateTime(date1Arg: DateTimeArgType, date2Arg: DateTimeArgType): Date | undefined {
69
+ const date1 = dateTimeTypeToDate(date1Arg);
70
+ const date2 = dateTimeTypeToDate(date2Arg);
71
+ return compareDateTime(date1, date2) > 0 ? date1 : date2;
72
+ }
73
+
74
+ function dateTimeTypeToDate(date: DateTimeArgType): Date | undefined {
75
+ if (typeof date === 'string') {
76
+ return new Date(date);
77
+ }
78
+ else {
79
+ return date ?? undefined;
80
+ }
81
+ }
82
+
83
+ export function isDateTimeLaterThanNow(date: DateTimeArgType): boolean {
84
+ return compareDateTime(date, new Date()) > 0;
85
+ }
86
+
87
+ export function areDateTimesEqual(date1: DateTimeArgType,
88
+ date2: DateTimeArgType): boolean {
89
+ return compareDateTime(date1, date2) === 0;
90
+ }
91
+
92
+ /**
93
+ * Returns a 1 if date1 is later than date2; 0 if both dates are equal; -1 if date2 is later than date1
94
+ * @param date1
95
+ * @param date2
96
+ */
97
+ export function compareDateTime(date1: DateTimeArgType,
98
+ date2: DateTimeArgType): number {
99
+ if (!date1 && !date2) {
100
+ return 0;
101
+ }
102
+ if (!date1) {
103
+ return -1;
104
+ }
105
+ if (!date2) {
106
+ return 1;
107
+ }
108
+ if (typeof date1 === 'string') {
109
+ date1 = new Date(date1);
110
+ }
111
+ if (typeof date2 === 'string') {
112
+ date2 = new Date(date2);
113
+ }
114
+
115
+ const date1Time = date1.getTime();
116
+ const date2Time = date2.getTime();
117
+
118
+ if (date1Time > date2Time) {
119
+ return 1;
120
+ }
121
+ if (date1Time === date2Time) {
122
+ return 0;
123
+ }
124
+ else {
125
+ return -1;
126
+ }
127
+ }
128
+
129
+ export function setDateButPreserveTime(dateArg: DateType, dateWithTheTimeYouWantToKeep: DateType | undefined): Date {
130
+ const date = dayjs(dateArg).format(DATE_FORMAT_ISO);
131
+ const time = dayjs(dateWithTheTimeYouWantToKeep).format(TIME_FORMAT_AM_PM);
132
+ const combinedDateTime = dayjs(date + " " + time).toDate();
133
+ return combinedDateTime;
134
+ }
135
+
136
+ export function setTimeOnDate(date: DateType | Date | undefined, parsedTime: ITime): Date {
137
+ const dateTime = new Date(date ?? Date.now());
138
+ const parsedTimeDate = new Date();
139
+ parsedTimeDate.setHours(parsedTime.hours, parsedTime.minutes, 0); // Will change the date depending on time; corrected later
140
+ const correctTimeOnDate = setDateButPreserveTime(dateTime, parsedTimeDate);
141
+ return correctTimeOnDate;
142
+ }
143
+
144
+ export function ensureDateTimeIsLocalDateTime(dateArg: DateType | undefined): Date {
145
+ if (dayjs(dateArg).isUTC()) {
146
+ const dateLocalTimeZone = dayjs(dateArg);
147
+ const date = dateLocalTimeZone.toDate();
148
+ return date;
149
+ }
150
+ else {
151
+ return new Date(dateArg ?? Date.now());
152
+ }
153
+ }
154
+
155
+ export function formatDateTimeToTimeString(date: Date | string | undefined | null): string {
156
+ date = new Date(date ?? Date.now());
157
+ const result = dayjs(date).format("h:mm A");
158
+ return result;
159
+ }
160
+
161
+ export function parseTimeString(timeStr: string): ITime | null {
162
+ const match = timeStr.match(PARSE_TIME_REG);
163
+
164
+ if (match) {
165
+ const hours = parseInt(match[1], 10);
166
+ const minutes = parseInt(match[2], 10);
167
+ const ampm = match[3].toUpperCase();
168
+
169
+ if ((hours >= 0 && hours <= 12) && (minutes >= 0 && minutes <= 59) && (ampm === 'AM' || ampm === 'PM')) {
170
+ return {
171
+ hours: ampm === 'PM' && hours < 12 ? hours + 12 : hours,
172
+ minutes,
173
+ ampm };
174
+ }
175
+ }
176
+ return null;
177
+ }
@@ -0,0 +1,175 @@
1
+ import {DayOfWeek, Recurrence, RecurringFrequency} from "@prisma/client";
2
+ import dayjs, {Dayjs} from "dayjs";
3
+ import dayjsWeekDay from "dayjs/plugin/weekday";
4
+ import {DateTimeArgType} from "../definitions";
5
+ import {compareDateTime, DATETIME_FORMAT_ISO_LIKE} from "./dateTimeUtils";
6
+
7
+ dayjs.extend(dayjsWeekDay);
8
+
9
+
10
+ export function getRecurringBashEventPossibleDateTimes(startDateTime: DateTimeArgType,
11
+ recurrence: Recurrence | undefined | null): Date[] {
12
+ return getRecurringBashEventPossibleDatesTimesInternal(startDateTime, recurrence, true) as Date[];
13
+ }
14
+
15
+ export function getRecurringBashEventPossibleDayjsTimes(startDateTime: DateTimeArgType,
16
+ recurrence: Recurrence | undefined | null): Dayjs[] {
17
+ return getRecurringBashEventPossibleDatesTimesInternal(startDateTime, recurrence, false) as Dayjs[];
18
+ }
19
+
20
+
21
+ function getRecurringBashEventPossibleDatesTimesInternal(startDateTime: DateTimeArgType,
22
+ recurrence: Recurrence | undefined | null,
23
+ toDate: boolean): (Dayjs | Date)[] {
24
+ if (!recurrence || !recurrence.frequency || recurrence.frequency === RecurringFrequency.Never) {
25
+ return [];
26
+ }
27
+
28
+ let recurrenceDates: Dayjs[] = [];
29
+ const beginningDate = findEarliestPossibleBashEventDate(startDateTime) as Dayjs; // Don't allow dates before the current date
30
+
31
+ switch (recurrence.frequency) {
32
+ case RecurringFrequency.Weekly:
33
+ recurrenceDates = getWeeklyRecurringDates(beginningDate, recurrence);
34
+ break;
35
+ default:
36
+ console.error(`Only weekly frequency is currently supported`);
37
+ }
38
+ return recurrenceDates.map((date: Dayjs): string => date.format(DATETIME_FORMAT_ISO_LIKE))
39
+ .sort()
40
+ .map((dateStr: string): Dayjs | Date => {
41
+ if (toDate) {
42
+ return dayjs(dateStr).toDate();
43
+ }
44
+ else {
45
+ return dayjs(dateStr);
46
+ }
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Get the weekly recurring dates
52
+ * @param beginningDateTime The beginning DateTime of the event, adjusted to be no earlier than now.
53
+ * @param recurrence
54
+ */
55
+ function getWeeklyRecurringDates(beginningDateTime: Dayjs, recurrence: Recurrence): Dayjs[] {
56
+ if (recurrence.frequency !== RecurringFrequency.Weekly) {
57
+ throw new Error(`Cannot do a weekly recurrence if the frequency isn't weekly.`);
58
+ }
59
+
60
+ const interval = recurrence.interval ?? 1;
61
+ const recurrenceEnds = dayjs(recurrence.ends).endOf('day'); // Get the end of the day to not miss the last day
62
+ const numberOfDays = recurrenceEnds.diff(beginningDateTime, 'days');
63
+ const numberOfWeeks = Math.ceil((numberOfDays / 7) / interval); // Get the number of days and then round up so that we include the ending date
64
+
65
+ const recurrenceDates: Dayjs[] = [];
66
+ let theNextWeekOfRecurrences = beginningDateTime;
67
+
68
+ // Go week by week getting the next recurrence date
69
+ for (let i = 0; i < numberOfWeeks; i++) {
70
+ let weekday: Dayjs | null = null;
71
+ for (const dayOfWeekEnum of recurrence.repeatOnDays) {
72
+ const dayOfWeekNum = dayOfWeekEnumToDayNumber(dayOfWeekEnum);
73
+ weekday = theNextWeekOfRecurrences.weekday(dayOfWeekNum);
74
+ if (weekday > recurrenceEnds || weekday < beginningDateTime) {
75
+ continue; // Continue because repeatOnDays isn't sorted by the day of the week
76
+ }
77
+ // Set the time on the date so that it matches the time when the event starts
78
+ weekday = copyTimeToDate(beginningDateTime, weekday);
79
+ recurrenceDates.push(weekday);
80
+ }
81
+ if (weekday) {
82
+ theNextWeekOfRecurrences = getBeginningOfWeekInterval(weekday, interval);
83
+ }
84
+ }
85
+ return recurrenceDates;
86
+ }
87
+
88
+ function copyTimeToDate(dateWithTimeToCopy: Dayjs, dateWhoseTimeToChange: Dayjs): Dayjs {
89
+ if (dateWithTimeToCopy.second() !== 0) {
90
+ console.warn(`dateWithTimeToCopy has non-zero seconds: ${dateWithTimeToCopy.toString()} \nFixing...`);
91
+ dateWithTimeToCopy = dateWithTimeToCopy.set('seconds', 0);
92
+ }
93
+ dateWhoseTimeToChange = dateWhoseTimeToChange.set('hours', dateWithTimeToCopy.hour());
94
+ dateWhoseTimeToChange = dateWhoseTimeToChange.set('minutes', dateWithTimeToCopy.minute());
95
+ dateWhoseTimeToChange = dateWhoseTimeToChange.set('seconds', 0); // Should be 0
96
+ return dateWhoseTimeToChange;
97
+ }
98
+
99
+ function dayOfWeekEnumToDayNumber(dayEnum: DayOfWeek): number {
100
+ switch (dayEnum) {
101
+ case DayOfWeek.Sunday:
102
+ return 0;
103
+ case DayOfWeek.Monday:
104
+ return 1;
105
+ case DayOfWeek.Tuesday:
106
+ return 2;
107
+ case DayOfWeek.Wednesday:
108
+ return 3;
109
+ case DayOfWeek.Thursday:
110
+ return 4;
111
+ case DayOfWeek.Friday:
112
+ return 5;
113
+ case DayOfWeek.Saturday:
114
+ return 6;
115
+ default:
116
+ throw new Error(`How did this happen? Invalid DayOfWeek: ${dayEnum}`); // Shouldn't happen
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get the next week determined by the interval by going to the end of the week interval and adding one day
122
+ * @param date
123
+ * @param interval
124
+ */
125
+ function getBeginningOfWeekInterval(date: Dayjs, interval: number): Dayjs {
126
+ if (!interval) {
127
+ interval = 1; // Avoid 0
128
+ }
129
+ // An interval of 2 would be (2 - 1) * 7 + 1 = 8 days to add to skip a week and go into the next week
130
+ const numberOfDaysToAdd = interval > 1 ? (interval - 1) * 7 + 1 : 1;
131
+ return date.endOf('week').add(numberOfDaysToAdd, 'day');
132
+ }
133
+
134
+ export function freqToGrammarString(freq: RecurringFrequency, interval: number | undefined, toLowerCase: boolean = false): string {
135
+ if (!interval) {
136
+ return freq;
137
+ }
138
+ const isPlural = interval > 1;
139
+ let result: string = freq;
140
+ switch (freq) {
141
+ case RecurringFrequency.Daily:
142
+ result = isPlural ? 'Days' : 'Day';
143
+ break;
144
+ case RecurringFrequency.Weekly:
145
+ result = isPlural ? 'Weeks' : 'Week';
146
+ break;
147
+ case RecurringFrequency.Monthly:
148
+ result = isPlural ? 'Months' : 'Month';
149
+ break;
150
+ case RecurringFrequency.Yearly:
151
+ result = isPlural ? 'Years' : 'Year';
152
+ break;
153
+ }
154
+ return toLowerCase ? result.toLowerCase() : result;
155
+ }
156
+
157
+ export function findEarliestPossibleBashEventDate(startDateTimeArg: DateTimeArgType): Dayjs | undefined {
158
+ return findEarliestOrLatestPossibleBashEventDate(startDateTimeArg, true);
159
+ }
160
+
161
+ function findEarliestOrLatestPossibleBashEventDate(startDateTimeArg: DateTimeArgType, findEarly: boolean): Dayjs | undefined {
162
+ if (!startDateTimeArg) {
163
+ return;
164
+ }
165
+ // Don't allow dates before the current date
166
+ const startDateTime = dayjs(startDateTimeArg);
167
+ const currentDateTime = dayjs();
168
+ const comparedDateTime = compareDateTime(currentDateTime.toDate(), startDateTimeArg);
169
+ if (findEarly) {
170
+ return comparedDateTime > 0 ? currentDateTime : startDateTime;
171
+ }
172
+ else {
173
+ return comparedDateTime < 0 ? currentDateTime : startDateTime;
174
+ }
175
+ }
@@ -0,0 +1,69 @@
1
+ import dayjs from "dayjs";
2
+ import {
3
+ NumberOfTicketsForDate,
4
+ URL_PARAMS_NUMBER_OF_TICKETS_TICKETS_DATE_DELIM,
5
+ URL_PARAMS_TICKET_ID_NUMBER_OF_TICKETS_DATE_DELIM,
6
+ URL_PARAMS_TICKET_LIST_DELIM,
7
+ URL_PARAMS_TICKETS_DATE_DELIM
8
+ } from "../definitions";
9
+ import { DATETIME_FORMAT_ISO_LIKE } from "./dateTimeUtils";
10
+
11
+
12
+ /**
13
+ * Returns A string structured like: [ticketTierId]__[numberOfTickets];;[date]~~[numberOfTickets];;[date]~~,...
14
+ * @param ticketList A map where the key is the ticketTierId
15
+ */
16
+ export function ticketListToString(ticketList: Map<string, NumberOfTicketsForDate[]>): string {
17
+ const ticketListArr: string[] = [];
18
+
19
+ for (const [ticketTierId, ticketListArgs] of ticketList.entries()) {
20
+ const ticketArgs: string[] = [`${ticketTierId}${URL_PARAMS_TICKET_ID_NUMBER_OF_TICKETS_DATE_DELIM}`];
21
+ for (const ticketNumAndDates of ticketListArgs) {
22
+ if (ticketNumAndDates.numberOfTickets > 0) {
23
+ ticketArgs.push(
24
+ `${ticketNumAndDates.numberOfTickets}${URL_PARAMS_TICKETS_DATE_DELIM}${ticketNumAndDates.ticketDateTime}`);
25
+ }
26
+ }
27
+ ticketListArr.push(ticketArgs.join(URL_PARAMS_NUMBER_OF_TICKETS_TICKETS_DATE_DELIM));
28
+ }
29
+ return ticketListArr.join(URL_PARAMS_TICKET_LIST_DELIM);
30
+ }
31
+
32
+ /**
33
+ * Returns map where the key is the ticketTierId
34
+ * @param ticketListStr A string structured like: [ticketTierId]__[numberOfTickets];;[date]~~[numberOfTickets];;[date]~~...,...
35
+ */
36
+ export function ticketListStrToTicketList(ticketListStr: string): Map<string, NumberOfTicketsForDate[]> {
37
+ const ticketList: Map<string, NumberOfTicketsForDate[]> = new Map();
38
+
39
+ // [ticketTierId]__[numberOfTickets];;[date]~~[numberOfTickets];;[date]~~...,...
40
+ const ticketListArgs = ticketListStr.split(URL_PARAMS_TICKET_LIST_DELIM);
41
+
42
+ ticketListArgs.forEach((tierIdAndTicketNumbersAndDates: string): void => {
43
+ const ticketNumAndDatesArr: NumberOfTicketsForDate[] = [];
44
+ // [ticketTierId]__[numberOfTickets];;[date]~~[numberOfTickets];;[date]~~...
45
+ const [ticketTierId, ticketNumAndDatesStr] = tierIdAndTicketNumbersAndDates.split(URL_PARAMS_TICKET_ID_NUMBER_OF_TICKETS_DATE_DELIM);
46
+ // [numberOfTickets];;[date]~~[numberOfTickets];;[date]~~...
47
+ const ticketNumAndDates = ticketNumAndDatesStr.split(URL_PARAMS_NUMBER_OF_TICKETS_TICKETS_DATE_DELIM)
48
+ .filter((ticketNumAndDateStr): boolean => !!ticketNumAndDateStr);
49
+
50
+ for (const ticketNumAndDateStr of ticketNumAndDates) {
51
+ // [numberOfTickets];;[date]
52
+ const [numberOfTickets, ticketDateTimeStr] = ticketNumAndDateStr.split(URL_PARAMS_TICKETS_DATE_DELIM);
53
+ let ticketDateTime: string | undefined;
54
+ if (!ticketDateTimeStr) {
55
+ ticketDateTime = undefined;
56
+ }
57
+ else {
58
+ ticketDateTime = dayjs(ticketDateTimeStr).format(DATETIME_FORMAT_ISO_LIKE);
59
+ }
60
+
61
+ ticketNumAndDatesArr.push({
62
+ numberOfTickets: parseInt(numberOfTickets),
63
+ ticketDateTime: ticketDateTime,
64
+ });
65
+ }
66
+ ticketList.set(ticketTierId, ticketNumAndDatesArr);
67
+ });
68
+ return ticketList;
69
+ }