musora-content-services 1.0.148 → 1.0.152

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.
@@ -9,7 +9,15 @@ const {globalConfig} = require('./config');
9
9
  *
10
10
  * @type {string[]}
11
11
  */
12
- const excludeFromGeneratedIndex = ['fetchUserLikes', 'postContentLiked', 'postContentUnliked'];
12
+ const excludeFromGeneratedIndex = [
13
+ 'fetchUserLikes',
14
+ 'postContentLiked',
15
+ 'postContentUnliked',
16
+ 'postRecordWatchSession',
17
+ 'postContentStarted',
18
+ 'postContentCompleted',
19
+ 'postContentReset'
20
+ ];
13
21
 
14
22
 
15
23
  /**
@@ -238,14 +246,25 @@ async function fetchDataHandler(url, dataVersion, method = "get") {
238
246
  return fetchHandler(url, method, dataVersion);
239
247
  }
240
248
 
241
- export async function fetchHandler(url, method = "get", dataVersion = null) {
249
+ async function postDataHandler(url, data) {
250
+ return fetchHandler(url, 'post', data);
251
+ }
252
+
253
+ export async function fetchHandler(url, method = "get", dataVersion = null, body = null) {
242
254
  let headers = {
243
255
  'Content-Type': 'application/json',
244
256
  'X-CSRF-TOKEN': globalConfig.railcontentConfig.token,
245
257
  };
246
258
  if (dataVersion) headers['Data-Version'] = dataVersion;
259
+ const options = {
260
+ method,
261
+ headers,
262
+ };
263
+ if (body) {
264
+ options.body = JSON.stringify(body);
265
+ }
247
266
  try {
248
- const response = await fetchAbsolute(url, {method, headers});
267
+ const response = await fetchAbsolute(url, options);
249
268
  const result = await response.json();
250
269
  if (result) {
251
270
  return result;
@@ -265,64 +284,559 @@ export async function fetchUserLikes(currentVersion) {
265
284
 
266
285
  export async function postContentLiked(contentId) {
267
286
  let url = `/content/user/likes/like/${contentId}`;
268
- return await fetchHandler(url, "post");
287
+ return await postDataHandler(url);
269
288
  }
270
289
 
271
290
  export async function postContentUnliked(contentId) {
272
291
  let url = `/content/user/likes/unlike/${contentId}`;
273
- return await fetchHandler(url, "post");
292
+ return await postDataHandler(url);
274
293
  }
275
294
 
295
+ export async function fetchContentProgress(currentVersion) {
296
+ let url = `/content/user/progress/all`;
297
+ return fetchDataHandler(url, currentVersion);
298
+ }
299
+
300
+ export async function postRecordWatchSession({
301
+ mediaId,
302
+ mediaType,
303
+ mediaCategory,
304
+ watchPosition,
305
+ totalDuration,
306
+ sessionToken,
307
+ brand,
308
+ contentId = null
309
+ }) {
310
+ let url = `/railtracker/media-playback-session`;
311
+ return postDataHandler(url, {
312
+ mediaId,
313
+ mediaType,
314
+ mediaCategory,
315
+ watchPosition,
316
+ totalDuration,
317
+ sessionToken,
318
+ brand,
319
+ contentId
320
+ });
321
+ }
322
+
323
+ /**
324
+ * Fetch enrolled user data for a given challenge. Intended to be used in the enrolled modal
325
+ *
326
+ * @param contentId - railcontent id of the challenge
327
+ * @returns {Promise<any|null>}
328
+ */
276
329
  export async function fetchChallengeMetadata(contentId) {
277
330
  let url = `/challenges/${contentId}`;
278
331
  return await fetchHandler(url, 'get');
279
332
  }
280
333
 
334
+ /**
335
+ * Fetch lesson, user, and challenge data for a given lesson
336
+ *
337
+ * @param contentId - railcontent id of the lesson
338
+ * @returns {Promise<any|null>}
339
+ */
281
340
  export async function fetchChallengeLessonData(contentId) {
282
341
  let url = `/challenges/lessons/${contentId}`;
283
342
  return await fetchHandler(url, 'get');
284
343
  }
285
344
 
345
+ /**
346
+ * Fetch challenge, lesson, and user metadata for a given challenge
347
+ *
348
+ * @param contentId - railcontent id of the challenge
349
+ * @returns {Promise<any|null>}
350
+ */
286
351
  export async function fetchUserChallengeProgress(contentId) {
287
352
  let url = `/challenges/user_data/${contentId}`;
288
353
  return await fetchHandler(url, 'get');
289
354
  }
290
355
 
356
+ /**
357
+ * Fetch the user's best award for this challenge
358
+ *
359
+ * @param contentId - railcontent id of the challenge
360
+ * @returns {Promise<any|null>} - streamed PDF
361
+ */
291
362
  export async function fetchUserAward(contentId) {
292
363
  let url = `/challenges/download_award/${contentId}`;
293
364
  return await fetchHandler(url, 'get');
294
365
  }
295
366
 
367
+ /**
368
+ * Get challange duration, user progress, and status for the list of challenges
369
+ * Intended to be used on the index page for challenges
370
+ *
371
+ * @param {array} contentIds - arary of railcontent ids of the challenges
372
+ * @returns {Promise<any|null>}
373
+ */
374
+ export async function fetchChallengeIndexMetadata(contentIds) {
375
+ let idsString = contentIds.toString();
376
+ let url = `/challenges/user_progress_for_index_page/get?content_ids=${idsString}`;
377
+ return await fetchHandler(url, 'get');
378
+ }
379
+
380
+ /**
381
+ * Enroll a user in a challenge and set the start date of the challenge to the provided day.
382
+ * Clears any existing progress data for this challenge
383
+ *
384
+ * @param {int|string} contentId - railcontent id of the challenge
385
+ * @param {string} startDate - prefered format YYYYMMDD, but any Carbon parsable string will do.
386
+ * @returns {Promise<any|null>}
387
+ */
296
388
  export async function postChallengesSetStartDate(contentId, startDate) {
297
389
  let url = `/challenges/set_start_date/${contentId}?start_date=${startDate}`;
298
390
  return await fetchHandler(url, 'post');
299
391
  }
300
392
 
393
+ /**
394
+ * Enroll the user in the provided challenge and set to unlocked
395
+ * Clears any current progress data for this challenge
396
+ *
397
+ * @param {int|string} contentId - railcontent id of the challenge
398
+ * @returns {Promise<any|null>}
399
+ */
301
400
  export async function postChallengesUnlock(contentId) {
302
401
  let url = `/challenges/unlock/${contentId}`;
303
402
  return await fetchHandler(url, 'post');
304
403
  }
305
404
 
405
+ /**
406
+ * Enroll the user in the given challenge on the challenge published_on date
407
+ * Clears any current progress data for this challenge
408
+ *
409
+ * @param {int|string} contentId - railcontent id of the challenge
410
+ * @returns {Promise<any|null>}
411
+ */
306
412
  export async function postChallengesEnroll(contentId) {
307
413
  let url = `/challenges/enroll/${contentId}`;
308
414
  return await fetchHandler(url, 'post');
309
415
  }
310
416
 
417
+ /**
418
+ * Remove the user from the provided challenge
419
+ * Clears any current progress data for this challenge
420
+ *
421
+ * @param {int|string} contentId - railcontent id of the challenge
422
+ * @returns {Promise<any|null>}
423
+ */
311
424
  export async function postChallengesLeave(contentId) {
312
425
  let url = `/challenges/leave/${contentId}`;
313
426
  return await fetchHandler(url, 'post');
314
427
  }
315
428
 
429
+ /**
430
+ * Enable enrollment notifications for the provided challenge
431
+ *
432
+ * @param {int|string} contentId - railcontent id of the challenge
433
+ * @returns {Promise<any|null>}
434
+ */
316
435
  export async function postChallengesEnrollmentNotification(contentId) {
317
436
  let url = `/challenges/notifications/enrollment_open/${contentId}`;
318
437
  return await fetchHandler(url, 'post');
319
438
  }
320
439
 
440
+ /**
441
+ * Enable community notifications for the provided challenge
442
+ *
443
+ * @param {int|string} contentId - railcontent id of the challenge
444
+ * @returns {Promise<any|null>}
445
+ */
321
446
  export async function postChallengesCommunityNotification(contentId) {
322
447
  let url = `/challenges/notifications/community_reminders/${contentId}`;
323
448
  return await fetchHandler(url, 'post');
324
449
  }
325
450
 
451
+ /**
452
+ * Complete the challenge lesson and update challenge progress
453
+ *
454
+ * @param {int|string} contentId - railcontent id of the challenge
455
+ * @returns {Promise<any|null>} - Modal data to display
456
+ */
457
+ export async function postCompleteLesson(contentId) {
458
+ let url = `/challenges/complete_lesson/${contentId}`;
459
+ return await fetchHandler(url, 'post');
460
+ }
461
+
462
+ /**
463
+ * Fetches user playlists for a specific brand.
464
+ *
465
+ * Allows optional pagination, sorting, and search parameters to control the result set.
466
+ *
467
+ * @param {string} brand - The brand identifier for which playlists are being fetched.
468
+ * @param {number} [params.limit=10] - The maximum number of playlists to return per page (default is 10).
469
+ * @param {number} [params.page=1] - The page number for pagination.
470
+ * @param {string} [params.sort='-created_at'] - The sorting order for the playlists (default is by created_at in descending order).
471
+ * @param {string} [params.searchTerm] - A search term to filter playlists by name.
472
+ *
473
+ * @returns {Promise<Object|null>} - A promise that resolves to the response from the API, containing the user playlists data.
474
+ *
475
+ * @example
476
+ * fetchUserPlaylists('drumeo', { page: 1, sort: 'name', searchTerm: 'rock' })
477
+ * .then(playlists => console.log(playlists))
478
+ * .catch(error => console.error(error));
479
+ */
480
+ export async function fetchUserPlaylists(brand, {page, limit, sort, searchTerm} = {}) {
481
+ let url;
482
+ const limitString = limit ? `&limit=${limit}` : '';
483
+ const pageString = page ? `&page=${page}` : '';
484
+ const sortString = sort ? `&sort=${sort}`:'';
485
+ const searchFilter = searchTerm ? `&term=${searchTerm}`: '';
486
+ url = `/playlists/all?brand=${brand}${limitString}${pageString}${sortString}${searchFilter}`;
487
+ return await fetchHandler(url);
488
+ }
489
+
490
+ /**
491
+ * Duplicates an existing playlist by sending a POST request to the API.
492
+ *
493
+ * This function calls the `/playlists/duplicate` endpoint, where the server replicates the specified playlist,
494
+ * including its items. Optionally, new `name`, `description`, `category`, or `thumbnail_url` parameters can be provided
495
+ * to customize the duplicated playlist. If a new name is not provided, the server appends " (Duplicate)" to the original name.
496
+ *
497
+ * @param {string|number} playlistId - The unique identifier of the playlist to be duplicated.
498
+ * @param {Object} [playlistData] - Optional data to customize the duplicated playlist.
499
+ * @param {string} [playlistData.name] - New name for the duplicated playlist (default is original name + " (Duplicate)").
500
+ * @param {string} [playlistData.description] - New description for the duplicated playlist (defaults to original description).
501
+ * @param {string} [playlistData.category] - New category for the duplicated playlist (defaults to original category).
502
+ * @param {string} [playlistData.thumbnail_url] - New URL for the duplicated playlist thumbnail (defaults to original thumbnail).
503
+ *
504
+ * @returns {Promise<Object>} - A promise that resolves to the duplicated playlist data, or rejects with an error if the duplication fails.
505
+ *
506
+ * The duplicated playlist contains:
507
+ * - `name` (string): Name of the new playlist.
508
+ * - `description` (string|null): Description of the duplicated playlist.
509
+ * - `category` (string|null): Category of the duplicated playlist.
510
+ * - `thumbnail_url` (string|null): URL of the playlist thumbnail.
511
+ * - `items` (Array): A list of items (e.g., songs, tracks) copied from the original playlist.
512
+ *
513
+ * @example
514
+ * duplicatePlaylist(12345, { name: "My New Playlist" })
515
+ * .then(duplicatedPlaylist => console.log(duplicatedPlaylist))
516
+ * .catch(error => console.error('Error duplicating playlist:', error));
517
+ */
518
+ export async function duplicatePlaylist(playlistId, playlistData) {
519
+ let url = `/playlists/duplicate/${playlistId}`;
520
+ return await fetchHandler(url, "post",null, playlistData);
521
+ }
522
+
523
+ /**
524
+ * Deletes a user’s playlist along with all associated items by sending a DELETE request to the API.
525
+ *
526
+ * This function calls the `/playlists/playlist` endpoint, where the server verifies the user’s ownership of the specified playlist.
527
+ * If the user is authorized, it deletes both the playlist and its associated items.
528
+ *
529
+ * @param {string|number} playlistId - The unique identifier of the playlist to be deleted.
530
+ *
531
+ * @returns {Promise<Object>} - A promise that resolves to an object containing:
532
+ * - `success` (boolean): Indicates if the deletion was successful (`true` for success).
533
+ * - `message` (string): Success confirmation message (e.g., "Playlist and associated items deleted successfully").
534
+ *
535
+ * If the user is unauthorized or the playlist does not exist, the promise rejects with an error.
536
+ *
537
+ * @example
538
+ * deletePlaylist(12345)
539
+ * .then(response => {
540
+ * if (response.success) {
541
+ * console.log(response.message);
542
+ * }
543
+ * })
544
+ * .catch(error => console.error('Error deleting playlist:', error));
545
+ */
546
+ export async function deletePlaylist(playlistId) {
547
+ let url = `/playlists/playlist/${playlistId}`;
548
+ return await fetchHandler(url, "delete");
549
+ }
550
+
551
+ /**
552
+ * Updates a user’s playlist by sending a PUT request with updated data to the API.
553
+ *
554
+ * This function calls the `/playlists/playlist/{playlistId}` endpoint, where the server validates the incoming data
555
+ * and verifies that the authenticated user is the playlist owner. If authorized, it updates the playlist details with the provided data.
556
+ *
557
+ * @param {string|number} playlistId - The unique identifier of the playlist to be updated.
558
+ * @param {Object} updatedData - An object containing the playlist data to update. The possible fields include:
559
+ * - `name` (string): The new name of the playlist (max 255 characters).
560
+ * - `description` (string): A new description for the playlist (max 1000 characters).
561
+ * - `category` (string): The updated category of the playlist (max 255 characters).
562
+ * - `private` (boolean): Whether the playlist is private.
563
+ * - `thumbnail_url` (string): The URL of the playlist thumbnail.
564
+ *
565
+ * @returns {Promise<Object>} - A promise that resolves to an object containing:
566
+ * - `success` (boolean): Indicates if the update was successful (`true` for success).
567
+ * - `message` (string): Success confirmation message if the update is successful.
568
+ * - Other fields containing the updated playlist data.
569
+ *
570
+ * If the user is unauthorized or the data validation fails, the promise rejects with an error.
571
+ *
572
+ * @example
573
+ * updatePlaylist(12345, { name: "My New Playlist Name", description: "Updated description" })
574
+ * .then(response => {
575
+ * if (response.success) {
576
+ * console.log(response.message);
577
+ * }
578
+ * })
579
+ * .catch(error => console.error('Error updating playlist:', error));
580
+ */
581
+ export async function updatePlaylist(playlistId, updatedData) {
582
+ const url = `/playlists/playlist/${playlistId}`;
583
+ return await fetchHandler(url, "PUT", null, updatedData);
584
+ }
585
+
586
+ /**
587
+ * Creates a new user playlist by sending a POST request with playlist data to the API.
588
+ *
589
+ * This function calls the `/playlists/playlist` endpoint, where the server validates the incoming data and associates
590
+ * the new playlist with the authenticated user. The `name` field is required, while other fields are optional.
591
+ *
592
+ * @param {Object} playlistData - An object containing data to create the playlist. The fields include:
593
+ * - `name` (string): The name of the new playlist (required, max 255 characters).
594
+ * - `description` (string): A description of the playlist (optional, max 1000 characters).
595
+ * - `category` (string): The category of the playlist.
596
+ * - `thumbnail_url` (string): The URL of the playlist thumbnail (optional, must be a valid URL).
597
+ * - `private` (boolean): Whether the playlist is private (optional, defaults to true).
598
+ * - `brand` (string): Brand identifier for the playlist.
599
+ *
600
+ * @returns {Promise<Object>} - A promise that resolves to the created playlist data if successful, or an error response if validation fails.
601
+ *
602
+ * The server response includes:
603
+ * - `message`: Success message indicating playlist creation (e.g., "Playlist created successfully").
604
+ * - `playlist`: The data for the created playlist, including the `user_id` of the authenticated user.
605
+ *
606
+ * @example
607
+ * createPlaylist({ name: "My Playlist", description: "A cool playlist", private: true })
608
+ * .then(response => console.log(response.message))
609
+ * .catch(error => console.error('Error creating playlist:', error));
610
+ */
611
+ export async function createPlaylist(playlistData) {
612
+ const url = `/playlists/playlist`;
613
+ return await fetchHandler(url, "POST", null, playlistData);
614
+ }
615
+
616
+ /**
617
+ * Sends a request to "like" a playlist on behalf of the authenticated user.
618
+ *
619
+ * This function calls the `/playlists/playlist/like` endpoint, where the server validates the `playlist_id` and checks
620
+ * if the authenticated user has already liked the playlist. If not, it creates a new "like" entry associated with the playlist.
621
+ *
622
+ * @param {string|number} playlistId - The unique identifier of the playlist to be liked.
623
+ *
624
+ * @returns {Promise<Object>} - A promise that resolves with the response from the API. The response contains:
625
+ * - `success` (boolean): Indicates if the like action was successful (`true` for success).
626
+ * - `message` (string): A success message if the playlist is liked successfully, or a notification if it was already liked.
627
+ * - `like` (Object|null): Details of the created "like" entry if the playlist is newly liked, or null if it was already liked.
628
+ *
629
+ * The `like` object in the response includes:
630
+ * - `playlist_id`: The ID of the liked playlist.
631
+ * - `user_id`: The ID of the user who liked the playlist.
632
+ * - `brand`: The brand associated with the like.
633
+ * - `created_at`: Timestamp of when the like was created.
634
+ *
635
+ * @example
636
+ * likePlaylist(12345)
637
+ * .then(response => {
638
+ * if (response.success) {
639
+ * console.log(response.message);
640
+ * }
641
+ * })
642
+ * .catch(error => console.error('Error liking playlist:', error));
643
+ */
644
+ export async function likePlaylist(playlistId) {
645
+ const url = `/playlists/like`;
646
+ const payload = { playlist_id: playlistId };
647
+ return await fetchHandler(url, "PUT", null, payload);
648
+ }
649
+
650
+ /**
651
+ * Removes a "like" from a playlist for the authenticated user.
652
+ *
653
+ * This function sends a DELETE request to the `/playlists/like` endpoint, where the server validates the `playlist_id`
654
+ * and checks if a like by the authenticated user already exists for the specified playlist. If so, it deletes the like.
655
+ *
656
+ * @param {string|number} playlistId - The unique identifier of the playlist whose like is to be removed.
657
+ *
658
+ * @returns {Promise<Object>} - A promise that resolves with the response from the API. The response contains:
659
+ * - `success` (boolean): Indicates if the removal was successful (`true` for success).
660
+ * - `message` (string): A success message if the playlist like is removed successfully or a notification if the playlist was not previously liked.
661
+ *
662
+ * @example
663
+ * deletePlaylistLike(12345)
664
+ * .then(response => {
665
+ * if (response.success) {
666
+ * console.log(response.message);
667
+ * }
668
+ * })
669
+ * .catch(error => console.error('Error removing playlist like:', error));
670
+ */
671
+ export async function deletePlaylistLike(playlistId) {
672
+ const url = `/playlists/like`;
673
+ const payload = { playlist_id: playlistId };
674
+ return await fetchHandler(url, "DELETE", null, payload);
675
+ }
676
+
677
+ /**
678
+ * Retrieves details of a specific playlist by its ID.
679
+ *
680
+ * This function sends a GET request to the `/playlists/playlist` endpoint with a specified playlist ID.
681
+ * The server validates the user's access to the playlist and returns playlist details if the user is authorized.
682
+ *
683
+ * @param {string|number} playlistId - The unique identifier of the playlist to retrieve.
684
+ *
685
+ * @returns {Promise<Object>} - A promise that resolves to the response from the API, containing:
686
+ * - `data` (Object): The playlist details, or an error message if access is denied or the playlist is not found.
687
+ *
688
+ * @example
689
+ * fetchPlaylist(12345)
690
+ * .then(response => console.log(response.data))
691
+ * .catch(error => console.error('Error fetching playlist:', error));
692
+ */
693
+ export async function fetchPlaylist(playlistId) {
694
+ const url = `/playlists/playlist/${playlistId}`;
695
+ return await fetchHandler(url, "GET");
696
+ }
697
+
698
+ /**
699
+ * Retrieves items within a specified playlist by playlist ID.
700
+ *
701
+ * This function sends a GET request to the `/playlists/playlist-lessons` endpoint to fetch items in the given playlist.
702
+ * The server combines data from the playlist and additional metadata from Sanity to enhance item details.
703
+ *
704
+ * @param {string|number} playlistId - The unique identifier of the playlist whose items are to be fetched.
705
+ *
706
+ * @returns {Promise<Array<Object>>} - A promise that resolves to an array of playlist items
707
+ *
708
+ * @example
709
+ * fetchPlaylistItems(12345)
710
+ * .then(items => console.log(items))
711
+ * .catch(error => console.error('Error fetching playlist items:', error));
712
+ */
713
+ export async function fetchPlaylistItems(playlistId) {
714
+ const url = `/playlists/playlist-lessons?playlist_id=${playlistId}`;
715
+ return await fetchHandler(url, "GET");
716
+ }
717
+
718
+ /**
719
+ * Updates a playlist item with the provided data.
720
+ *
721
+ * @param {Object} updatedData - The data to update the playlist item with.
722
+ * @param {number} updatedData.user_playlist_item_id - The ID of the playlist item to update.
723
+ * @param {number} [updatedData.start_second] - (Optional) The start time in seconds for the item.
724
+ * @param {number} [updatedData.end_second] - (Optional) The end time in seconds for the item.
725
+ * @param {string} [updatedData.playlist_item_name] - (Optional) The new name for the playlist item.
726
+ * @param {number} [updatedData.position] - (Optional) The new position for the playlist item within the playlist.
727
+ * @returns {Promise<Object|null>} - A promise that resolves to an object containing:
728
+ * - `success` (boolean): Indicates if the update was successful (`true` for success).
729
+ * - `data` (Object): The updated playlist item data.
730
+ *
731
+ * Resolves to `null` if the request fails.
732
+ * @throws {Error} - Throws an error if the request fails.
733
+ *
734
+ * @example
735
+ * const updatedData = {
736
+ * user_playlist_item_id: 123,
737
+ * start_second: 30,
738
+ * end_second: 120,
739
+ * playlist_item_name: "Updated Playlist Item Name",
740
+ * position: 2
741
+ * };
742
+ *
743
+ * updatePlaylistItem(updatedData)
744
+ * .then(response => {
745
+ * if (response.success) {
746
+ * console.log("Playlist item updated successfully:", response.data);
747
+ * }
748
+ * })
749
+ * .catch(error => {
750
+ * console.error("Error updating playlist item:", error);
751
+ * });
752
+ */
753
+ export async function updatePlaylistItem(updatedData) {
754
+ const url = `/playlists/item`;
755
+ return await fetchHandler(url, "POST", null, updatedData);
756
+ }
757
+
758
+ /**
759
+ * Deletes a playlist item and repositions other items in the playlist if necessary.
760
+ *
761
+ * @param {Object} payload - The data required to delete the playlist item.
762
+ * @param {number} payload.user_playlist_item_id - The ID of the playlist item to delete.
763
+ * @returns {Promise<Object|null>} - A promise that resolves to an object containing:
764
+ * - `success` (boolean): Indicates if the deletion was successful (`true` for success).
765
+ * - `message` (string): A success message if the item is deleted successfully.
766
+ * - `error` (string): An error message if the deletion fails.
767
+ *
768
+ * Resolves to `null` if the request fails.
769
+ * @throws {Error} - Throws an error if the request fails.
770
+ *
771
+ * @example
772
+ * const payload = {
773
+ * user_playlist_item_id: 123
774
+ * };
775
+ *
776
+ * deletePlaylistItem(payload)
777
+ * .then(response => {
778
+ * if (response.success) {
779
+ * console.log("Playlist item deleted successfully:", response.message);
780
+ * } else {
781
+ * console.error("Error:", response.error);
782
+ * }
783
+ * })
784
+ * .catch(error => {
785
+ * console.error("Error deleting playlist item:", error);
786
+ * });
787
+ */
788
+ export async function deletePlaylistItem(payload) {
789
+ const url = `/playlists/item`;
790
+ return await fetchHandler(url, "DELETE", null, payload);
791
+ }
792
+
793
+ /**
794
+ * Fetches detailed data for a specific playlist item, including associated Sanity and Assignment information if available.
795
+ *
796
+ * @param {Object} payload - The request payload containing necessary parameters.
797
+ * @param {number} payload.user_playlist_item_id - The unique ID of the playlist item to fetch.
798
+ * @returns {Promise<Object|null>} - A promise that resolves to an object with the fetched playlist item data, including:
799
+ * - `success` (boolean): Indicates if the data retrieval was successful (`true` on success).
800
+ * - `data` (Object): Contains the detailed playlist item data enriched with Sanity and Assignment details, if available.
801
+ *
802
+ * Resolves to `null` if the request fails.
803
+ * @throws {Error} - Throws an error if the request encounters issues during retrieval.
804
+ *
805
+ * @example
806
+ * const payload = { user_playlist_item_id: 123 };
807
+ *
808
+ * fetchPlaylistItem(payload)
809
+ * .then(response => {
810
+ * if (response?.success) {
811
+ * console.log("Fetched playlist item data:", response.data);
812
+ * } else {
813
+ * console.log("Failed to fetch playlist item data.");
814
+ * }
815
+ * })
816
+ * .catch(error => {
817
+ * console.error("Error fetching playlist item:", error);
818
+ * });
819
+ */
820
+ export async function fetchPlaylistItem(payload) {
821
+ const playlistItemId = payload.user_playlist_item_id;
822
+ const url = `/playlists/item/${playlistItemId}`;
823
+ return await fetchHandler(url);
824
+ }
825
+
826
+ export async function postContentStarted(contentId) {
827
+ let url = `/content/${contentId}/started`;
828
+ return postDataHandler(url);
829
+ }
830
+
831
+ export async function postContentCompleted(contentId) {
832
+ let url = `/content/${contentId}/completed`;
833
+ return postDataHandler(url);
834
+ }
835
+
836
+ export async function postContentReset(contentId) {
837
+ let url = `/content/${contentId}/reset`;
838
+ return postDataHandler(url);
839
+ }
326
840
 
327
841
 
328
842
  function fetchAbsolute(url, params) {
@@ -332,4 +846,4 @@ function fetchAbsolute(url, params) {
332
846
  }
333
847
  }
334
848
  return fetch(url, params);
335
- }
849
+ }
@@ -904,6 +904,9 @@ export async function fetchNextPreviousLesson(railcontentId) {
904
904
  */
905
905
  export async function fetchLessonContent(railContentId) {
906
906
  const filterParams = {};
907
+ // Format changes made to the `fields` object may also need to be reflected in Musora-web-platform SanityGateway.php $fields object
908
+ // Currently only for challenges and challenge lessons
909
+ // If you're unsure, message Adrian, or just add them.
907
910
  const fields = `title,
908
911
  published_on,
909
912
  "type":_type,
@@ -1359,6 +1362,77 @@ export async function fetchGenreLessons(brand, name, contentType, {
1359
1362
  return fetchSanity(query, true);
1360
1363
  }
1361
1364
 
1365
+ export async function fetchTopLevelParentId(railcontentId) {
1366
+ const query = `*[railcontent_id == ${railcontentId}]{
1367
+ railcontent_id,
1368
+ 'parents': *[^._id in child[]._ref && !(_id in path('drafts.**'))]{
1369
+ railcontent_id,
1370
+ 'parents': *[^._id in child[]._ref && !(_id in path('drafts.**'))]{
1371
+ railcontent_id,
1372
+ 'parents': *[^._id in child[]._ref && !(_id in path('drafts.**'))]{
1373
+ railcontent_id,
1374
+ 'parents': *[^._id in child[]._ref && !(_id in path('drafts.**'))]{
1375
+ railcontent_id,
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ }`;
1381
+ let response = await fetchSanity(query, false, {processNeedAccess: false});
1382
+ if (!response) return null;
1383
+ let currentLevel = response;
1384
+ for (let i = 0; i < 4; i++) {
1385
+ if (currentLevel['parents'].length > 0) {
1386
+ currentLevel = currentLevel['parents'][0];
1387
+ } else {
1388
+ return currentLevel['railcontent_id'];
1389
+ }
1390
+ }
1391
+ return null;
1392
+ }
1393
+
1394
+ export async function fetchHierarchy(railcontentId) {
1395
+ let topLevelId = await fetchTopLevelParentId(railcontentId);
1396
+ const query = `*[railcontent_id == ${topLevelId}]{
1397
+ railcontent_id,
1398
+ 'children': child[]->{
1399
+ railcontent_id,
1400
+ 'children': child[]->{
1401
+ railcontent_id,
1402
+ 'children': child[]->{
1403
+ railcontent_id,
1404
+ 'children': child[]->{
1405
+ railcontent_id,
1406
+ }
1407
+ }
1408
+ }
1409
+ }
1410
+ }`;
1411
+ let response = await fetchSanity(query, false, {processNeedAccess: false});
1412
+ if (!response) return null;
1413
+ let data = {
1414
+ parents: {},
1415
+ children: {}
1416
+ };
1417
+ populateHierarchyLookups(response, data, null);
1418
+ return data;
1419
+ }
1420
+
1421
+ function populateHierarchyLookups(currentLevel, data, parentId) {
1422
+ let contentId = currentLevel['railcontent_id'];
1423
+ let children = currentLevel['children'];
1424
+ data.parents[contentId] = parentId;
1425
+ if (children) {
1426
+ data.children[contentId] = children.map(child => child['railcontent_id']);
1427
+ for (let i = 0; i < children.length; i++) {
1428
+ populateHierarchyLookups(children[i], data, contentId);
1429
+ }
1430
+ } else {
1431
+ data.children[contentId] = [];
1432
+ }
1433
+ }
1434
+
1435
+
1362
1436
 
1363
1437
  /**
1364
1438
  *