musora-content-services 2.3.26 → 2.5.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.
Files changed (126) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/node.js.yml +0 -0
  5. package/.prettierignore +0 -0
  6. package/.prettierrc +0 -0
  7. package/.yarnrc.yml +1 -0
  8. package/CHANGELOG.md +19 -0
  9. package/README.md +0 -0
  10. package/babel.config.cjs +0 -0
  11. package/docs/Content-Organization.html +0 -0
  12. package/docs/ContentOrganization.html +2 -2
  13. package/docs/Gamification.html +2 -2
  14. package/docs/UserManagement.html +0 -0
  15. package/docs/UserManagementSystem.html +26 -2
  16. package/docs/api_types.js.html +2 -2
  17. package/docs/config.js.html +2 -2
  18. package/docs/content-org_content-org.js.html +2 -2
  19. package/docs/content-org_playlists-types.js.html +6 -6
  20. package/docs/content-org_playlists.js.html +220 -18
  21. package/docs/content.js.html +26 -11
  22. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  24. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  25. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  26. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  27. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  28. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  29. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  34. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  35. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  36. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  39. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  40. package/docs/gamification_awards.js.html +2 -2
  41. package/docs/gamification_gamification.js.html +2 -2
  42. package/docs/gamification_types.js.html +2 -2
  43. package/docs/global.html +14 -23
  44. package/docs/global.html#User +0 -0
  45. package/docs/index.html +2 -2
  46. package/docs/module-Awards.html +2 -2
  47. package/docs/module-Config.html +2 -2
  48. package/docs/module-Content-Services-V2.html +10 -9
  49. package/docs/module-Content-Services.html +763 -0
  50. package/docs/module-Interests.html +1066 -0
  51. package/docs/module-Permissions.html +2 -2
  52. package/docs/module-Playlists.html +1738 -157
  53. package/docs/module-Railcontent-Services.html +823 -3862
  54. package/docs/module-Sanity-Services.html +33 -33
  55. package/docs/module-Session-Management.html +0 -0
  56. package/docs/module-Sessions.html +2 -2
  57. package/docs/module-User-Activity.html +405 -32
  58. package/docs/module-User-Management.html +0 -0
  59. package/docs/module-User-Permissions.html +0 -0
  60. package/docs/module-UserManagement.html +2 -2
  61. package/docs/railcontent.js.html +65 -464
  62. package/docs/sanity.js.html +7 -5
  63. package/docs/scripts/collapse.js +0 -0
  64. package/docs/scripts/commonNav.js +0 -0
  65. package/docs/scripts/linenumber.js +0 -0
  66. package/docs/scripts/nav.js +0 -0
  67. package/docs/scripts/polyfill.js +0 -0
  68. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
  69. package/docs/scripts/prettify/lang-css.js +0 -0
  70. package/docs/scripts/prettify/prettify.js +0 -0
  71. package/docs/scripts/search.js +0 -0
  72. package/docs/styles/jsdoc.css +0 -0
  73. package/docs/styles/prettify.css +0 -0
  74. package/docs/types.js.html +0 -0
  75. package/docs/userActivity.js.html +177 -45
  76. package/docs/user_interests.js.html +150 -0
  77. package/docs/user_management.js.html +2 -2
  78. package/docs/user_permissions.js.html +2 -2
  79. package/docs/user_sessions.js.html +2 -2
  80. package/docs/user_types.js.html +2 -2
  81. package/docs/user_user-management-system.js.html +3 -2
  82. package/docs/user_user-management.js.html +0 -0
  83. package/jest.config.js +0 -0
  84. package/jsdoc.json +0 -0
  85. package/link_mcs.sh +0 -0
  86. package/package.json +1 -1
  87. package/src/contentMetaData.js +29 -5
  88. package/src/contentTypeConfig.js +3 -2
  89. package/src/index.d.ts +17 -0
  90. package/src/index.js +17 -0
  91. package/src/infrastructure/http/HttpClient.ts +120 -0
  92. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +45 -0
  93. package/src/infrastructure/http/index.ts +13 -0
  94. package/src/infrastructure/http/interfaces/HeaderProvider.ts +3 -0
  95. package/src/infrastructure/http/interfaces/HttpError.ts +7 -0
  96. package/src/infrastructure/http/interfaces/NetworkError.ts +6 -0
  97. package/src/infrastructure/http/interfaces/RequestExecutor.ts +5 -0
  98. package/src/infrastructure/http/interfaces/RequestOptions.ts +5 -0
  99. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +18 -0
  100. package/src/services/content-org/playlists.js +0 -0
  101. package/src/services/content.js +24 -9
  102. package/src/services/contentAggregator.js +18 -10
  103. package/src/services/railcontent.js +28 -0
  104. package/src/services/recommendations.js +21 -25
  105. package/src/services/user/interests.js +78 -0
  106. package/src/services/user/user-management-system.js +1 -0
  107. package/src/services/userActivity.js +23 -0
  108. package/test/HttpClient.test.js +257 -0
  109. package/test/contentLikes.test.js +0 -0
  110. package/test/contentProgress.test.js +0 -0
  111. package/test/dataContext.test.js +0 -0
  112. package/test/forum.test.js +0 -0
  113. package/test/imageSRCBuilder.test.js +0 -0
  114. package/test/imageSRCVerify.test.js +0 -0
  115. package/test/initializeTests.js +0 -0
  116. package/test/lib/lastUpdated.test.js +0 -0
  117. package/test/live/contentProgressLive.test.js +0 -0
  118. package/test/live/railcontentLive.test.js +0 -0
  119. package/test/localStorageMock.js +0 -0
  120. package/test/log.js +0 -0
  121. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  122. package/test/mockData/mockData_user_practices.json +0 -0
  123. package/test/streakMessage.test.js +0 -0
  124. package/test/user/permissions.test.js +0 -0
  125. package/test/userActivity.test.js +0 -0
  126. package/tools/generate-index.cjs +0 -0
@@ -0,0 +1,45 @@
1
+ import { RequestExecutor } from '../interfaces/RequestExecutor'
2
+ import { RequestOptions } from '../interfaces/RequestOptions'
3
+ import { HttpError } from '../interfaces/HttpError'
4
+
5
+ export class FetchRequestExecutor implements RequestExecutor {
6
+ async execute<T>(url: string, options: RequestOptions): Promise<T> {
7
+ const response = await fetch(url, options)
8
+
9
+ if (!response.ok) {
10
+ throw await this.createHttpError(response, url, options.method)
11
+ }
12
+
13
+ return this.parseResponse<T>(response)
14
+ }
15
+
16
+ private async createHttpError(
17
+ response: Response,
18
+ url: string,
19
+ method: string
20
+ ): Promise<HttpError> {
21
+ const error: HttpError = {
22
+ status: response.status,
23
+ statusText: response.statusText,
24
+ url,
25
+ method,
26
+ }
27
+
28
+ try {
29
+ error.body = await response.json()
30
+ } catch (e) {
31
+ error.body = await response.text()
32
+ }
33
+
34
+ return error
35
+ }
36
+
37
+ private async parseResponse<T>(response: Response): Promise<T> {
38
+ const contentType = response.headers.get('content-type')
39
+ if (contentType && contentType.indexOf('application/json') !== -1) {
40
+ return (await response.json()) as T
41
+ } else {
42
+ return (await response.text()) as unknown as T
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,13 @@
1
+ // Interfaces
2
+ export { RequestOptions } from './interfaces/RequestOptions'
3
+ export { HttpError } from './interfaces/HttpError'
4
+ export { NetworkError } from './interfaces/NetworkError'
5
+ export { HeaderProvider } from './interfaces/HeaderProvider'
6
+ export { RequestExecutor } from './interfaces/RequestExecutor'
7
+
8
+ // Implementations
9
+ export { DefaultHeaderProvider } from './providers/DefaultHeaderProvider'
10
+ export { FetchRequestExecutor } from './executors/FetchRequestExecutor'
11
+
12
+ // Main client
13
+ export { HttpClient } from './HttpClient'
@@ -0,0 +1,3 @@
1
+ export interface HeaderProvider {
2
+ getHeaders(): Record<string, string>
3
+ }
@@ -0,0 +1,7 @@
1
+ export interface HttpError {
2
+ status: number
3
+ statusText: string
4
+ url: string
5
+ method: string
6
+ body?: any
7
+ }
@@ -0,0 +1,6 @@
1
+ export interface NetworkError {
2
+ message: string
3
+ url: string
4
+ method: string
5
+ originalError: Error
6
+ }
@@ -0,0 +1,5 @@
1
+ import { RequestOptions } from './RequestOptions'
2
+
3
+ export interface RequestExecutor {
4
+ execute<T>(url: string, options: RequestOptions): Promise<T>
5
+ }
@@ -0,0 +1,5 @@
1
+ export interface RequestOptions {
2
+ method: string
3
+ headers: Record<string, string>
4
+ body?: string
5
+ }
@@ -0,0 +1,18 @@
1
+ import { HeaderProvider } from '../interfaces/HeaderProvider'
2
+ import { globalConfig } from '../../../services/config.js'
3
+
4
+ export class DefaultHeaderProvider implements HeaderProvider {
5
+ getHeaders(): Record<string, string> {
6
+ const headers: Record<string, string> = {
7
+ 'Content-Type': 'application/json',
8
+ Accept: 'application/json',
9
+ }
10
+
11
+ // Handle timezone
12
+ if (globalConfig.localTimezoneString) {
13
+ headers['M-Client-Timezone'] = globalConfig.localTimezoneString
14
+ }
15
+
16
+ return headers
17
+ }
18
+ }
File without changes
@@ -21,23 +21,18 @@ import {recommendations} from "./recommendations";
21
21
 
22
22
  export async function getLessonContentRows (brand='drumeo', pageName = 'lessons') {
23
23
  let recentContentIds = await fetchRecent(brand, pageName, { progress: 'recent' });
24
- recentContentIds = recentContentIds.map(item => item.id);
25
24
 
26
25
  let contentRows = await getContentRows(brand, pageName);
27
26
  contentRows = Array.isArray(contentRows) ? contentRows : [];
28
27
  contentRows.unshift({
29
28
  id: 'recent',
30
29
  title: 'Recent ' + capitalizeFirstLetter(pageName),
31
- content: recentContentIds || []
30
+ items: recentContentIds || []
32
31
  });
33
32
 
34
33
  const results = await Promise.all(
35
34
  contentRows.map(async (row) => {
36
- if (row.content.length == 0){
37
- return { id: row.id, title: row.title, items: [] }
38
- }
39
- const data = await fetchByRailContentIds(row.content)
40
- return { id: row.id, title: row.title, items: data }
35
+ return { id: row.id, title: row.title, items: row.items }
41
36
  })
42
37
  )
43
38
  return results
@@ -163,7 +158,8 @@ export async function getRecent(brand, pageName, tabName = 'all', {
163
158
  * @param {Object} params - Parameters for pagination.
164
159
  * @param {number} [params.page=1] - The page number for pagination.
165
160
  * @param {number} [params.limit=10] - The maximum number of content items per row.
166
- * @returns {Promise<Object>} - The fetched content rows.
161
+ * @returns {Promise<Object>} - The fetched content rows with complete Sanity data instead of just content IDs.
162
+ * When contentRowId is provided, returns an object with type, data, and meta properties.
167
163
  *
168
164
  * @example
169
165
  * getContentRows('drumeo', 'lessons', 'Your-Daily-Warmup', {
@@ -179,7 +175,26 @@ export async function getContentRows(brand, pageName, contentRowId , {
179
175
  } = {}) {
180
176
  const contentRow = contentRowId ? `&content_row_id=${contentRowId}` : ''
181
177
  const url = `/api/content/v1/rows?brand=${brand}&page_name=${pageName}${contentRow}&page=${page}&limit=${limit}`;
182
- return await fetchHandler(url, 'get', null);
178
+ const contentRows = await fetchHandler(url, 'get', null) || [];
179
+ const results = await Promise.all(
180
+ contentRows.map(async (row) => {
181
+ if (row.content.length === 0){
182
+ return { id: row.id, title: row.title, items: [] }
183
+ }
184
+ const data = await fetchByRailContentIds(row.content)
185
+ return { id: row.id, title: row.title, items: data }
186
+ })
187
+ )
188
+
189
+ if (contentRowId) {
190
+ return {
191
+ type: TabResponseType.CATALOG,
192
+ data: results[0].items,
193
+ meta: {}
194
+ };
195
+ }
196
+
197
+ return results
183
198
  }
184
199
 
185
200
  /**
@@ -1,6 +1,6 @@
1
1
  import { getProgressStateByIds, getProgressPercentageByIds, getResumeTimeSecondsByIds } from "./contentProgress"
2
2
  import { isContentLikedByIds } from "./contentLikes"
3
- import { fetchLikeCount } from "./railcontent"
3
+ import { fetchLikeCount, fetchLastInteractedChild } from "./railcontent"
4
4
 
5
5
 
6
6
 
@@ -15,7 +15,8 @@ export async function addContextToContent(dataPromise, ...dataArgs)
15
15
  addIsLiked = false,
16
16
  addLikeCount = false,
17
17
  addProgressStatus = false,
18
- addResumeTimeSeconds = false
18
+ addResumeTimeSeconds = false,
19
+ addLastInteractedChild = false,
19
20
  } = options
20
21
 
21
22
  const dataParam = lastArg === options ? dataArgs.slice(0, -1) : dataArgs
@@ -35,13 +36,15 @@ export async function addContextToContent(dataPromise, ...dataArgs)
35
36
 
36
37
  if(ids.length === 0) return false
37
38
 
38
- const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData] = await Promise.all([
39
+ const [progressPercentageData, progressStatusData, isLikedData, resumeTimeData, lastInteractedChildData] = await Promise.all([
39
40
  addProgressPercentage ? getProgressPercentageByIds(ids) : Promise.resolve(null),
40
41
  addProgressStatus ? getProgressStateByIds(ids) : Promise.resolve(null),
41
42
  addIsLiked ? isContentLikedByIds(ids) : Promise.resolve(null),
42
43
  addResumeTimeSeconds ? getResumeTimeSecondsByIds(ids) : Promise.resolve(null),
44
+ addLastInteractedChild ? fetchLastInteractedChild(ids) : Promise.resolve(null),
43
45
  ])
44
-
46
+ console.log('ids', ids)
47
+ console.log('lastInteractedChildData', lastInteractedChildData)
45
48
  const addContext = async (item) => ({
46
49
  ...item,
47
50
  ...(addProgressPercentage ? { progressPercentage: progressPercentageData?.[item.id] } : {}),
@@ -49,12 +52,17 @@ export async function addContextToContent(dataPromise, ...dataArgs)
49
52
  ...(addIsLiked ? { isLiked: isLikedData?.[item.id] } : {}),
50
53
  ...(addLikeCount && ids.length === 1 ? { likeCount: await fetchLikeCount(item.id) } : {}),
51
54
  ...(addResumeTimeSeconds ? { resumeTime: resumeTimeData?.[item.id] } : {}),
55
+ ...(addLastInteractedChild ? { lastInteractedChild: lastInteractedChildData?.[item.id] } : {}),
52
56
  })
53
-
54
- const newData = Array.isArray(data)
55
- ? await Promise.all(data.map(addContext))
56
- : await addContext(data)
57
-
58
- return newData
57
+ if (dataField) {
58
+ data[dataField] = Array.isArray(data[dataField])
59
+ ? await Promise.all(data[dataField].map(addContext))
60
+ : await addContext(data[dataField])
61
+ return data
62
+ } else {
63
+ return Array.isArray(data)
64
+ ? await Promise.all(data.map(addContext))
65
+ : await addContext(data)
66
+ }
59
67
  }
60
68
 
@@ -821,6 +821,34 @@ export async function fetchUserPracticeNotes(date) {
821
821
  return await fetchHandler(url, 'GET', null)
822
822
  }
823
823
 
824
+
825
+ /**
826
+ * Get the id and slug of last interacted child. Only valid for certain content types
827
+ *
828
+ * @async
829
+ * @function fetchLastInteractedChild
830
+ * @param {array} content_ids - Content ids of to get the last interacted child of
831
+ *
832
+ *
833
+ * @returns {Promise<Object>} - keyed object per valid content ids with the child
834
+ *
835
+ * @example
836
+ * try {
837
+ * const response = await fetchLastInteractedChild([191369, 410427]);
838
+ * console.log('child id', response[191369].content_id)
839
+ * console.log('child slug', response[191369].slug)
840
+ * } catch (error) {
841
+ * console.error('Failed to get children', error);
842
+ * }
843
+ */
844
+ export async function fetchLastInteractedChild(content_ids) {
845
+ const params = new URLSearchParams();
846
+ content_ids.forEach(id => params.append('content_ids[]', id));
847
+ const url = `/api/content/v1/user/last_interacted_child?${params.toString()}`
848
+ return await fetchHandler(url, 'GET', null)
849
+ }
850
+
851
+
824
852
  function fetchAbsolute(url, params) {
825
853
  if (globalConfig.sessionConfig.authToken) {
826
854
  params.headers['Authorization'] = `Bearer ${globalConfig.sessionConfig.authToken}`
@@ -2,8 +2,8 @@
2
2
  * @module Railcontent-Services
3
3
  */
4
4
 
5
- import {globalConfig} from './config.js'
6
- import {fetchJSONHandler} from '../lib/httpHelper.js'
5
+ import { globalConfig } from './config.js'
6
+ import { HttpClient } from '../infrastructure/http/HttpClient'
7
7
 
8
8
  /**
9
9
  * Exported functions that are excluded from index generation.
@@ -36,7 +36,11 @@ export async function fetchSimilarItems(content_id, brand, count = 10) {
36
36
  }
37
37
  const url = `/similar_items/`
38
38
  try {
39
- const response = await fetchHandler(url, 'POST', data)
39
+ const httpClient = new HttpClient(
40
+ globalConfig.recommendationsConfig.baseUrl,
41
+ globalConfig.recommendationsConfig.token
42
+ )
43
+ const response = await httpClient.post(url, data)
40
44
  // we requested count + 1 then filtered out the extra potential value, so we need slice to the correct size if necessary
41
45
  return response['similar_items'].filter((item) => item !== content_id).slice(0, count)
42
46
  } catch (error) {
@@ -71,7 +75,11 @@ export async function rankCategories(brand, categories) {
71
75
  }
72
76
  const url = `/rank_each_list/`
73
77
  try {
74
- const response = await fetchHandler(url, 'POST', data)
78
+ const httpClient = new HttpClient(
79
+ globalConfig.recommendationsConfig.baseUrl,
80
+ globalConfig.recommendationsConfig.token
81
+ )
82
+ const response = await httpClient.post(url, data)
75
83
  let rankedCategories = {}
76
84
  response['ranked_playlists'].forEach(
77
85
  (category) =>
@@ -106,7 +114,11 @@ export async function rankItems(brand, content_ids) {
106
114
  }
107
115
  const url = `/rank_items/`
108
116
  try {
109
- const response = await fetchHandler(url, 'POST', data)
117
+ const httpClient = new HttpClient(
118
+ globalConfig.recommendationsConfig.baseUrl,
119
+ globalConfig.recommendationsConfig.token
120
+ )
121
+ const response = await httpClient.post(url, data)
110
122
  return response['ranked_content_ids']
111
123
  } catch (error) {
112
124
  console.error('Fetch error:', error)
@@ -114,31 +126,15 @@ export async function rankItems(brand, content_ids) {
114
126
  }
115
127
  }
116
128
 
117
- export async function recommendations(brand, {section = ''} = {}) {
129
+ export async function recommendations(brand, { section = '' } = {}) {
118
130
  section = section.toUpperCase().replace('-', '_')
119
- const sectionString = section ? `&section=${section}` : '';
131
+ const sectionString = section ? `&section=${section}` : ''
120
132
  const url = `/api/content/v1/recommendations?brand=${brand}${sectionString}`
121
133
  try {
122
- // This goes through the MPB, not the recommendations api, so we use fetchJSONHandler instead of the local handler
123
- return fetchJSONHandler(
124
- url,
125
- globalConfig.sessionConfig.token,
126
- globalConfig.baseUrl,
127
- 'get'
128
- )
134
+ const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
135
+ return httpClient.get(url)
129
136
  } catch (error) {
130
137
  console.error('Fetch error:', error)
131
138
  return null
132
139
  }
133
140
  }
134
-
135
- async function fetchHandler(url, method = 'get', body = null) {
136
- return fetchJSONHandler(
137
- url,
138
- globalConfig.recommendationsConfig.token,
139
- globalConfig.recommendationsConfig.baseUrl,
140
- method,
141
- null,
142
- body
143
- )
144
- }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @module Interests
3
+ */
4
+ import { globalConfig } from '../config.js'
5
+ import { fetchHandler } from '../railcontent.js'
6
+ import './types.js'
7
+
8
+ const baseUrl = `/api/user-management-system`
9
+
10
+ /**
11
+ * @param {number} [userId=globalConfig.sessionConfig.userId] - The user ID
12
+ * @returns {Promise<Array<number>>} - The list of interests
13
+ */
14
+ export async function fetchInterests(userId = globalConfig.sessionConfig.userId) {
15
+ const url = `${baseUrl}/v1/users/${userId}/interests`
16
+ return fetchHandler(url, 'get')
17
+ }
18
+
19
+ /**
20
+ * @param {number} contentId
21
+ * @returns {Promise<any>}
22
+ */
23
+ export async function markContentAsInterested(contentId) {
24
+ if (!contentId) {
25
+ throw new Error('contentId is required')
26
+ }
27
+
28
+ const url = `${baseUrl}/v1/users/interests/${contentId}`
29
+ return fetchHandler(url, 'post')
30
+ }
31
+
32
+ /**
33
+ * @param {number} contentId
34
+ * @returns {Promise<any>}
35
+ */
36
+ export async function removeContentAsInterested(contentId) {
37
+ if (!contentId) {
38
+ throw new Error('contentId is required')
39
+ }
40
+
41
+ const url = `${baseUrl}/v1/users/interests/${contentId}`
42
+ return fetchHandler(url, 'delete')
43
+ }
44
+
45
+ /**
46
+ * @param {number} [userId=globalConfig.sessionConfig.userId] - The user ID
47
+ * @returns {Promise<Array<number>>} - The list of content the user is not interested in
48
+ */
49
+ export async function fetchUninterests(userId = globalConfig.sessionConfig.userId) {
50
+ const url = `${baseUrl}/v1/users/${userId}/uninterests`
51
+ return fetchHandler(url, 'get')
52
+ }
53
+
54
+ /**
55
+ * @param {number} contentId
56
+ * @returns {Promise<any>}
57
+ */
58
+ export async function markContentAsNotInterested(contentId) {
59
+ if (!contentId) {
60
+ throw new Error('contentId is required')
61
+ }
62
+
63
+ const url = `${baseUrl}/v1/users/uninterests/${contentId}`
64
+ return fetchHandler(url, 'post')
65
+ }
66
+
67
+ /**
68
+ * @param {number} contentId
69
+ * @returns {Promise<any>}
70
+ */
71
+ export async function removeContentAsNotInterested(contentId) {
72
+ if (!contentId) {
73
+ throw new Error('contentId is required')
74
+ }
75
+
76
+ const url = `${baseUrl}/v1/users/uninterests/${contentId}`
77
+ return fetchHandler(url, 'delete')
78
+ }
@@ -3,4 +3,5 @@
3
3
  * @property {module:Sessions} Sessions
4
4
  * @property {module:Permissions} Permissions
5
5
  * @property {module:UserManagement} UserManagement
6
+ * @property {module:Interests} Interests
6
7
  */
@@ -135,6 +135,22 @@ export async function getUserMonthlyStats( params = {}) {
135
135
  today.setHours(0, 0, 0, 0);
136
136
 
137
137
  let startOfGrid = getMonday(firstDayOfMonth)
138
+
139
+ let previousWeekStart = new Date(startOfGrid)
140
+ previousWeekStart.setDate(previousWeekStart.getDate() - 7)
141
+
142
+ let previousWeekEnd = new Date(startOfGrid)
143
+ previousWeekEnd.setDate(previousWeekEnd.getDate() - 1)
144
+
145
+ let hadStreakBeforeMonth = false
146
+ for (let d = new Date(previousWeekStart); d <= previousWeekEnd; d.setDate(d.getDate() + 1)) {
147
+ let dayKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
148
+ if (practices[dayKey]) {
149
+ hadStreakBeforeMonth = true
150
+ break
151
+ }
152
+ }
153
+
138
154
  let endOfMonth = new Date(year, month + 1, 0)
139
155
  while (endOfMonth.getDay() !== 0) {
140
156
  endOfMonth.setDate(endOfMonth.getDate() + 1)
@@ -181,6 +197,13 @@ export async function getUserMonthlyStats( params = {}) {
181
197
  })
182
198
  }
183
199
 
200
+ if (hadStreakBeforeMonth) {
201
+ const firstWeekKey = getWeekNumber(startOfGrid)
202
+ if (weeklyStats[firstWeekKey]) {
203
+ weeklyStats[firstWeekKey].continueStreak = true
204
+ }
205
+ }
206
+
184
207
  let filteredPractices = Object.keys(practices)
185
208
  .filter((date) => new Date(date) <= endOfMonth)
186
209
  .reduce((obj, key) => {