-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathFeaturedItemsFormHelper.ts
More file actions
369 lines (308 loc) · 11 KB
/
FeaturedItemsFormHelper.ts
File metadata and controls
369 lines (308 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import {
FeaturedItem,
CustomFeaturedItem,
FeaturedItemType
} from '@/collection/domain/models/FeaturedItem'
import { CustomFeaturedItemField, DvObjectFeaturedItemField, FeaturedItemsFormData } from '../types'
import {
CustomFeaturedItemDTO,
FeaturedItemsDTO,
DvObjectFeaturedItemDTO
} from '@/collection/domain/useCases/DTOs/FeaturedItemsDTO'
import { QueryParamKey, Route } from '@/sections/Route.enum'
const BASENAME_URL = import.meta.env.BASE_URL ?? ''
export class FeaturedItemsFormHelper {
/**
* @description To define the default form featured items values
*/
static defineFormDefaultFeaturedItems(
existingFeaturedItems: FeaturedItem[]
): FeaturedItemsFormData['featuredItems'] {
if (!existingFeaturedItems.length) {
return [{ type: 'base' }]
}
return existingFeaturedItems.map((featuredItem) => {
if (featuredItem.type === FeaturedItemType.CUSTOM) {
const { id, content, imageFileUrl } = featuredItem
const customFeaturedItemFormFieldData: CustomFeaturedItemField = {
itemId: id,
type: FeaturedItemType.CUSTOM,
content,
image: imageFileUrl ? imageFileUrl : null
}
return customFeaturedItemFormFieldData
} else {
const { id, type, dvObjectIdentifier } = featuredItem
const dvObjectFeaturedItemFormFieldData: DvObjectFeaturedItemField = {
itemId: id,
type,
dvObjectIdentifier,
dvObjectUrl: this.transformDvObjectTypeAndIdentifierToSpaURL(type, dvObjectIdentifier)
}
return dvObjectFeaturedItemFormFieldData
}
})
}
/**
* @description This method is to transform current form data into "actual featured items" to show the current preview while the user is editing
*/
static transformCustomFormFieldsToFeaturedItems(
featuredItemsFieldValues: CustomFeaturedItemField[]
): CustomFeaturedItem[] {
return featuredItemsFieldValues.map((field, index) => {
const { content, image, itemId } = field
const currentFeaturedItem: CustomFeaturedItem = {
id: itemId ?? this.generateFakeNumberId(),
type: FeaturedItemType.CUSTOM,
displayOrder: index + 1,
content
}
if (image && image instanceof File) {
const objectUrl = URL.createObjectURL(image)
currentFeaturedItem.imageFileUrl = objectUrl
}
if (image && typeof image === 'string') {
currentFeaturedItem.imageFileUrl = image
}
return currentFeaturedItem
})
}
/**
* @description This method is to transform the form data into DTOs to send to the backend
*/
static defineFeaturedItemsDTO(
formFeaturedItems: FeaturedItemsFormData['featuredItems']
): FeaturedItemsDTO {
const itemsMapped: FeaturedItemsDTO = formFeaturedItems.map((item, index) => {
if (item.type === FeaturedItemType.CUSTOM) {
const { content, image, itemId } = item
const itemDTO: CustomFeaturedItemDTO = {
type: FeaturedItemType.CUSTOM,
content,
displayOrder: index,
keepFile: false
}
const isNewItem = itemId === undefined
const imageIsFile = image instanceof File
const imageRemainsOriginal = typeof image === 'string' && image !== ''
// Add itemId of existing item
if (itemId) {
itemDTO.id = itemId
}
// New item
if (isNewItem) {
itemDTO.file = imageIsFile ? image : undefined
itemDTO.keepFile = false
}
// Existing item
if (!isNewItem) {
itemDTO.file = imageIsFile ? image : undefined
itemDTO.keepFile = imageRemainsOriginal && !imageIsFile ? true : false
}
return itemDTO
} else {
const { itemId, type, dvObjectIdentifier } = item as DvObjectFeaturedItemField
const itemDTO: DvObjectFeaturedItemDTO = {
type: type as
| FeaturedItemType.COLLECTION
| FeaturedItemType.DATASET
| FeaturedItemType.FILE,
dvObjectIdentifier,
displayOrder: index
}
if (itemId) {
itemDTO.id = itemId
}
return itemDTO
}
})
return itemsMapped
}
/**
* @description This method parses a URL to extract the type and identifier of a Dataverse object (Collection, Dataset, or File). Works for URLs from the modern frontend and JSF versions of Dataverse.
* @param input - The URL or DOI string to parse.
* @returns An object containing the type and identifier of the Dataverse object, or null if the input is not a valid URL or does not match any known patterns.
*/
static extractDvObjectTypeAndIdentiferFromUrlValue(input: string): {
type: FeaturedItemType | null
identifier: string | null
} {
const value = input.trim()
// --- Direct DOI ---
if (this.isValidDOI(value)) {
return {
type: FeaturedItemType.DATASET,
identifier: value
}
}
try {
const url = new URL(value)
const path = url.pathname.replace(/^\/modern/, '') // remove "/modern" if exists
const searchParams = url.searchParams
// --- COLLECTION ---
if (path.startsWith(`${Route.COLLECTIONS_BASE}/`) || path.startsWith('/dataverse/')) {
const alias = path.split('/')[2]
return {
type: FeaturedItemType.COLLECTION,
identifier: alias
}
}
// --- DATASET ---
if (path === Route.DATASETS || path === '/dataset.xhtml') {
const pid = searchParams.get('persistentId')
if (pid) {
return {
type: FeaturedItemType.DATASET,
identifier: pid
}
}
}
// --- FILE ---
if (path === Route.FILES || path === '/file.xhtml') {
const fileId = searchParams.get('id') || searchParams.get('fileId')
if (fileId) {
return {
type: FeaturedItemType.FILE,
identifier: fileId
}
}
}
} catch (e) {
// Not a valid constructed URL
return {
type: null,
identifier: null
}
}
return {
type: null,
identifier: null
}
}
/**
* @description This method transforms the type and identifier of a Dataverse object into a URL for the Modern version of Dataverse.
* @param type - The type of the Dataverse object (Collection, Dataset, or File).
* @param identifier - The identifier of the Dataverse object (e.g., alias for Collection, persistent ID for Dataset, or file ID for File).
* @returns A string representing the URL for the specified Dataverse object.
*/
static transformDvObjectTypeAndIdentifierToSpaURL = (
type: FeaturedItemType.COLLECTION | FeaturedItemType.DATASET | FeaturedItemType.FILE,
identifier: string
): string => {
switch (type) {
case FeaturedItemType.COLLECTION:
return `${window.location.origin}${BASENAME_URL}${Route.COLLECTIONS_BASE}/${identifier}`
case FeaturedItemType.DATASET:
return `${window.location.origin}${BASENAME_URL}${Route.DATASETS}?${QueryParamKey.PERSISTENT_ID}=${identifier}`
case FeaturedItemType.FILE:
return `${window.location.origin}${BASENAME_URL}${Route.FILES}?${QueryParamKey.FILE_ID}=${identifier}`
default:
return '#' // Fallback URL if type is unknown
}
}
static isValidDvObjectUrl(input: string): boolean {
const trimmed = input.trim()
if (!/^https?:\/\//i.test(trimmed)) {
return false
}
try {
const url = new URL(trimmed)
const path = url.pathname.replace(/^\/modern/, '') // remove "/modern" if exists
const searchParams = url.searchParams
// --- COLLECTION ---
if (path.startsWith(`${Route.COLLECTIONS_BASE}/`) || path.startsWith('/dataverse/')) {
const alias = path.split('/')[2]
return !!alias
}
// --- DATASET ---
if (path === Route.DATASETS || path === '/dataset.xhtml') {
const pid = searchParams.get('persistentId')
return !!pid
}
// --- FILE ---
if (path === Route.FILES || path === '/file.xhtml') {
const fileId = searchParams.get('id') || searchParams.get('fileId')
return !!fileId
}
return false
} catch {
return false
}
}
/**
* @description This method checks if a given string is a valid DOI (Digital Object Identifier). https://support.datacite.org/docs/doi-basics
* @param value - The string to check.
* @returns A boolean indicating whether the string is a valid DOI.
*/
static isValidDOI(value: string): boolean {
const doiRegex = /^https?:\/\/doi\.org\/10\.\d{4,9}\/[-._;()/:A-Z0-9]+$/i
return doiRegex.test(value)
}
static formatBytes(bytes: number, decimals = 2): string {
if (!+bytes) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
static generateFakeNumberId(): number {
return Date.now() + Math.floor(Math.random() * 1000)
}
/**
* @description This method checks if the image has a recommended aspect ratio (width is 30% greater than height).
*/
static hasRecommendedAspectRatio(file: File): Promise<{
hasRecommendedAspectRatio: boolean
aspectRatioStringValue: string
}> {
return new Promise((resolve) => {
const img = new Image()
img.src = URL.createObjectURL(file)
img.onload = () => {
const { width, height } = img
const aspectRatio = width / height
const minRatio = 1.3 // means the width is 30% greater than the height
URL.revokeObjectURL(img.src)
const aspectRatioStringValue = FeaturedItemsFormHelper.getAspectRatioString(width, height)
resolve({
hasRecommendedAspectRatio: aspectRatio >= minRatio,
aspectRatioStringValue: aspectRatioStringValue
})
}
img.onerror = () =>
resolve({
hasRecommendedAspectRatio: false,
aspectRatioStringValue: '0:0'
})
})
}
/**
* @description This method calculates the aspect ratio of an image and returns it as a string in the format "width:height".
* @example
* getAspectRatioString(1920, 1080) // returns "16:9"
* getAspectRatioString(800, 600) // returns "4:3"
*/
static getAspectRatioString = (width: number, height: number): string => {
const maxVal = 16
const gcd = (a: number, b: number): number => {
while (b !== 0) [a, b] = [b, a % b]
return a
}
let w = width
let h = height
const divisor = gcd(w, h)
w = w / divisor
h = h / divisor
// If the width or height is greater than the maxVal, scale it down
if (w > maxVal || h > maxVal) {
const scale = maxVal / Math.max(w, h)
w = Math.round(w * scale)
h = Math.round(h * scale)
const reduced = gcd(w, h)
w = w / reduced
h = h / reduced
}
return `${w}:${h}`
}
}