@mundogamernetwork/shared-ui 1.1.96 → 1.2.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/components/playtest/PlaytestBrief.vue +152 -0
- package/components/playtest/PlaytestPendingConfirmation.vue +84 -0
- package/components/playtest/PlaytestResponseFlow.vue +371 -0
- package/components/playtest/StarRatingInput.vue +105 -0
- package/components/playtest/accessibility/CriterionRow.vue +167 -0
- package/components/playtest/baseline/BaselineSectionStep.vue +106 -0
- package/components/playtest/compliance/ChecklistItemRow.vue +200 -0
- package/components/playtest/concept-poll/EmojiReactionVote.vue +145 -0
- package/components/playtest/concept-poll/FiveSecondTestVote.vue +197 -0
- package/components/playtest/concept-poll/HeadToHeadVote.vue +98 -0
- package/components/playtest/concept-poll/MultiSelectVote.vue +104 -0
- package/components/playtest/concept-poll/RankedChoiceVote.vue +169 -0
- package/components/playtest/concept-poll/SingleSelectVote.vue +98 -0
- package/components/playtest/concept-poll/StarRatingVote.vue +110 -0
- package/components/playtest/concept-poll/VoteReasonField.vue +74 -0
- package/components/playtest/lqa/FileIssueForm.vue +181 -0
- package/components/playtest/lqa/IssueList.vue +99 -0
- package/components/playtest/lqa/LocaleSelector.vue +73 -0
- package/components/playtest/moderated/JoinCallPanel.vue +135 -0
- package/components/playtest/moderated/SessionCard.vue +146 -0
- package/composables/usePlaytestResponse.ts +446 -0
- package/nuxt.config.ts +42 -4
- package/package.json +10 -3
- package/services/playtestTesterService.ts +416 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import httpService from './httpService'
|
|
2
|
+
|
|
3
|
+
// Playtest tester-facing service — every endpoint under /playtest/* consumed
|
|
4
|
+
// by the tester-facing response-taking flows (baseline, concept polls,
|
|
5
|
+
// localization QA, accessibility audits, compliance checklists, moderated
|
|
6
|
+
// sessions). Mirrors keyService.ts's flat function-export style: thin
|
|
7
|
+
// wrappers over httpService (cookie-auth axios instance, no manual token
|
|
8
|
+
// plumbing — see services/httpService.ts).
|
|
9
|
+
//
|
|
10
|
+
// All routes verified directly against api-main's PlaytestProvider route
|
|
11
|
+
// registration and Request/Controller classes this session (not assumed from
|
|
12
|
+
// the plan doc) — see docs/playtest/00-feature-map.md in api-main.
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type PlaytestEngine = 'quick' | 'deep'
|
|
19
|
+
|
|
20
|
+
export interface PlaytestFormSectionMeta {
|
|
21
|
+
label: string
|
|
22
|
+
engines: PlaytestEngine[]
|
|
23
|
+
scale: boolean
|
|
24
|
+
text: 'none' | 'optional' | 'required'
|
|
25
|
+
default: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlaytestCatalog {
|
|
29
|
+
engines: PlaytestEngine[]
|
|
30
|
+
sections: Record<string, PlaytestFormSectionMeta>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PlaytestCampaign {
|
|
34
|
+
id: number
|
|
35
|
+
user_id: number
|
|
36
|
+
title: string
|
|
37
|
+
game_ref: string | null
|
|
38
|
+
engine: PlaytestEngine
|
|
39
|
+
category: 'concept_poll' | 'localization_qa' | 'accessibility_audit' | null
|
|
40
|
+
status: string
|
|
41
|
+
enabled_sections: string[]
|
|
42
|
+
audience: Record<string, any> | null
|
|
43
|
+
accessibility_criteria: string[] | null
|
|
44
|
+
targeting_level: 'open' | 'matched_only' | 'basic' | string
|
|
45
|
+
responses_requested: number
|
|
46
|
+
responses_accepted: number
|
|
47
|
+
reward_type: 'mgc' | 'cash'
|
|
48
|
+
reward_amount: string | number
|
|
49
|
+
reward_currency: string | null
|
|
50
|
+
closes_at: string | null
|
|
51
|
+
eligible?: boolean
|
|
52
|
+
ineligibility_reason?: string | null
|
|
53
|
+
[key: string]: any
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PlaytestAnswerEntry {
|
|
57
|
+
rating?: number
|
|
58
|
+
text?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SubmitBaselineResponsePayload {
|
|
62
|
+
answers: Record<string, PlaytestAnswerEntry>
|
|
63
|
+
time_on_task_seconds?: number
|
|
64
|
+
device?: string
|
|
65
|
+
os?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PlaytestResponse {
|
|
69
|
+
id: number
|
|
70
|
+
playtest_campaign_id: number
|
|
71
|
+
user_id: number
|
|
72
|
+
status: 'submitted' | 'accepted' | 'rejected'
|
|
73
|
+
answers: Record<string, PlaytestAnswerEntry> | null
|
|
74
|
+
quality_score: number | null
|
|
75
|
+
time_on_task_seconds: number | null
|
|
76
|
+
device: string | null
|
|
77
|
+
os: string | null
|
|
78
|
+
reward_status: string | null
|
|
79
|
+
target_locale: string | null
|
|
80
|
+
campaign?: PlaytestCampaign
|
|
81
|
+
payout?: any
|
|
82
|
+
[key: string]: any
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ConceptPollType =
|
|
86
|
+
| 'head_to_head'
|
|
87
|
+
| 'single_select'
|
|
88
|
+
| 'multi_select'
|
|
89
|
+
| 'ranked_choice'
|
|
90
|
+
| 'five_second_test'
|
|
91
|
+
| 'star_rating'
|
|
92
|
+
| 'emoji_reaction'
|
|
93
|
+
|
|
94
|
+
export interface PlaytestConceptPollOption {
|
|
95
|
+
id: number
|
|
96
|
+
playtest_concept_poll_id: number
|
|
97
|
+
label: string
|
|
98
|
+
value: string | null
|
|
99
|
+
media_url: string | null
|
|
100
|
+
position: number
|
|
101
|
+
votes: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PlaytestConceptPoll {
|
|
105
|
+
id: number
|
|
106
|
+
playtest_campaign_id: number
|
|
107
|
+
poll_type: ConceptPollType
|
|
108
|
+
asset_kind: string | null
|
|
109
|
+
question: string
|
|
110
|
+
description: string | null
|
|
111
|
+
status: string
|
|
112
|
+
closes_at: string | null
|
|
113
|
+
options: PlaytestConceptPollOption[]
|
|
114
|
+
campaign?: PlaytestCampaign
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// `answer` shape strictly depends on poll_type — see VoteConceptPollRequest.php,
|
|
118
|
+
// verified this session. `reason` is UNCONDITIONALLY required regardless of
|
|
119
|
+
// poll_type (min 3 chars) — a PickFu-style convention, not optional anywhere.
|
|
120
|
+
export type ConceptPollAnswer =
|
|
121
|
+
| { option_id: number } // head_to_head | single_select
|
|
122
|
+
| { option_id: number; recall_text: string } // five_second_test
|
|
123
|
+
| { option_id: number; stars: number } // star_rating (1-5)
|
|
124
|
+
| { option_id: number; emoji: string } // emoji_reaction
|
|
125
|
+
| { option_ids: number[] } // multi_select
|
|
126
|
+
| { ranking: number[] } // ranked_choice
|
|
127
|
+
|
|
128
|
+
export interface VoteConceptPollPayload {
|
|
129
|
+
answer: ConceptPollAnswer
|
|
130
|
+
reason: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface PlaytestCampaignLocale {
|
|
134
|
+
id: number
|
|
135
|
+
playtest_campaign_id: number
|
|
136
|
+
locale: string
|
|
137
|
+
build_reference: string | null
|
|
138
|
+
responses_target: number
|
|
139
|
+
responses_received: number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type LocalizationIssueType =
|
|
143
|
+
| 'mistranslation'
|
|
144
|
+
| 'text_truncation'
|
|
145
|
+
| 'missing_string'
|
|
146
|
+
| 'cultural_inappropriateness'
|
|
147
|
+
| 'audio_subtitle_desync'
|
|
148
|
+
| 'untranslated_string'
|
|
149
|
+
| 'grammar_punctuation'
|
|
150
|
+
| 'inconsistent_terminology'
|
|
151
|
+
|
|
152
|
+
export type LocalizationIssueSeverity = 'blocker' | 'critical' | 'major' | 'minor' | 'cosmetic'
|
|
153
|
+
|
|
154
|
+
export interface StoreLocalizationIssuePayload {
|
|
155
|
+
campaign_locale_id: number
|
|
156
|
+
issue_type: LocalizationIssueType
|
|
157
|
+
severity: LocalizationIssueSeverity
|
|
158
|
+
location?: string | null
|
|
159
|
+
description: string
|
|
160
|
+
expected_text?: string | null
|
|
161
|
+
actual_text?: string | null
|
|
162
|
+
media_url?: string | null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface PlaytestLocalizationIssue extends StoreLocalizationIssuePayload {
|
|
166
|
+
id: number
|
|
167
|
+
playtest_campaign_id: number
|
|
168
|
+
playtest_response_id: number
|
|
169
|
+
user_id: number
|
|
170
|
+
status: string
|
|
171
|
+
created_at: string
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PlaytestAccessibilityCriterionMeta {
|
|
175
|
+
label: string
|
|
176
|
+
accommodation: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type AccessibilityCriteriaCatalog = Record<string, PlaytestAccessibilityCriterionMeta>
|
|
180
|
+
|
|
181
|
+
export type AccessibilityEvaluationResult = 'pass' | 'fail' | 'partial' | 'not_applicable'
|
|
182
|
+
|
|
183
|
+
export interface AccessibilityEvaluationEntry {
|
|
184
|
+
criterion: string
|
|
185
|
+
result: AccessibilityEvaluationResult
|
|
186
|
+
notes?: string | null
|
|
187
|
+
tester_uses_accommodation?: boolean
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface SubmitAccessibilityEvaluationsPayload {
|
|
191
|
+
evaluations: AccessibilityEvaluationEntry[]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface PlaytestComplianceChecklistItem {
|
|
195
|
+
key: string
|
|
196
|
+
label: string
|
|
197
|
+
description?: string | null
|
|
198
|
+
severity?: string | null
|
|
199
|
+
evidence_required?: boolean
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface PlaytestComplianceChecklist {
|
|
203
|
+
id: number
|
|
204
|
+
playtest_campaign_id: number
|
|
205
|
+
name: string
|
|
206
|
+
category: string
|
|
207
|
+
items: PlaytestComplianceChecklistItem[]
|
|
208
|
+
campaign?: PlaytestCampaign
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type ComplianceResultStatus = 'pass' | 'fail' | 'not_applicable'
|
|
212
|
+
|
|
213
|
+
export interface ComplianceResultEntry {
|
|
214
|
+
key: string
|
|
215
|
+
status: ComplianceResultStatus
|
|
216
|
+
notes?: string | null
|
|
217
|
+
evidence_url?: string | null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// This endpoint FULLY REPLACES the whole `results` array server-side on every
|
|
221
|
+
// call (SubmitComplianceResultRequest + PlaytestComplianceRepository::submitResult
|
|
222
|
+
// upsert the checklist's full result set) — callers must always send the
|
|
223
|
+
// complete accumulated array, never a diff, or previously-completed items are
|
|
224
|
+
// silently dropped. Enforced client-side by usePlaytestResponse's accumulator.
|
|
225
|
+
export interface SubmitComplianceResultPayload {
|
|
226
|
+
results: ComplianceResultEntry[]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface PlaytestModeratedSlot {
|
|
230
|
+
id: number
|
|
231
|
+
playtest_campaign_id: number
|
|
232
|
+
moderator_id: number
|
|
233
|
+
moderator_type: 'studio' | 'mgn_staff'
|
|
234
|
+
starts_at: string
|
|
235
|
+
duration_minutes: number
|
|
236
|
+
is_booked: boolean
|
|
237
|
+
notes: string | null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type PlaytestModeratedSessionStatus = 'scheduled' | 'completed' | 'no_show' | 'cancelled'
|
|
241
|
+
|
|
242
|
+
export interface PlaytestModeratedSession {
|
|
243
|
+
id: number
|
|
244
|
+
playtest_campaign_id: number
|
|
245
|
+
playtest_moderated_slot_id: number
|
|
246
|
+
tester_id: number
|
|
247
|
+
moderator_id: number
|
|
248
|
+
moderator_type: 'studio' | 'mgn_staff'
|
|
249
|
+
meeting_id: number | null
|
|
250
|
+
scheduled_at: string
|
|
251
|
+
duration_minutes: number
|
|
252
|
+
status: PlaytestModeratedSessionStatus
|
|
253
|
+
playtest_response_id: number | null
|
|
254
|
+
moderator_summary: string | null
|
|
255
|
+
recording_url: string | null
|
|
256
|
+
completed_at: string | null
|
|
257
|
+
cancelled_at: string | null
|
|
258
|
+
cancellation_reason: string | null
|
|
259
|
+
tester_confirmed_at: string | null
|
|
260
|
+
campaign?: PlaytestCampaign
|
|
261
|
+
slot?: PlaytestModeratedSlot
|
|
262
|
+
// Eager-loaded by PlaytestModeratedSessionRepository::findWithRelations()
|
|
263
|
+
// (used by GET /playtest/moderated-sessions/{id}) — meeting_link is the
|
|
264
|
+
// actual join-call URL (Meeting model, app/Models/Meeting.php).
|
|
265
|
+
meeting?: { id: number; meeting_link: string | null; meeting_provider_id: string | null; platform: string | null } | null
|
|
266
|
+
[key: string]: any
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface UploadPlaytestMediaResult {
|
|
270
|
+
data: { url: string }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface PaginatedResponse<T> {
|
|
274
|
+
data: T[]
|
|
275
|
+
meta?: { pagination?: Record<string, any> }
|
|
276
|
+
current_page?: number
|
|
277
|
+
last_page?: number
|
|
278
|
+
total?: number
|
|
279
|
+
[key: string]: any
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Catalog / discovery
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/** Public: engines + modular baseline section catalog. */
|
|
287
|
+
export const fetchPlaytestCatalog = () =>
|
|
288
|
+
httpService.get<PlaytestCatalog>('/playtest/catalog')
|
|
289
|
+
|
|
290
|
+
/** Authenticated: active campaigns open to the tester, with eligibility. */
|
|
291
|
+
export const fetchAvailablePlaytestCampaigns = () =>
|
|
292
|
+
httpService.get<PlaytestCampaign[]>('/playtest/available')
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Baseline (quick/deep, category=null)
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Submit (or, on repeat call, no-op — firstOrCreate is idempotent server-side)
|
|
300
|
+
* a baseline response. Creates the underlying PlaytestResponse lazily.
|
|
301
|
+
*/
|
|
302
|
+
export const submitPlaytestResponse = (campaignId: number, payload: SubmitBaselineResponsePayload) =>
|
|
303
|
+
httpService.post<PlaytestResponse>(`/playtest/campaigns/${campaignId}/responses`, payload)
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Concept polls (category=concept_poll, engine=quick)
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
export const fetchConceptPoll = (pollId: number) =>
|
|
310
|
+
httpService.get<PlaytestConceptPoll>(`/playtest/concept-polls/${pollId}`)
|
|
311
|
+
|
|
312
|
+
/** reason is unconditionally required (min 3 chars) for every poll_type. */
|
|
313
|
+
export const voteConceptPoll = (pollId: number, payload: VoteConceptPollPayload) =>
|
|
314
|
+
httpService.post(`/playtest/concept-polls/${pollId}/vote`, payload)
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Localization QA (category=localization_qa)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
export const fetchCampaignLocales = (campaignId: number) =>
|
|
321
|
+
httpService.get<PlaytestCampaignLocale[]>(`/playtest/campaigns/${campaignId}/locales`)
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Files a localization defect. The first issue filed on a campaign lazily
|
|
325
|
+
* creates the tester's PlaytestResponse server-side — no separate "start"
|
|
326
|
+
* call exists or is needed.
|
|
327
|
+
*/
|
|
328
|
+
export const storeLocalizationIssue = (campaignId: number, payload: StoreLocalizationIssuePayload) =>
|
|
329
|
+
httpService.post<PlaytestLocalizationIssue>(`/playtest/campaigns/${campaignId}/localization-issues`, payload)
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Accessibility audits (category=accessibility_audit)
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
/** Public: fixed catalog of 7 accessibility criteria. */
|
|
336
|
+
export const fetchAccessibilityCriteriaCatalog = () =>
|
|
337
|
+
httpService.get<AccessibilityCriteriaCatalog>('/playtest/accessibility-criteria')
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Batch-submits criterion evaluations. Server-side this upserts per-criterion
|
|
341
|
+
* (safe to auto-save each criterion as completed as the tester progresses,
|
|
342
|
+
* not just at the end) — unlike compliance results below, this is NOT a
|
|
343
|
+
* full-replace endpoint.
|
|
344
|
+
*/
|
|
345
|
+
export const submitAccessibilityEvaluations = (campaignId: number, payload: SubmitAccessibilityEvaluationsPayload) =>
|
|
346
|
+
httpService.post(`/playtest/campaigns/${campaignId}/accessibility-evaluations`, payload)
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Compliance / technical QA checklists (capability layered on any campaign)
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
export const fetchComplianceChecklists = (campaignId: number) =>
|
|
353
|
+
httpService.get<PlaytestComplianceChecklist[]>(`/playtest/campaigns/${campaignId}/compliance-checklists`)
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Submits results for one checklist. IMPORTANT: this fully REPLACES the whole
|
|
357
|
+
* `results` array server-side on every call — always send the complete
|
|
358
|
+
* accumulated array (every item answered so far), never a diff, or
|
|
359
|
+
* previously-completed items are silently dropped.
|
|
360
|
+
*/
|
|
361
|
+
export const submitComplianceResult = (checklistId: number, payload: SubmitComplianceResultPayload) =>
|
|
362
|
+
httpService.post(`/playtest/compliance-checklists/${checklistId}/results`, payload)
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Moderated / live sessions
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
export const fetchAvailableModeratedSlots = (campaignId: number) =>
|
|
369
|
+
httpService.get<PlaytestModeratedSlot[]>(`/playtest/campaigns/${campaignId}/moderated-slots/available`)
|
|
370
|
+
|
|
371
|
+
export const claimModeratedSlot = (slotId: number) =>
|
|
372
|
+
httpService.post<PlaytestModeratedSession>(`/playtest/moderated-slots/${slotId}/claim`)
|
|
373
|
+
|
|
374
|
+
export const fetchMyModeratedSessions = () =>
|
|
375
|
+
httpService.get<PlaytestModeratedSession[]>('/playtest/my-moderated-sessions')
|
|
376
|
+
|
|
377
|
+
export const fetchModeratedSession = (sessionId: number) =>
|
|
378
|
+
httpService.get<PlaytestModeratedSession>(`/playtest/moderated-sessions/${sessionId}`)
|
|
379
|
+
|
|
380
|
+
/** Additive tester signal (sets tester_confirmed_at) — does not change `status`. */
|
|
381
|
+
export const confirmModeratedSession = (sessionId: number) =>
|
|
382
|
+
httpService.post<PlaytestModeratedSession>(`/playtest/moderated-sessions/${sessionId}/confirm`)
|
|
383
|
+
|
|
384
|
+
export const markModeratedSessionNoShow = (sessionId: number) =>
|
|
385
|
+
httpService.post<PlaytestModeratedSession>(`/playtest/moderated-sessions/${sessionId}/no-show`)
|
|
386
|
+
|
|
387
|
+
export const cancelModeratedSession = (sessionId: number, cancellationReason?: string) =>
|
|
388
|
+
httpService.post<PlaytestModeratedSession>(`/playtest/moderated-sessions/${sessionId}/cancel`, {
|
|
389
|
+
cancellation_reason: cancellationReason ?? null,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Media upload (LQA media_url / compliance evidence_url)
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Generic tester media upload — jpg/jpeg/gif/png/webp/mp4/mov/webm up to
|
|
398
|
+
* 20MB. Returns {data:{url}}. Use the resulting url as LQA's media_url or
|
|
399
|
+
* compliance's evidence_url.
|
|
400
|
+
*/
|
|
401
|
+
export const uploadPlaytestMedia = (file: File) => {
|
|
402
|
+
const form = new FormData()
|
|
403
|
+
form.append('file', file)
|
|
404
|
+
return httpService.post<UploadPlaytestMediaResult>('/playtest/media', form, {
|
|
405
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// My responses (tester-scoped; consuming app builds the actual list UI)
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
export const fetchMyPlaytestResponses = (status?: 'submitted' | 'accepted' | 'rejected', perPage = 20, page = 1) =>
|
|
414
|
+
httpService.get<PaginatedResponse<PlaytestResponse>>('/playtest/my-responses', {
|
|
415
|
+
params: { status, per_page: perPage, page },
|
|
416
|
+
})
|