musora-content-services 2.87.0 → 2.88.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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/index.d.ts +7 -0
- package/src/index.js +7 -0
- package/src/services/content-org/learning-paths.ts +5 -12
- package/src/services/reporting/README.md +316 -0
- package/src/services/reporting/reporting.ts +189 -0
- package/src/services/reporting/types.ts +56 -0
- package/.claude/settings.local.json +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.88.1](https://github.com/railroadmedia/musora-content-services/compare/v2.88.0...v2.88.1) (2025-11-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* add queries in get daily session and get active path ([#593](https://github.com/railroadmedia/musora-content-services/issues/593)) ([91f3974](https://github.com/railroadmedia/musora-content-services/commit/91f3974c530ca695fa87f5bfe14066e5a473db79))
|
|
11
|
+
|
|
12
|
+
## [2.88.0](https://github.com/railroadmedia/musora-content-services/compare/v2.86.1...v2.88.0) (2025-11-26)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **BEH-1418:** refactor learning path endpoints ([#589](https://github.com/railroadmedia/musora-content-services/issues/589)) ([e3637f6](https://github.com/railroadmedia/musora-content-services/commit/e3637f6359ddf8f4295eead890de6457b942b4ea))
|
|
18
|
+
* Implement unified user reporting system ([7aa42ab](https://github.com/railroadmedia/musora-content-services/commit/7aa42ab8dcee09ce3f293d9fbb1a203dcbe94414))
|
|
19
|
+
* **MU2-1250:** Implement unified user reporting system ([3303282](https://github.com/railroadmedia/musora-content-services/commit/33032829b729efa9f9b1e40f31179127c8c74f9d))
|
|
20
|
+
|
|
5
21
|
## [2.87.0](https://github.com/railroadmedia/musora-content-services/compare/v2.86.1...v2.87.0) (2025-11-26)
|
|
6
22
|
|
|
7
23
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -242,6 +242,11 @@ import {
|
|
|
242
242
|
recommendations
|
|
243
243
|
} from './services/recommendations.js';
|
|
244
244
|
|
|
245
|
+
import {
|
|
246
|
+
getReportIssueOptions,
|
|
247
|
+
report
|
|
248
|
+
} from './services/reporting/reporting.ts';
|
|
249
|
+
|
|
245
250
|
import {
|
|
246
251
|
buildEntityAndTotalQuery,
|
|
247
252
|
fetchAll,
|
|
@@ -597,6 +602,7 @@ declare module 'musora-content-services' {
|
|
|
597
602
|
getRecent,
|
|
598
603
|
getRecentActivity,
|
|
599
604
|
getRecommendedForYou,
|
|
605
|
+
getReportIssueOptions,
|
|
600
606
|
getResumeTimeSeconds,
|
|
601
607
|
getResumeTimeSecondsByIds,
|
|
602
608
|
getSanityDate,
|
|
@@ -662,6 +668,7 @@ declare module 'musora-content-services' {
|
|
|
662
668
|
removeContentAsNotInterested,
|
|
663
669
|
removeUserPractice,
|
|
664
670
|
replyToComment,
|
|
671
|
+
report,
|
|
665
672
|
reportComment,
|
|
666
673
|
reportPlaylist,
|
|
667
674
|
requestEmailChange,
|
package/src/index.js
CHANGED
|
@@ -242,6 +242,11 @@ import {
|
|
|
242
242
|
recommendations
|
|
243
243
|
} from './services/recommendations.js';
|
|
244
244
|
|
|
245
|
+
import {
|
|
246
|
+
getReportIssueOptions,
|
|
247
|
+
report
|
|
248
|
+
} from './services/reporting/reporting.ts';
|
|
249
|
+
|
|
245
250
|
import {
|
|
246
251
|
buildEntityAndTotalQuery,
|
|
247
252
|
fetchAll,
|
|
@@ -596,6 +601,7 @@ export {
|
|
|
596
601
|
getRecent,
|
|
597
602
|
getRecentActivity,
|
|
598
603
|
getRecommendedForYou,
|
|
604
|
+
getReportIssueOptions,
|
|
599
605
|
getResumeTimeSeconds,
|
|
600
606
|
getResumeTimeSecondsByIds,
|
|
601
607
|
getSanityDate,
|
|
@@ -661,6 +667,7 @@ export {
|
|
|
661
667
|
removeContentAsNotInterested,
|
|
662
668
|
removeUserPractice,
|
|
663
669
|
replyToComment,
|
|
670
|
+
report,
|
|
664
671
|
reportComment,
|
|
665
672
|
reportPlaylist,
|
|
666
673
|
requestEmailChange,
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { fetchHandler } from '../railcontent.js'
|
|
6
|
-
import { fetchByRailContentId,
|
|
6
|
+
import { fetchByRailContentId, fetchMethodV2Structure } from '../sanity.js'
|
|
7
7
|
import { addContextToContent } from '../contentAggregator.js'
|
|
8
8
|
import {
|
|
9
9
|
contentStatusCompleted,
|
|
10
10
|
contentStatusReset,
|
|
11
11
|
getProgressState,
|
|
12
|
-
getProgressStateByIds
|
|
13
12
|
} from '../contentProgress.js'
|
|
14
13
|
|
|
15
14
|
const BASE_PATH: string = `/api/content-org`
|
|
@@ -21,10 +20,6 @@ interface ActiveLearningPathResponse {
|
|
|
21
20
|
active_learning_path_id: number,
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
23
|
/**
|
|
29
24
|
* Gets today's daily session for the user.
|
|
30
25
|
* @param brand
|
|
@@ -32,9 +27,8 @@ interface ActiveLearningPathResponse {
|
|
|
32
27
|
*/
|
|
33
28
|
export async function getDailySession(brand: string, userDate: Date) {
|
|
34
29
|
const stringDate = userDate.toISOString().split('T')[0]
|
|
35
|
-
const url: string = `${LEARNING_PATHS_PATH}/daily-session/get`
|
|
36
|
-
|
|
37
|
-
return await fetchHandler(url, 'GET', null, body)
|
|
30
|
+
const url: string = `${LEARNING_PATHS_PATH}/daily-session/get?brand=${brand}&userDate=${userDate}`
|
|
31
|
+
return await fetchHandler(url, 'GET', null, null)
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
/**
|
|
@@ -59,9 +53,8 @@ export async function updateDailySession(
|
|
|
59
53
|
* @param brand
|
|
60
54
|
*/
|
|
61
55
|
export async function getActivePath(brand: string) {
|
|
62
|
-
const url: string = `${LEARNING_PATHS_PATH}/active-path/get`
|
|
63
|
-
|
|
64
|
-
return await fetchHandler(url, 'GET', null, body)
|
|
56
|
+
const url: string = `${LEARNING_PATHS_PATH}/active-path/get?brand=${brand}`
|
|
57
|
+
return await fetchHandler(url, 'GET', null, null)
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
/**
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Reporting Service
|
|
2
|
+
|
|
3
|
+
Service for submitting user reports about content, comments, forum posts, and playlists.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This service provides two main functions for reporting content:
|
|
8
|
+
|
|
9
|
+
1. **`report()`** - Unified method to submit reports for any type of content (content, comments, forum posts, playlists)
|
|
10
|
+
2. **`getReportIssueOptions()`** - Helper function to get valid issue options for each reportable type, with support for platform-specific options (web vs mobile app)
|
|
11
|
+
|
|
12
|
+
Reports are sent to the appropriate team (support or mentors) based on the content type. Both functions are designed to be used by the web platform (MPF) and mobile app (MA) to ensure consistent reporting options and behavior.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- ✅ Unified `report()` method for all reportable types
|
|
17
|
+
- ✅ Type-safe issue selection with TypeScript generics
|
|
18
|
+
- ✅ Platform-specific options (web vs mobile app)
|
|
19
|
+
- ✅ Helper function to get valid issue options by type
|
|
20
|
+
- ✅ Rate limiting (10 requests per minute per user)
|
|
21
|
+
- ✅ Emails sent to appropriate teams (support or mentors)
|
|
22
|
+
- ✅ Full TypeScript support with mapped types
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
The reporting service is included in `musora-content-services`. Simply import the functions you need:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { report, getReportIssueOptions } from 'musora-content-services'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Both functions are exported from `services/reporting/reporting.ts`.
|
|
33
|
+
|
|
34
|
+
## API Reference
|
|
35
|
+
|
|
36
|
+
### `report<T>(params)`
|
|
37
|
+
|
|
38
|
+
Unified method to submit a report for any type. Uses TypeScript generics for type-safe issue selection.
|
|
39
|
+
|
|
40
|
+
**Type Parameter:**
|
|
41
|
+
- `T extends ReportableType` - The type of content being reported
|
|
42
|
+
|
|
43
|
+
**Parameters:**
|
|
44
|
+
```typescript
|
|
45
|
+
type ReportParams<T extends ReportableType> = {
|
|
46
|
+
type: T // 'content' | 'comment' | 'forum_post' | 'playlist'
|
|
47
|
+
id: number // ID of the entity being reported
|
|
48
|
+
issue: IssueTypeMap[T] // Type-safe issue (varies by type)
|
|
49
|
+
details?: string // Required when issue is 'other', not sent otherwise
|
|
50
|
+
brand: string // Required: 'drumeo', 'pianote', 'guitareo', 'singeo', 'playbass'
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Returns:** `Promise<ReportResponse>`
|
|
55
|
+
```typescript
|
|
56
|
+
interface ReportResponse {
|
|
57
|
+
report_id: number // The ID of the submitted report
|
|
58
|
+
message: string // Success message
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Example - Report Content:**
|
|
63
|
+
```typescript
|
|
64
|
+
import { report } from 'musora-content-services'
|
|
65
|
+
|
|
66
|
+
const response = await report({
|
|
67
|
+
type: 'content',
|
|
68
|
+
id: 12345,
|
|
69
|
+
issue: 'video_issue',
|
|
70
|
+
brand: 'drumeo'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
console.log(`Report submitted with ID: ${response.report_id}`)
|
|
74
|
+
console.log(response.message) // "Report submitted successfully"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Example - Report Comment:**
|
|
78
|
+
```typescript
|
|
79
|
+
await report({
|
|
80
|
+
type: 'comment',
|
|
81
|
+
id: 67890,
|
|
82
|
+
issue: 'offensive_language',
|
|
83
|
+
brand: 'pianote'
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Example - Report Forum Post:**
|
|
88
|
+
```typescript
|
|
89
|
+
await report({
|
|
90
|
+
type: 'forum_post',
|
|
91
|
+
id: 45678,
|
|
92
|
+
issue: 'abusive',
|
|
93
|
+
brand: 'guitareo'
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Example - Report Playlist:**
|
|
98
|
+
```typescript
|
|
99
|
+
await report({
|
|
100
|
+
type: 'playlist',
|
|
101
|
+
id: 99999,
|
|
102
|
+
issue: 'incorrect_metadata',
|
|
103
|
+
brand: 'singeo'
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Example - Report with 'other' issue (details required):**
|
|
108
|
+
```typescript
|
|
109
|
+
await report({
|
|
110
|
+
type: 'content',
|
|
111
|
+
id: 12345,
|
|
112
|
+
issue: 'other',
|
|
113
|
+
details: 'The instructor audio is out of sync with the video',
|
|
114
|
+
brand: 'drumeo'
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `getReportIssueOptions(type, isMobileApp?)`
|
|
121
|
+
|
|
122
|
+
Helper function to get valid issue options for a specific reportable type. Returns both the value and user-friendly label for each option.
|
|
123
|
+
|
|
124
|
+
**Parameters:**
|
|
125
|
+
```typescript
|
|
126
|
+
type: ReportableType // 'content' | 'comment' | 'forum_post' | 'playlist'
|
|
127
|
+
isMobileApp?: boolean // Default: false. Set to true for mobile app options
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Returns:** `ReportIssueOption[]`
|
|
131
|
+
```typescript
|
|
132
|
+
interface ReportIssueOption {
|
|
133
|
+
value: string // Issue value to send to API
|
|
134
|
+
label: string // User-friendly label for display
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Example - Web Platform:**
|
|
139
|
+
```typescript
|
|
140
|
+
import { getReportIssueOptions } from 'musora-content-services'
|
|
141
|
+
|
|
142
|
+
// Get content issue options for web
|
|
143
|
+
const options = getReportIssueOptions('content')
|
|
144
|
+
// Returns:
|
|
145
|
+
// [
|
|
146
|
+
// { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
147
|
+
// { value: 'video_issue', label: 'Video issue' },
|
|
148
|
+
// { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
149
|
+
// { value: 'other', label: 'Other' }
|
|
150
|
+
// ]
|
|
151
|
+
options.map(opt => ({
|
|
152
|
+
...opt,
|
|
153
|
+
label: t(`report.${opt.value}`)
|
|
154
|
+
}))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Example - Mobile App:**
|
|
158
|
+
```typescript
|
|
159
|
+
// Get content issue options for mobile app (includes download option)
|
|
160
|
+
const options = getReportIssueOptions('content', true)
|
|
161
|
+
// Returns:
|
|
162
|
+
// [
|
|
163
|
+
// { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
164
|
+
// { value: 'video_issue', label: 'Video issue' },
|
|
165
|
+
// { value: 'download_unavailable', label: 'Download is not available' },
|
|
166
|
+
// { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
167
|
+
// { value: 'other', label: 'Other' }
|
|
168
|
+
// ]
|
|
169
|
+
options.map(opt => ({
|
|
170
|
+
...opt,
|
|
171
|
+
label: t(`report.${opt.value}`)
|
|
172
|
+
}))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Example - Comment Issues:**
|
|
176
|
+
```typescript
|
|
177
|
+
const commentOptions = getReportIssueOptions('comment')
|
|
178
|
+
// Returns:
|
|
179
|
+
// [
|
|
180
|
+
// { value: 'offensive_language', label: 'It contains offensive language or content' },
|
|
181
|
+
// { value: 'abusive', label: "It's abusive or harmful" },
|
|
182
|
+
// { value: 'personal_information', label: 'It contains personal information' },
|
|
183
|
+
// { value: 'misleading', label: "It's misleading or a false claim" },
|
|
184
|
+
// { value: 'other', label: 'Other reasons' }
|
|
185
|
+
// ]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Report Issues by Type
|
|
191
|
+
|
|
192
|
+
### Content & Playlists:
|
|
193
|
+
- `incorrect_metadata` - The lesson image, title or description is incorrect
|
|
194
|
+
- `video_issue` - Video issue
|
|
195
|
+
- `download_unavailable` - Download is not available (mobile app only)
|
|
196
|
+
- `assignment_issue` - An issue with lesson assignment
|
|
197
|
+
- `other` - Other
|
|
198
|
+
|
|
199
|
+
### Comments & Forum Posts:
|
|
200
|
+
- `offensive_language` - It contains offensive language or content
|
|
201
|
+
- `abusive` - It's abusive or harmful
|
|
202
|
+
- `personal_information` - It contains personal information
|
|
203
|
+
- `misleading` - It's misleading or a false claim
|
|
204
|
+
- `other` - Other reasons
|
|
205
|
+
|
|
206
|
+
**Important:** The `details` field is **required** when `issue` is `'other'` and should **not be sent** for other issue types.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Rate Limiting
|
|
211
|
+
|
|
212
|
+
API requests are rate-limited to **10 requests per minute per user**. If you exceed this limit, you'll receive a `429 Too Many Requests` error.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Email Recipients
|
|
217
|
+
|
|
218
|
+
Reports are automatically routed to the appropriate team:
|
|
219
|
+
|
|
220
|
+
| Report Type | Email Recipient |
|
|
221
|
+
|-------------|----------------|
|
|
222
|
+
| Forum Posts | mentors@musora.com |
|
|
223
|
+
| Comments | mentors@musora.com |
|
|
224
|
+
| Content | support@musora.com |
|
|
225
|
+
| Playlists | support@musora.com |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Platform-Specific Options
|
|
230
|
+
|
|
231
|
+
The `getReportIssueOptions()` function supports platform-specific options via the `isMobileApp` parameter:
|
|
232
|
+
|
|
233
|
+
**Web Platform (default):**
|
|
234
|
+
- Does NOT include "Download is not available" option
|
|
235
|
+
- Use: `getReportIssueOptions('content')`
|
|
236
|
+
|
|
237
|
+
**Mobile App:**
|
|
238
|
+
- INCLUDES "Download is not available" option for content and playlists
|
|
239
|
+
- Use: `getReportIssueOptions('content', true)`
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Error Handling
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { report } from 'musora-content-services'
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await report({
|
|
251
|
+
type: 'content',
|
|
252
|
+
id: 123,
|
|
253
|
+
issue: 'video_issue',
|
|
254
|
+
brand: 'drumeo'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Handle success
|
|
258
|
+
showToast('Report submitted successfully')
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error.status === 429) {
|
|
261
|
+
// Rate limit exceeded
|
|
262
|
+
showToast('Too many reports. Please try again later.')
|
|
263
|
+
} else if (error.status === 422) {
|
|
264
|
+
// Validation error
|
|
265
|
+
showToast(error.message || 'Invalid report data')
|
|
266
|
+
} else if (error.status === 401) {
|
|
267
|
+
// Not authenticated
|
|
268
|
+
showToast('Please log in to submit a report')
|
|
269
|
+
} else {
|
|
270
|
+
// Other errors
|
|
271
|
+
showToast('Failed to submit report')
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Type Safety
|
|
279
|
+
|
|
280
|
+
The reporting service uses TypeScript generics to ensure type-safe issue selection:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// ✅ Type-safe: 'video_issue' is valid for content
|
|
284
|
+
await report({
|
|
285
|
+
type: 'content',
|
|
286
|
+
id: 123,
|
|
287
|
+
issue: 'video_issue', // TypeScript knows this is valid
|
|
288
|
+
brand: 'drumeo'
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ❌ Type error: 'offensive_language' is not valid for content
|
|
292
|
+
await report({
|
|
293
|
+
type: 'content',
|
|
294
|
+
id: 123,
|
|
295
|
+
issue: 'offensive_language', // TypeScript error!
|
|
296
|
+
brand: 'drumeo'
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// ✅ Type-safe: 'offensive_language' is valid for comments
|
|
300
|
+
await report({
|
|
301
|
+
type: 'comment',
|
|
302
|
+
id: 456,
|
|
303
|
+
issue: 'offensive_language', // TypeScript knows this is valid
|
|
304
|
+
brand: 'drumeo'
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The `IssueTypeMap` maps each reportable type to its valid issues:
|
|
309
|
+
```typescript
|
|
310
|
+
type IssueTypeMap = {
|
|
311
|
+
forum_post: ForumIssueType
|
|
312
|
+
comment: CommentIssueType
|
|
313
|
+
content: ContentIssueType
|
|
314
|
+
playlist: PlaylistIssueType
|
|
315
|
+
}
|
|
316
|
+
```
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Reporting
|
|
3
|
+
* @description Service for submitting user reports about content, comments, forum posts, and playlists.
|
|
4
|
+
*
|
|
5
|
+
* This service provides a unified method to report any type of content when users encounter
|
|
6
|
+
* inappropriate behavior, technical issues, or other problems.
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
11
|
+
import { globalConfig } from '../config.js'
|
|
12
|
+
import {
|
|
13
|
+
ReportResponse,
|
|
14
|
+
ReportableType,
|
|
15
|
+
IssueTypeMap,
|
|
16
|
+
ReportIssueOption,
|
|
17
|
+
} from './types'
|
|
18
|
+
import {Brand} from "../../lib/brands";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parameters for submitting a report with type-safe issue values
|
|
22
|
+
*/
|
|
23
|
+
export type ReportParams<T extends ReportableType = ReportableType> = {
|
|
24
|
+
/** Type of content being reported */
|
|
25
|
+
type: T
|
|
26
|
+
/** ID of the content being reported */
|
|
27
|
+
id: number
|
|
28
|
+
/** Issue category - type-safe based on reportable type */
|
|
29
|
+
issue: IssueTypeMap[T]
|
|
30
|
+
/** Details about the issue - required when issue is 'other', not sent otherwise */
|
|
31
|
+
details?: string
|
|
32
|
+
/** Brand context (required: drumeo, pianote, guitareo, singeo, playbass) */
|
|
33
|
+
brand: Brand
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Submit a report for any type of content.
|
|
38
|
+
*
|
|
39
|
+
* This is a unified method that handles all report types (content, comment, forum_post, playlist).
|
|
40
|
+
* The issue parameter is type-safe based on the reportable type.
|
|
41
|
+
*
|
|
42
|
+
* @param {ReportParams} params - The report parameters
|
|
43
|
+
* @returns {Promise<ReportResponse>} The report submission response
|
|
44
|
+
* @throws {HttpError} Throws HttpError if the request fails
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Report content with technical issue - no details needed
|
|
48
|
+
* const response = await report({
|
|
49
|
+
* type: 'content',
|
|
50
|
+
* id: 123,
|
|
51
|
+
* issue: 'video_issue',
|
|
52
|
+
* brand: 'drumeo'
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* console.log(`Report submitted with ID: ${response.report_id}`)
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Report with 'other' issue - details required
|
|
59
|
+
* await report({
|
|
60
|
+
* type: 'content',
|
|
61
|
+
* id: 456,
|
|
62
|
+
* issue: 'other',
|
|
63
|
+
* details: 'The instructor audio is out of sync with the video',
|
|
64
|
+
* brand: 'drumeo'
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Report forum post
|
|
69
|
+
* await report({
|
|
70
|
+
* type: 'forum_post',
|
|
71
|
+
* id: 789,
|
|
72
|
+
* issue: 'abusive',
|
|
73
|
+
* brand: 'drumeo'
|
|
74
|
+
* })
|
|
75
|
+
*/
|
|
76
|
+
export async function report<T extends ReportableType>(params: ReportParams<T>): Promise<ReportResponse> {
|
|
77
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
78
|
+
|
|
79
|
+
// Build request body
|
|
80
|
+
const requestBody: any = {
|
|
81
|
+
reportable_type: params.type,
|
|
82
|
+
reportable_id: params.id,
|
|
83
|
+
issue: params.issue,
|
|
84
|
+
brand: params.brand,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add details only when provided (required for 'other' issue)
|
|
88
|
+
if (params.details) {
|
|
89
|
+
requestBody.details = params.details
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await httpClient.post<ReportResponse>(
|
|
93
|
+
'/api/user-reports/v1/reports',
|
|
94
|
+
requestBody
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return response
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get valid report issue options for a specific reportable type
|
|
102
|
+
*
|
|
103
|
+
* @param {ReportableType} type - The type of content being reported
|
|
104
|
+
* @param {boolean} isMobileApp - Whether this is for mobile app (includes download option)
|
|
105
|
+
* @returns {ReportIssueOption[]} Array of valid issue options with their labels
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Web options (default)
|
|
109
|
+
* const contentOptions = getReportIssueOptions('content')
|
|
110
|
+
* // Returns: [
|
|
111
|
+
* // { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
112
|
+
* // { value: 'video_issue', label: 'Video issue' },
|
|
113
|
+
* // { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
114
|
+
* // { value: 'other', label: 'Other' }
|
|
115
|
+
* // ]
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Mobile app options (includes download)
|
|
119
|
+
* const contentOptions = getReportIssueOptions('content', true)
|
|
120
|
+
* // Returns: [
|
|
121
|
+
* // { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
122
|
+
* // { value: 'video_issue', label: 'Video issue' },
|
|
123
|
+
* // { value: 'download_unavailable', label: 'Download is not available' },
|
|
124
|
+
* // { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
125
|
+
* // { value: 'other', label: 'Other' }
|
|
126
|
+
* // ]
|
|
127
|
+
*/
|
|
128
|
+
export function getReportIssueOptions(type: ReportableType, isMobileApp: boolean = false): ReportIssueOption[] {
|
|
129
|
+
switch (type) {
|
|
130
|
+
case 'forum_post':
|
|
131
|
+
return [
|
|
132
|
+
{ value: 'offensive_language', label: 'It contains offensive language or content' },
|
|
133
|
+
{ value: 'abusive', label: "It's abusive or harmful" },
|
|
134
|
+
{ value: 'personal_information', label: 'It contains personal information' },
|
|
135
|
+
{ value: 'misleading', label: "It's misleading or a false claim" },
|
|
136
|
+
{ value: 'other', label: 'Other reasons' },
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
case 'comment':
|
|
140
|
+
return [
|
|
141
|
+
{ value: 'offensive_language', label: 'It contains offensive language or content' },
|
|
142
|
+
{ value: 'abusive', label: "It's abusive or harmful" },
|
|
143
|
+
{ value: 'personal_information', label: 'It contains personal information' },
|
|
144
|
+
{ value: 'misleading', label: "It's misleading or a false claim" },
|
|
145
|
+
{ value: 'other', label: 'Other reasons' },
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
case 'content':
|
|
149
|
+
const contentOptions = [
|
|
150
|
+
{ value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
151
|
+
{ value: 'video_issue', label: 'Video issue' },
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
// Add download option only for mobile app
|
|
155
|
+
if (isMobileApp) {
|
|
156
|
+
contentOptions.push({ value: 'download_unavailable', label: 'Download is not available' })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
contentOptions.push(
|
|
160
|
+
{ value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
161
|
+
{ value: 'other', label: 'Other' }
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return contentOptions
|
|
165
|
+
|
|
166
|
+
case 'playlist':
|
|
167
|
+
const playlistOptions = [
|
|
168
|
+
{ value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
169
|
+
{ value: 'video_issue', label: 'Video issue' },
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
// Add download option only for mobile app
|
|
173
|
+
if (isMobileApp) {
|
|
174
|
+
playlistOptions.push({ value: 'download_unavailable', label: 'Download is not available' })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
playlistOptions.push(
|
|
178
|
+
{ value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
179
|
+
{ value: 'other', label: 'Other' }
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return playlistOptions
|
|
183
|
+
|
|
184
|
+
default:
|
|
185
|
+
return [
|
|
186
|
+
{ value: 'other', label: 'Other' },
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Reporting Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type of entity being reported
|
|
7
|
+
*/
|
|
8
|
+
export type ReportableType = 'content' | 'comment' | 'forum_post' | 'playlist'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Valid issue types for forum posts
|
|
12
|
+
*/
|
|
13
|
+
export type ForumIssueType = 'spam' | 'harassment' | 'inappropriate' | 'other'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Valid issue types for comments
|
|
17
|
+
*/
|
|
18
|
+
export type CommentIssueType = 'offensive_language' | 'abusive' | 'personal_information' | 'misleading' | 'other'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Valid issue types for content
|
|
22
|
+
*/
|
|
23
|
+
export type ContentIssueType = 'incorrect_metadata' | 'video_issue' | 'download_unavailable' | 'assignment_issue' | 'other'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Valid issue types for playlists
|
|
27
|
+
*/
|
|
28
|
+
export type PlaylistIssueType = 'incorrect_metadata' | 'video_issue' | 'download_unavailable' | 'assignment_issue' | 'other'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map reportable type to its valid issue types
|
|
32
|
+
*/
|
|
33
|
+
export type IssueTypeMap = {
|
|
34
|
+
forum_post: ForumIssueType
|
|
35
|
+
comment: CommentIssueType
|
|
36
|
+
content: ContentIssueType
|
|
37
|
+
playlist: PlaylistIssueType
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Response from submitting a report
|
|
42
|
+
*/
|
|
43
|
+
export interface ReportResponse {
|
|
44
|
+
/** The ID of the submitted report */
|
|
45
|
+
report_id: number
|
|
46
|
+
/** Success message */
|
|
47
|
+
message: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Report issue option
|
|
52
|
+
*/
|
|
53
|
+
export interface ReportIssueOption {
|
|
54
|
+
value: string
|
|
55
|
+
label: string
|
|
56
|
+
}
|