serene-core-client 0.1.1
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/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/src/apollo/access.d.ts +1 -0
- package/dist/src/apollo/access.js +9 -0
- package/dist/src/apollo/mailing-lists.d.ts +2 -0
- package/dist/src/apollo/mailing-lists.js +33 -0
- package/dist/src/apollo/quotas.d.ts +1 -0
- package/dist/src/apollo/quotas.js +21 -0
- package/dist/src/apollo/techs.d.ts +1 -0
- package/dist/src/apollo/techs.js +15 -0
- package/dist/src/apollo/tips.d.ts +3 -0
- package/dist/src/apollo/tips.js +45 -0
- package/dist/src/apollo/user-preferences.d.ts +2 -0
- package/dist/src/apollo/user-preferences.js +32 -0
- package/dist/src/apollo/users.d.ts +6 -0
- package/dist/src/apollo/users.js +60 -0
- package/dist/src/components/account/profile.d.ts +8 -0
- package/dist/src/components/account/profile.js +137 -0
- package/dist/src/components/basics/copy-text-icon.d.ts +7 -0
- package/dist/src/components/basics/copy-text-icon.js +15 -0
- package/dist/src/components/basics/editable-string-list.d.ts +7 -0
- package/dist/src/components/basics/editable-string-list.js +41 -0
- package/dist/src/components/basics/json-display.d.ts +7 -0
- package/dist/src/components/basics/json-display.js +37 -0
- package/dist/src/components/basics/mui-auto-focus.d.ts +2 -0
- package/dist/src/components/basics/mui-auto-focus.js +16 -0
- package/dist/src/components/basics/text-area-field.d.ts +12 -0
- package/dist/src/components/basics/text-area-field.js +23 -0
- package/dist/src/components/basics/view-link-field.d.ts +8 -0
- package/dist/src/components/basics/view-link-field.js +6 -0
- package/dist/src/components/basics/view-markdown-field.d.ts +7 -0
- package/dist/src/components/basics/view-markdown-field.js +7 -0
- package/dist/src/components/basics/view-text-field.d.ts +7 -0
- package/dist/src/components/basics/view-text-field.js +6 -0
- package/dist/src/components/buttons/labeled-icon-button.d.ts +12 -0
- package/dist/src/components/buttons/labeled-icon-button.js +19 -0
- package/dist/src/components/css/loaders.d.ts +0 -0
- package/dist/src/components/css/loaders.js +1 -0
- package/dist/src/components/mailing-lists/sign-up.d.ts +5 -0
- package/dist/src/components/mailing-lists/sign-up.js +75 -0
- package/dist/src/components/notifications/action.d.ts +8 -0
- package/dist/src/components/notifications/action.js +15 -0
- package/dist/src/components/tech/load-techs.d.ts +7 -0
- package/dist/src/components/tech/load-techs.js +41 -0
- package/dist/src/components/tech/tech-autocomplete.d.ts +17 -0
- package/dist/src/components/tech/tech-autocomplete.js +15 -0
- package/dist/src/components/tips/load-tips.d.ts +7 -0
- package/dist/src/components/tips/load-tips.js +47 -0
- package/dist/src/components/tips/tip.d.ts +11 -0
- package/dist/src/components/tips/tip.js +44 -0
- package/dist/src/services/access/service.d.ts +26 -0
- package/dist/src/services/access/service.js +82 -0
- package/dist/src/services/locale/countries.d.ts +6 -0
- package/dist/src/services/locale/countries.js +481 -0
- package/dist/src/services/rest-api/request.d.ts +1 -0
- package/dist/src/services/rest-api/request.js +8 -0
- package/dist/src/services/rest-api/service.d.ts +21 -0
- package/dist/src/services/rest-api/service.js +79 -0
- package/dist/src/services/users/profile-service.d.ts +16 -0
- package/dist/src/services/users/profile-service.js +24 -0
- package/dist/src/services/users/user-preferences-service.d.ts +6 -0
- package/dist/src/services/users/user-preferences-service.js +41 -0
- package/dist/src/services/users/user-service.d.ts +22 -0
- package/dist/src/services/users/user-service.js +208 -0
- package/dist/src/services/utils/date.d.ts +1 -0
- package/dist/src/services/utils/date.js +4 -0
- package/dist/src/services/utils/functions.d.ts +1 -0
- package/dist/src/services/utils/functions.js +6 -0
- package/dist/src/services/utils/service.d.ts +3 -0
- package/dist/src/services/utils/service.js +9 -0
- package/dist/src/services/utils/string.d.ts +5 -0
- package/dist/src/services/utils/string.js +37 -0
- package/dist/src/services/utils/tree.d.ts +4 -0
- package/dist/src/services/utils/tree.js +65 -0
- package/dist/src/types/types.d.ts +3 -0
- package/dist/src/types/types.js +3 -0
- package/index.ts +36 -0
- package/package.json +32 -0
- package/src/apollo/access.ts +10 -0
- package/src/apollo/mailing-lists.ts +35 -0
- package/src/apollo/quotas.ts +22 -0
- package/src/apollo/techs.ts +16 -0
- package/src/apollo/tips.ts +48 -0
- package/src/apollo/user-preferences.ts +34 -0
- package/src/apollo/users.ts +66 -0
- package/src/components/account/profile.tsx +279 -0
- package/src/components/basics/copy-text-icon.tsx +34 -0
- package/src/components/basics/editable-string-list.tsx +97 -0
- package/src/components/basics/json-display.tsx +71 -0
- package/src/components/basics/mui-auto-focus.tsx +19 -0
- package/src/components/basics/text-area-field.tsx +63 -0
- package/src/components/basics/view-link-field.tsx +32 -0
- package/src/components/basics/view-markdown-field.tsx +31 -0
- package/src/components/basics/view-text-field.tsx +28 -0
- package/src/components/buttons/labeled-icon-button.tsx +53 -0
- package/src/components/css/loaders.tsx +0 -0
- package/src/components/mailing-lists/sign-up.tsx +126 -0
- package/src/components/notifications/action.tsx +50 -0
- package/src/components/tech/load-techs.tsx +66 -0
- package/src/components/tech/tech-autocomplete.tsx +65 -0
- package/src/components/tips/load-tips.tsx +75 -0
- package/src/components/tips/tip.tsx +96 -0
- package/src/services/access/service.ts +126 -0
- package/src/services/locale/countries.ts +483 -0
- package/src/services/rest-api/request.ts +10 -0
- package/src/services/rest-api/service.ts +130 -0
- package/src/services/users/profile-service.ts +28 -0
- package/src/services/users/user-preferences-service.ts +64 -0
- package/src/services/users/user-service.ts +310 -0
- package/src/services/utils/date.ts +6 -0
- package/src/services/utils/functions.ts +8 -0
- package/src/services/utils/service.ts +12 -0
- package/src/services/utils/string.ts +48 -0
- package/src/services/utils/tree.ts +88 -0
- package/src/types/types.ts +4 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getUserPreferencesQuery } from '../../apollo/user-preferences'
|
|
2
|
+
|
|
3
|
+
export class UserPreferencesService {
|
|
4
|
+
|
|
5
|
+
zipCountries = [ 'Philippines',
|
|
6
|
+
'United States' ]
|
|
7
|
+
|
|
8
|
+
async getByKeys(
|
|
9
|
+
apolloClient: any,
|
|
10
|
+
userProfileId: string,
|
|
11
|
+
category: string,
|
|
12
|
+
keys: string[]) {
|
|
13
|
+
|
|
14
|
+
// GraphQL call to get or create user
|
|
15
|
+
var results: any = null
|
|
16
|
+
|
|
17
|
+
console.log('UserPreferencesService.getByKeys(): ' +
|
|
18
|
+
`userProfileId: ${userProfileId} category: ${category} ` +
|
|
19
|
+
`keys: ${keys}`)
|
|
20
|
+
|
|
21
|
+
await apolloClient.query({
|
|
22
|
+
query: getUserPreferencesQuery,
|
|
23
|
+
variables: {
|
|
24
|
+
userProfileId: userProfileId,
|
|
25
|
+
category: category,
|
|
26
|
+
keys: keys
|
|
27
|
+
}
|
|
28
|
+
}).then((result: any) => results = result)
|
|
29
|
+
.catch((error: any) => {
|
|
30
|
+
console.log(`error.networkError: ${JSON.stringify(error.networkError)}`)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
console.log(`results: ${JSON.stringify(results)}`)
|
|
34
|
+
|
|
35
|
+
return results.data['getUserPreferences']
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getZipOrPostalCodeLabel(
|
|
39
|
+
country: string,
|
|
40
|
+
billingCountryAsMyCountry: boolean,
|
|
41
|
+
billingCountry: string) {
|
|
42
|
+
|
|
43
|
+
if (this.hasZipInsteadOfPostalCode(
|
|
44
|
+
country,
|
|
45
|
+
billingCountryAsMyCountry,
|
|
46
|
+
billingCountry)) {
|
|
47
|
+
return 'Zip'
|
|
48
|
+
} else {
|
|
49
|
+
return 'Postal code'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
hasZipInsteadOfPostalCode(
|
|
54
|
+
country: string,
|
|
55
|
+
billingCountryAsMyCountry: boolean,
|
|
56
|
+
billingCountry: string) {
|
|
57
|
+
|
|
58
|
+
if (billingCountryAsMyCountry === true) {
|
|
59
|
+
return this.zipCountries.includes(country)
|
|
60
|
+
} else {
|
|
61
|
+
return this.zipCountries.includes(billingCountry)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { getCookie, setCookie } from 'cookies-next'
|
|
2
|
+
import { getOrCreateSignedOutUserMutation, getOrCreateUserByEmailMutation, verifySignedInUserProfileIdQuery } from '../../apollo/users'
|
|
3
|
+
import { getSession } from 'next-auth/react'
|
|
4
|
+
|
|
5
|
+
interface ReqRes {
|
|
6
|
+
req: any
|
|
7
|
+
res: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class UsersService {
|
|
11
|
+
|
|
12
|
+
// Consts
|
|
13
|
+
clName = 'UsersService'
|
|
14
|
+
signedInCookieName = 'signedInUserUq'
|
|
15
|
+
signedOutCookieName = 'signedOutUserUq'
|
|
16
|
+
|
|
17
|
+
// Code
|
|
18
|
+
formatCreateBlankUser(json: any) {
|
|
19
|
+
|
|
20
|
+
// Debug
|
|
21
|
+
const fnName = `${this.clName}.formatCreateBlankUser()`
|
|
22
|
+
|
|
23
|
+
// console.log(`${fnName}: json: ` + JSON.stringify(json))
|
|
24
|
+
|
|
25
|
+
// Formatting
|
|
26
|
+
const userRecord = json.createBlankUser
|
|
27
|
+
|
|
28
|
+
if (!userRecord) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var user = {
|
|
33
|
+
'id': userRecord
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return user
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
formatUserById(json: any) {
|
|
40
|
+
|
|
41
|
+
// Debug
|
|
42
|
+
const fnName = `${this.clName}.formatUserById()`
|
|
43
|
+
|
|
44
|
+
// console.log(`${fnName}: json: ` + JSON.stringify(json))
|
|
45
|
+
|
|
46
|
+
// Formatting
|
|
47
|
+
const userRecord = json.userById
|
|
48
|
+
|
|
49
|
+
if (!userRecord) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var user = {
|
|
54
|
+
'id': userRecord.id
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return user
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getSignedInOrOutUserIdFromCookie(
|
|
61
|
+
{ req, res }: ReqRes,
|
|
62
|
+
apolloClient: any) {
|
|
63
|
+
|
|
64
|
+
const session = await getSession({req: req})
|
|
65
|
+
|
|
66
|
+
if (session) {
|
|
67
|
+
return this.getUserIdFromCookieAndVerify(
|
|
68
|
+
{req: req, res: res},
|
|
69
|
+
apolloClient)
|
|
70
|
+
} else {
|
|
71
|
+
return this.getSignedOutUserIdFromCookie({req: req, res: res})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getSignedOutUserIdFromCookie(
|
|
76
|
+
{ req, res }: ReqRes) {
|
|
77
|
+
|
|
78
|
+
// Debug
|
|
79
|
+
const fnName = `${this.clName}.getSignedOutUserIdFromCookie()`
|
|
80
|
+
|
|
81
|
+
// Get cookie
|
|
82
|
+
const signedOutIdValue =
|
|
83
|
+
getCookie(this.signedOutCookieName,
|
|
84
|
+
{ req, res })
|
|
85
|
+
|
|
86
|
+
// Debug
|
|
87
|
+
// console.log(`${fnName}: signedOutIdValue: ${signedOutIdValue}`)
|
|
88
|
+
|
|
89
|
+
// Return
|
|
90
|
+
var id
|
|
91
|
+
|
|
92
|
+
if (signedOutIdValue == null) {
|
|
93
|
+
return null
|
|
94
|
+
} else {
|
|
95
|
+
id = signedOutIdValue
|
|
96
|
+
return id
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async getUserIdFromCookieAndVerify(
|
|
101
|
+
{ req, res }: ReqRes,
|
|
102
|
+
apolloClient: any) {
|
|
103
|
+
|
|
104
|
+
// Debug
|
|
105
|
+
const fnName = `${this.clName}.getUserIdFromCookieAndVerify()`
|
|
106
|
+
|
|
107
|
+
// Get signed-in userId
|
|
108
|
+
const idValue: string | undefined = await
|
|
109
|
+
getCookie(
|
|
110
|
+
this.signedInCookieName,
|
|
111
|
+
{ req, res })
|
|
112
|
+
|
|
113
|
+
// Debug
|
|
114
|
+
// console.log(`${fnName}: idValue: ${idValue}`)
|
|
115
|
+
|
|
116
|
+
// Set userProfileId if idValue not null
|
|
117
|
+
var userProfileId: string | null | undefined = undefined
|
|
118
|
+
|
|
119
|
+
if (idValue == null) {
|
|
120
|
+
return null
|
|
121
|
+
} else {
|
|
122
|
+
userProfileId = idValue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Verify that the signed-in userId exists
|
|
126
|
+
|
|
127
|
+
// GraphQL get or create user
|
|
128
|
+
var results: any = null
|
|
129
|
+
|
|
130
|
+
await apolloClient.query({
|
|
131
|
+
query: verifySignedInUserProfileIdQuery,
|
|
132
|
+
variables: {
|
|
133
|
+
userProfileId: userProfileId
|
|
134
|
+
}
|
|
135
|
+
}).then((result: any) => results = result)
|
|
136
|
+
.catch((error: any) => {
|
|
137
|
+
console.log(`${fnName}: error.networkError: ` +
|
|
138
|
+
JSON.stringify(error.networkError))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// For now, if the userId isn't found in the DB, return null.
|
|
142
|
+
// TODO: return an error message, that the user should clear the browser
|
|
143
|
+
// cache and retry.
|
|
144
|
+
if (results != null) {
|
|
145
|
+
if (results.data['verifySignedInUserProfileId']) {
|
|
146
|
+
if (results.data['verifySignedInUserProfileId'] === false) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Return
|
|
157
|
+
return userProfileId
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async getOrCreateUser(
|
|
161
|
+
{ req, res }: ReqRes,
|
|
162
|
+
session: any,
|
|
163
|
+
apolloClient: any,
|
|
164
|
+
defaultUserPreferences: any) {
|
|
165
|
+
|
|
166
|
+
// Debug
|
|
167
|
+
const fnName = `${this.clName}.getOrCreateUser()`
|
|
168
|
+
|
|
169
|
+
// Cookie values
|
|
170
|
+
var signedInId: string | null | undefined = ''
|
|
171
|
+
var signedOutId: string | null | undefined = ''
|
|
172
|
+
|
|
173
|
+
if (session) {
|
|
174
|
+
|
|
175
|
+
signedInId = await
|
|
176
|
+
this.getUserIdFromCookieAndVerify(
|
|
177
|
+
{ req, res },
|
|
178
|
+
apolloClient)
|
|
179
|
+
|
|
180
|
+
} else {
|
|
181
|
+
// Note: don't remove the await (getCookie needs it)
|
|
182
|
+
signedOutId = await this.getSignedOutUserIdFromCookie({ req, res })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Signed-out get/create user
|
|
186
|
+
const originalSignedOutId = signedOutId
|
|
187
|
+
|
|
188
|
+
if (session) {
|
|
189
|
+
|
|
190
|
+
// Debug
|
|
191
|
+
// console.log(`${fnName}: hasSession: ${!!session} email: ${session.user.email}`)
|
|
192
|
+
|
|
193
|
+
// GraphQL call to get or create user
|
|
194
|
+
var results: any = null
|
|
195
|
+
|
|
196
|
+
await apolloClient.mutate({
|
|
197
|
+
mutation: getOrCreateUserByEmailMutation,
|
|
198
|
+
variables: {
|
|
199
|
+
email: session.user.email,
|
|
200
|
+
defaultUserPreferences: JSON.stringify(defaultUserPreferences)
|
|
201
|
+
}
|
|
202
|
+
}).then((result: any) => results = result)
|
|
203
|
+
.catch((error: any) => {
|
|
204
|
+
console.log(`${fnName}: error: ${error}`)
|
|
205
|
+
console.log(`${fnName}: error.networkError: ${JSON.stringify(error.networkError)}`)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Debug
|
|
209
|
+
// console.log(`${fnName}: results: ` + JSON.stringify(results))
|
|
210
|
+
|
|
211
|
+
// Get results
|
|
212
|
+
const newSignedInUserProfile = results.data['getOrCreateUserByEmail']
|
|
213
|
+
|
|
214
|
+
// Update with newly created userProfile record
|
|
215
|
+
if (signedInId !== newSignedInUserProfile.id) {
|
|
216
|
+
signedInId = newSignedInUserProfile.id
|
|
217
|
+
|
|
218
|
+
// console.log('getOrCreateUser: calling setSignedInUserCookie..')
|
|
219
|
+
this.setSignedInUserCookie(
|
|
220
|
+
{req, res},
|
|
221
|
+
newSignedInUserProfile.id)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Return
|
|
225
|
+
return newSignedInUserProfile
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// No session available (signed-out)
|
|
229
|
+
// If not signed-in/out user cookie
|
|
230
|
+
if (originalSignedOutId !== signedOutId) {
|
|
231
|
+
|
|
232
|
+
this.setSignedOutUserCookie(
|
|
233
|
+
{req, res},
|
|
234
|
+
signedOutId)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// GraphQL call to get or create userProfile for signed-out user
|
|
238
|
+
// If a signedOutId exists, but the record doesn't, the DB's signed out
|
|
239
|
+
// userProfiles could have been cleaned.
|
|
240
|
+
var results: any = null
|
|
241
|
+
|
|
242
|
+
await apolloClient.mutate({
|
|
243
|
+
mutation: getOrCreateSignedOutUserMutation,
|
|
244
|
+
variables: {
|
|
245
|
+
signedOutId: signedOutId,
|
|
246
|
+
defaultUserPreferences: JSON.stringify(defaultUserPreferences)
|
|
247
|
+
}
|
|
248
|
+
}).then((result: any) => results = result)
|
|
249
|
+
.catch((error: any) => {
|
|
250
|
+
console.log(`${fnName}: error: ${error}`)
|
|
251
|
+
console.log(`${fnName}: error.networkError: ${JSON.stringify(error.networkError)}`)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Handle null result
|
|
255
|
+
if (results == null) {
|
|
256
|
+
|
|
257
|
+
const message = `${fnName}: null returned from GraphQL request`
|
|
258
|
+
|
|
259
|
+
console.error(message)
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Debug
|
|
264
|
+
// console.log(`${fnName}: results: ` + JSON.stringify(results))
|
|
265
|
+
|
|
266
|
+
// Get results
|
|
267
|
+
var signedOutUserProfile: any
|
|
268
|
+
|
|
269
|
+
const signedOutUser = results.data['getOrCreateSignedOutUser']
|
|
270
|
+
|
|
271
|
+
if (signedOutUser) {
|
|
272
|
+
signedOutUserProfile = signedOutUser
|
|
273
|
+
signedOutId = signedOutUserProfile.id
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create cookie for signed-out id
|
|
277
|
+
this.setSignedOutUserCookie({req, res}, signedOutId)
|
|
278
|
+
|
|
279
|
+
// Return
|
|
280
|
+
// console.log(`${fnName}: returning signed-out user id: ${signedOutId}`)
|
|
281
|
+
|
|
282
|
+
return signedOutUserProfile
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setSignedInUserCookie(
|
|
286
|
+
{ req, res }: ReqRes,
|
|
287
|
+
id: string) {
|
|
288
|
+
|
|
289
|
+
// Set userUq in cookie
|
|
290
|
+
setCookie(
|
|
291
|
+
this.signedInCookieName,
|
|
292
|
+
id,
|
|
293
|
+
{ req,
|
|
294
|
+
res,
|
|
295
|
+
maxAge: 60 * 60 * 24 * 30 }) // 30 days
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setSignedOutUserCookie(
|
|
299
|
+
{ req, res }: ReqRes,
|
|
300
|
+
id: string | null | undefined) {
|
|
301
|
+
|
|
302
|
+
// Set signedOutUserUq in cookie
|
|
303
|
+
setCookie(
|
|
304
|
+
this.signedOutCookieName,
|
|
305
|
+
id,
|
|
306
|
+
{ req,
|
|
307
|
+
res,
|
|
308
|
+
maxAge: 60 * 60 * 24 * 30 }) // 30 days
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class StringUtilsService {
|
|
2
|
+
|
|
3
|
+
toNaturalCase(input: string) {
|
|
4
|
+
|
|
5
|
+
if (!input) return ''
|
|
6
|
+
|
|
7
|
+
// Step 1: Normalize input (handle snake_case, kebab-case, camelCase)
|
|
8
|
+
let words = input
|
|
9
|
+
// Insert space before capital letters (camelCase to space-separated)
|
|
10
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
11
|
+
// Replace underscores and hyphens with space
|
|
12
|
+
.replace(/[_\-]+/g, ' ')
|
|
13
|
+
// Split into words
|
|
14
|
+
.split(' ')
|
|
15
|
+
|
|
16
|
+
// Step 2: Capitalize each word, but keep acronyms (like TV, AI) in uppercase
|
|
17
|
+
return words
|
|
18
|
+
.map((word: string) => {
|
|
19
|
+
if (word.length <= 2 && word === word.toUpperCase()) {
|
|
20
|
+
// likely an acronym: TV, AI, ML
|
|
21
|
+
return word;
|
|
22
|
+
}
|
|
23
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
24
|
+
})
|
|
25
|
+
.join(' ')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getShortLocaleString(date: Date) {
|
|
29
|
+
|
|
30
|
+
const tz = date.toLocaleDateString('en', {
|
|
31
|
+
day: '2-digit',
|
|
32
|
+
timeZoneName: 'short'
|
|
33
|
+
}).slice(4)
|
|
34
|
+
|
|
35
|
+
return `${date.toLocaleString()} ${tz}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSnippet(
|
|
39
|
+
text: string,
|
|
40
|
+
maxChars: number) {
|
|
41
|
+
|
|
42
|
+
if (text.length < maxChars) {
|
|
43
|
+
return text
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return text.substring(0, maxChars) + '..'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// This function assumes that the list has properties id and parentId. There is
|
|
2
|
+
// an assumption that there's only one root node (parentId will be null).
|
|
3
|
+
export class TreeUtilsService {
|
|
4
|
+
|
|
5
|
+
// Helper function
|
|
6
|
+
addChildNodesWithChildrenPropertyName(
|
|
7
|
+
item: any,
|
|
8
|
+
parentIdMap: Map<string, any[]>) {
|
|
9
|
+
|
|
10
|
+
// Create node. Create a shallow copy of the object to prevent the "Object
|
|
11
|
+
// is not extensible" error on the next line.
|
|
12
|
+
var node = { ...item }
|
|
13
|
+
node.children = []
|
|
14
|
+
|
|
15
|
+
// Add child nodes
|
|
16
|
+
const childItems = parentIdMap.get(node.id)
|
|
17
|
+
|
|
18
|
+
// console.log(`. childItems: ${JSON.stringify(childItems)}`)
|
|
19
|
+
|
|
20
|
+
if (childItems !== undefined) {
|
|
21
|
+
for (const childItem of childItems) {
|
|
22
|
+
|
|
23
|
+
const childNode = this.addChildNodesWithChildrenPropertyName(
|
|
24
|
+
childItem,
|
|
25
|
+
parentIdMap)
|
|
26
|
+
|
|
27
|
+
node.children.push(childNode)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return node
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
listToTree(
|
|
35
|
+
list: any[],
|
|
36
|
+
childNodesPropertyName: string = 'children') {
|
|
37
|
+
|
|
38
|
+
// Convert to maps indexed by id and parentId, also get the root node
|
|
39
|
+
var node: any = null
|
|
40
|
+
var idMap = new Map<string, any>()
|
|
41
|
+
var parentIdMap = new Map<string, any[]>()
|
|
42
|
+
|
|
43
|
+
for (const item of list) {
|
|
44
|
+
idMap.set(item.id, item)
|
|
45
|
+
|
|
46
|
+
if (item.parentId === null) {
|
|
47
|
+
// Root node has parentId of null
|
|
48
|
+
node = item
|
|
49
|
+
} else {
|
|
50
|
+
// Populate parentIdMap except for root node which has parentId of null.
|
|
51
|
+
// Null can't be an index to a map anyway.
|
|
52
|
+
var parentItems = parentIdMap.get(item.parentId)
|
|
53
|
+
|
|
54
|
+
if (parentItems === undefined) {
|
|
55
|
+
parentItems = [item]
|
|
56
|
+
} else {
|
|
57
|
+
parentItems.push(item)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
parentIdMap.set(item.parentId, parentItems)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// console.log(`. parentIdMap: ${JSON.stringify(parentIdMap)}`)
|
|
65
|
+
|
|
66
|
+
// Copy the idMap to a new map that tracks which items have yet to be added
|
|
67
|
+
// to the tree. Remove the root node.
|
|
68
|
+
var itemsNotAdded = idMap
|
|
69
|
+
|
|
70
|
+
if (node === null) {
|
|
71
|
+
throw 'A root node wasn\'t identified'
|
|
72
|
+
} else {
|
|
73
|
+
itemsNotAdded.delete(node.id)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create the tree
|
|
77
|
+
if (childNodesPropertyName === 'children') {
|
|
78
|
+
node = this.addChildNodesWithChildrenPropertyName(
|
|
79
|
+
node,
|
|
80
|
+
parentIdMap)
|
|
81
|
+
} else {
|
|
82
|
+
throw `Unhandled childNodesPropertyName: ${childNodesPropertyName}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return root node
|
|
86
|
+
return node
|
|
87
|
+
}
|
|
88
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"outDir": "dist", // Compiled files go here
|
|
8
|
+
"declaration": true, // Generate .d.ts types for consumers
|
|
9
|
+
"strict": true, // Strict type checking
|
|
10
|
+
"esModuleInterop": true, // Allow default imports from CommonJS
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./src/*"]
|
|
15
|
+
},
|
|
16
|
+
"skipLibCheck": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"next-env.d.ts",
|
|
20
|
+
"**/*.ts",
|
|
21
|
+
"**/*.tsx",
|
|
22
|
+
".next/types/**/*.ts",
|
|
23
|
+
".next/dev/types/**/*.ts",
|
|
24
|
+
"**/*.mts"
|
|
25
|
+
],
|
|
26
|
+
}
|