@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 +4 -2
- package/prisma/schema.prisma +44 -18
- package/src/index.ts +4 -0
- package/src/utils/addressUtils.ts +173 -0
- package/src/utils/dateTimeUtils.ts +177 -0
- package/src/utils/recurrenceUtils.ts +175 -0
- package/src/utils/ticketListUtils.ts +69 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bash-app/bash-common",
|
|
3
|
-
"version": "1.0
|
|
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",
|
package/prisma/schema.prisma
CHANGED
|
@@ -767,22 +767,48 @@ model AssociatedBash {
|
|
|
767
767
|
}
|
|
768
768
|
|
|
769
769
|
model Service {
|
|
770
|
-
id
|
|
771
|
-
ownerId
|
|
772
|
-
owner
|
|
773
|
-
name
|
|
774
|
-
coverPhoto
|
|
775
|
-
media
|
|
776
|
-
serviceType
|
|
777
|
-
subServiceType
|
|
778
|
-
serviceLinks
|
|
779
|
-
address
|
|
780
|
-
description
|
|
781
|
-
canContact
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
@@ -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
|
+
}
|