@codingfactory/socialkit-vue 0.6.0 → 0.7.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.
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/services/gamification.d.ts +87 -0
- package/dist/services/gamification.d.ts.map +1 -0
- package/dist/services/gamification.js +263 -0
- package/dist/services/gamification.js.map +1 -0
- package/dist/stores/gamification.d.ts +2875 -0
- package/dist/stores/gamification.d.ts.map +1 -0
- package/dist/stores/gamification.js +1136 -0
- package/dist/stores/gamification.js.map +1 -0
- package/dist/types/api.d.ts +1 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/gamification.d.ts +267 -0
- package/dist/types/gamification.d.ts.map +1 -0
- package/dist/types/gamification.js +5 -0
- package/dist/types/gamification.js.map +1 -0
- package/dist/types/reputation.d.ts +55 -0
- package/dist/types/reputation.d.ts.map +1 -0
- package/dist/types/reputation.js +5 -0
- package/dist/types/reputation.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +65 -0
- package/src/services/gamification.ts +432 -0
- package/src/stores/gamification.ts +1565 -0
- package/src/types/api.ts +1 -0
- package/src/types/gamification.ts +286 -0
- package/src/types/reputation.ts +78 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable gamification service for SocialKit-powered frontends.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ApiService } from './api.js'
|
|
6
|
+
import type {
|
|
7
|
+
Achievement,
|
|
8
|
+
GamificationNotification,
|
|
9
|
+
LeaderboardEntry,
|
|
10
|
+
PointTransaction,
|
|
11
|
+
Quest,
|
|
12
|
+
Reward,
|
|
13
|
+
UserStats,
|
|
14
|
+
} from '../types/gamification.js'
|
|
15
|
+
import type {
|
|
16
|
+
ReputationPrivilege,
|
|
17
|
+
ReputationSnapshot,
|
|
18
|
+
UserStreakStatus,
|
|
19
|
+
} from '../types/reputation.js'
|
|
20
|
+
|
|
21
|
+
type LeaderboardPeriodInput = 'daily' | 'weekly' | 'monthly' | 'all-time' | 'all_time'
|
|
22
|
+
|
|
23
|
+
interface CursorCollectionResponse<TItem> {
|
|
24
|
+
data: TItem[]
|
|
25
|
+
next_cursor?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface QuestCompletionResponse {
|
|
29
|
+
quest_id: string
|
|
30
|
+
completed_at: string
|
|
31
|
+
points_awarded: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GamificationWebSocketMessageEventLike {
|
|
35
|
+
data: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GamificationWebSocketLike {
|
|
39
|
+
readonly OPEN: number
|
|
40
|
+
readyState: number
|
|
41
|
+
onopen: ((event: unknown) => void) | null
|
|
42
|
+
onmessage: ((event: GamificationWebSocketMessageEventLike) => void) | null
|
|
43
|
+
onclose: ((event: unknown) => void) | null
|
|
44
|
+
onerror: ((event: unknown) => void) | null
|
|
45
|
+
close(): void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type GamificationWebSocketFactory = (url: string) => GamificationWebSocketLike
|
|
49
|
+
|
|
50
|
+
export interface GamificationServiceLogger {
|
|
51
|
+
debug: (...args: unknown[]) => void
|
|
52
|
+
error: (...args: unknown[]) => void
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface GamificationServiceConfig {
|
|
56
|
+
client: ApiService
|
|
57
|
+
webSocketUrl?: string | null
|
|
58
|
+
webSocketFactory?: GamificationWebSocketFactory
|
|
59
|
+
reconnectIntervalMs?: number
|
|
60
|
+
logger?: GamificationServiceLogger
|
|
61
|
+
debug?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GamificationServiceInstance {
|
|
65
|
+
getUserStats(userId?: string): Promise<UserStats>
|
|
66
|
+
getUserReputation(userId?: string): Promise<UserStats>
|
|
67
|
+
getUserStreak(): Promise<UserStreakStatus>
|
|
68
|
+
getTransactions(options?: { limit?: number; offset?: number }): Promise<PointTransaction[]>
|
|
69
|
+
getReputationSnapshots(options?: { period?: 'week' | 'month' | 'year' }): Promise<ReputationSnapshot[]>
|
|
70
|
+
getPrivilegesCatalog(): Promise<ReputationPrivilege[]>
|
|
71
|
+
getAchievements(options?: { status?: 'locked' | 'unlocked' | 'all'; page?: number }): Promise<CursorCollectionResponse<Achievement>>
|
|
72
|
+
claimAchievement(achievementId: string): Promise<Achievement>
|
|
73
|
+
getQuests(options?: { status?: 'available' | 'in_progress' | 'completed' | 'all'; page?: number }): Promise<CursorCollectionResponse<Quest>>
|
|
74
|
+
claimQuestReward(questId: string): Promise<QuestCompletionResponse>
|
|
75
|
+
abandonQuest(questId: string): Promise<void>
|
|
76
|
+
getLeaderboard(options?: {
|
|
77
|
+
period?: LeaderboardPeriodInput
|
|
78
|
+
limit?: number
|
|
79
|
+
offset?: number
|
|
80
|
+
spaceId?: string
|
|
81
|
+
}): Promise<LeaderboardEntry[]>
|
|
82
|
+
getUserRank(): Promise<{ rank: number; percentile: number }>
|
|
83
|
+
getRewards(options?: {
|
|
84
|
+
status?: 'available' | 'redeemed' | 'all'
|
|
85
|
+
page?: number
|
|
86
|
+
}): Promise<CursorCollectionResponse<Reward>>
|
|
87
|
+
redeemReward(rewardId: string): Promise<Reward>
|
|
88
|
+
connectWebSocket(): GamificationWebSocketLike | null
|
|
89
|
+
disconnectWebSocket(): void
|
|
90
|
+
subscribe(event: string, callback: (data: GamificationNotification) => void): void
|
|
91
|
+
unsubscribe(event: string, callback: (data: GamificationNotification) => void): void
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const createQuestCompletionIdempotencyKey = (questId: string): string => {
|
|
95
|
+
const randomPart = typeof globalThis.crypto?.randomUUID === 'function'
|
|
96
|
+
? globalThis.crypto.randomUUID()
|
|
97
|
+
: Math.random().toString(36).slice(2)
|
|
98
|
+
|
|
99
|
+
return `quest-complete:${questId}:${randomPart}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createDefaultWebSocket(url: string): GamificationWebSocketLike {
|
|
103
|
+
if (typeof WebSocket === 'undefined') {
|
|
104
|
+
throw new Error('WebSocket is unavailable in this environment')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return new WebSocket(url) as unknown as GamificationWebSocketLike
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class GamificationService implements GamificationServiceInstance {
|
|
111
|
+
private ws: GamificationWebSocketLike | null = null
|
|
112
|
+
private reconnectInterval: ReturnType<typeof setInterval> | null = null
|
|
113
|
+
private listeners: Map<string, Set<(data: GamificationNotification) => void>> = new Map()
|
|
114
|
+
|
|
115
|
+
public constructor(
|
|
116
|
+
private readonly client: ApiService,
|
|
117
|
+
private readonly webSocketUrl: string | null,
|
|
118
|
+
private readonly webSocketFactory: GamificationWebSocketFactory,
|
|
119
|
+
private readonly reconnectIntervalMs: number,
|
|
120
|
+
private readonly logger: GamificationServiceLogger,
|
|
121
|
+
private readonly debug: boolean,
|
|
122
|
+
) {}
|
|
123
|
+
|
|
124
|
+
public async getUserStats(userId?: string): Promise<UserStats> {
|
|
125
|
+
const endpoint = userId
|
|
126
|
+
? `/v1/gamification/users/${userId}/reputation`
|
|
127
|
+
: '/v1/gamification/users/me/reputation'
|
|
128
|
+
const response = await this.client.get<UserStats>(endpoint)
|
|
129
|
+
return response.data
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async getUserReputation(userId?: string): Promise<UserStats> {
|
|
133
|
+
return this.getUserStats(userId)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async getUserStreak(): Promise<UserStreakStatus> {
|
|
137
|
+
const response = await this.client.get<UserStreakStatus>('/v1/gamification/users/me/streak')
|
|
138
|
+
return response.data
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public async getTransactions(options?: {
|
|
142
|
+
limit?: number
|
|
143
|
+
offset?: number
|
|
144
|
+
}): Promise<PointTransaction[]> {
|
|
145
|
+
const params = new URLSearchParams()
|
|
146
|
+
if (options?.limit) {
|
|
147
|
+
params.append('limit', options.limit.toString())
|
|
148
|
+
}
|
|
149
|
+
if (options?.offset) {
|
|
150
|
+
params.append('offset', options.offset.toString())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const query = params.toString()
|
|
154
|
+
const url = query.length > 0
|
|
155
|
+
? `/v1/gamification/users/me/transactions?${query}`
|
|
156
|
+
: '/v1/gamification/users/me/transactions'
|
|
157
|
+
|
|
158
|
+
const response = await this.client.get<PointTransaction[]>(url)
|
|
159
|
+
return response.data
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public async getReputationSnapshots(options?: {
|
|
163
|
+
period?: 'week' | 'month' | 'year'
|
|
164
|
+
}): Promise<ReputationSnapshot[]> {
|
|
165
|
+
const params = new URLSearchParams()
|
|
166
|
+
if (options?.period) {
|
|
167
|
+
params.append('period', options.period)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const query = params.toString()
|
|
171
|
+
const url = query.length > 0
|
|
172
|
+
? `/v1/gamification/users/me/reputation/snapshots?${query}`
|
|
173
|
+
: '/v1/gamification/users/me/reputation/snapshots'
|
|
174
|
+
|
|
175
|
+
const response = await this.client.get<ReputationSnapshot[]>(url)
|
|
176
|
+
return response.data
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public async getPrivilegesCatalog(): Promise<ReputationPrivilege[]> {
|
|
180
|
+
const response = await this.client.get<ReputationPrivilege[]>('/v1/gamification/privileges')
|
|
181
|
+
return response.data
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async getAchievements(options?: {
|
|
185
|
+
status?: 'locked' | 'unlocked' | 'all'
|
|
186
|
+
page?: number
|
|
187
|
+
}): Promise<CursorCollectionResponse<Achievement>> {
|
|
188
|
+
const params = new URLSearchParams()
|
|
189
|
+
if (options?.status && options.status !== 'all') {
|
|
190
|
+
params.append('status', options.status)
|
|
191
|
+
}
|
|
192
|
+
if (options?.page) {
|
|
193
|
+
params.append('page', options.page.toString())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const response = await this.client.get<CursorCollectionResponse<Achievement>>(
|
|
197
|
+
`/v1/gamification/achievements?${params.toString()}`
|
|
198
|
+
)
|
|
199
|
+
return response.data
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public async claimAchievement(achievementId: string): Promise<Achievement> {
|
|
203
|
+
const response = await this.client.post<Achievement>(`/v1/gamification/achievements/${achievementId}/claim`)
|
|
204
|
+
return response.data
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public async getQuests(options?: {
|
|
208
|
+
status?: 'available' | 'in_progress' | 'completed' | 'all'
|
|
209
|
+
page?: number
|
|
210
|
+
}): Promise<CursorCollectionResponse<Quest>> {
|
|
211
|
+
const params = new URLSearchParams()
|
|
212
|
+
if (options?.status && options.status !== 'all') {
|
|
213
|
+
params.append('status', options.status)
|
|
214
|
+
}
|
|
215
|
+
if (options?.page) {
|
|
216
|
+
params.append('page', options.page.toString())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const endpoint = options?.status === 'available'
|
|
220
|
+
? '/v1/gamification/quests/available'
|
|
221
|
+
: '/v1/gamification/quests/my-quests'
|
|
222
|
+
const response = await this.client.get<CursorCollectionResponse<Quest>>(`${endpoint}?${params.toString()}`)
|
|
223
|
+
return response.data
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public async claimQuestReward(questId: string): Promise<QuestCompletionResponse> {
|
|
227
|
+
const idempotencyKey = createQuestCompletionIdempotencyKey(questId)
|
|
228
|
+
const response = await this.client.post<QuestCompletionResponse>(
|
|
229
|
+
`/v1/gamification/quests/${questId}/complete`,
|
|
230
|
+
undefined,
|
|
231
|
+
{
|
|
232
|
+
headers: {
|
|
233
|
+
'Idempotency-Key': idempotencyKey,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
return response.data
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public async abandonQuest(questId: string): Promise<void> {
|
|
241
|
+
await this.client.delete(`/v1/gamification/quests/${questId}/abandon`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public async getLeaderboard(options?: {
|
|
245
|
+
period?: LeaderboardPeriodInput
|
|
246
|
+
limit?: number
|
|
247
|
+
offset?: number
|
|
248
|
+
spaceId?: string
|
|
249
|
+
}): Promise<LeaderboardEntry[]> {
|
|
250
|
+
const params = new URLSearchParams()
|
|
251
|
+
if (options?.period) {
|
|
252
|
+
const normalizedPeriod = options.period === 'all_time' ? 'all-time' : options.period
|
|
253
|
+
params.append('period', normalizedPeriod)
|
|
254
|
+
}
|
|
255
|
+
if (options?.limit) {
|
|
256
|
+
params.append('limit', options.limit.toString())
|
|
257
|
+
}
|
|
258
|
+
if (options?.offset) {
|
|
259
|
+
params.append('offset', options.offset.toString())
|
|
260
|
+
}
|
|
261
|
+
if (options?.spaceId) {
|
|
262
|
+
params.append('space_id', options.spaceId)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const query = params.toString()
|
|
266
|
+
const url = query.length > 0
|
|
267
|
+
? `/v1/gamification/leaderboard?${query}`
|
|
268
|
+
: '/v1/gamification/leaderboard'
|
|
269
|
+
|
|
270
|
+
const response = await this.client.get<LeaderboardEntry[]>(url)
|
|
271
|
+
return response.data
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public async getUserRank(): Promise<{ rank: number; percentile: number }> {
|
|
275
|
+
const response = await this.client.get<{ rank: number; percentile: number }>('/v1/gamification/users/me/rank')
|
|
276
|
+
return response.data
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public async getRewards(options?: {
|
|
280
|
+
status?: 'available' | 'redeemed' | 'all'
|
|
281
|
+
page?: number
|
|
282
|
+
}): Promise<CursorCollectionResponse<Reward>> {
|
|
283
|
+
const params = new URLSearchParams()
|
|
284
|
+
if (options?.status && options.status !== 'all') {
|
|
285
|
+
params.append('status', options.status)
|
|
286
|
+
}
|
|
287
|
+
if (options?.page) {
|
|
288
|
+
params.append('page', options.page.toString())
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const query = params.toString()
|
|
292
|
+
const endpoint = query.length > 0
|
|
293
|
+
? `/v1/gamification/rewards?${query}`
|
|
294
|
+
: '/v1/gamification/rewards'
|
|
295
|
+
|
|
296
|
+
const response = await this.client.get<Reward[]>(endpoint)
|
|
297
|
+
return { data: response.data }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public async redeemReward(rewardId: string): Promise<Reward> {
|
|
301
|
+
const response = await this.client.post<Reward>(`/v1/gamification/rewards/${rewardId}/redeem`)
|
|
302
|
+
return response.data
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public connectWebSocket(): GamificationWebSocketLike | null {
|
|
306
|
+
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
|
307
|
+
return this.ws
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const socketUrl = typeof this.webSocketUrl === 'string' && this.webSocketUrl.length > 0
|
|
311
|
+
? this.webSocketUrl
|
|
312
|
+
: null
|
|
313
|
+
if (socketUrl === null) {
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
this.ws = this.webSocketFactory(`${socketUrl}/gamification`)
|
|
319
|
+
|
|
320
|
+
this.ws.onopen = () => {
|
|
321
|
+
this.debugLog('Gamification WebSocket connected')
|
|
322
|
+
if (this.reconnectInterval) {
|
|
323
|
+
clearInterval(this.reconnectInterval)
|
|
324
|
+
this.reconnectInterval = null
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.ws.onmessage = (event) => {
|
|
329
|
+
try {
|
|
330
|
+
const payload = typeof event.data === 'string'
|
|
331
|
+
? JSON.parse(event.data)
|
|
332
|
+
: event.data
|
|
333
|
+
this.handleWebSocketMessage(payload as GamificationNotification)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.logger.error('Failed to parse WebSocket message:', error)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.ws.onclose = () => {
|
|
340
|
+
this.debugLog('Gamification WebSocket disconnected')
|
|
341
|
+
this.attemptReconnect()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.ws.onerror = (error) => {
|
|
345
|
+
this.logger.error('Gamification WebSocket error:', error)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return this.ws
|
|
349
|
+
} catch (error) {
|
|
350
|
+
this.logger.error('Failed to connect WebSocket:', error)
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public disconnectWebSocket(): void {
|
|
356
|
+
if (this.ws) {
|
|
357
|
+
this.ws.close()
|
|
358
|
+
this.ws = null
|
|
359
|
+
}
|
|
360
|
+
if (this.reconnectInterval) {
|
|
361
|
+
clearInterval(this.reconnectInterval)
|
|
362
|
+
this.reconnectInterval = null
|
|
363
|
+
}
|
|
364
|
+
this.listeners.clear()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public subscribe(event: string, callback: (data: GamificationNotification) => void): void {
|
|
368
|
+
if (!this.listeners.has(event)) {
|
|
369
|
+
this.listeners.set(event, new Set())
|
|
370
|
+
}
|
|
371
|
+
this.listeners.get(event)?.add(callback)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public unsubscribe(event: string, callback: (data: GamificationNotification) => void): void {
|
|
375
|
+
this.listeners.get(event)?.delete(callback)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private debugLog(message: string, ...args: unknown[]): void {
|
|
379
|
+
if (!this.debug) {
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this.logger.debug(message, ...args)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private attemptReconnect(): void {
|
|
387
|
+
if (this.reconnectInterval) {
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.reconnectInterval = setInterval(() => {
|
|
392
|
+
this.debugLog('Gamification WebSocket reconnect attempt')
|
|
393
|
+
this.connectWebSocket()
|
|
394
|
+
}, this.reconnectIntervalMs)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private handleWebSocketMessage(data: GamificationNotification): void {
|
|
398
|
+
const listeners = this.listeners.get(data.type)
|
|
399
|
+
if (listeners) {
|
|
400
|
+
listeners.forEach((listener) => {
|
|
401
|
+
listener(data)
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const updateListeners = this.listeners.get('update')
|
|
406
|
+
if (updateListeners) {
|
|
407
|
+
updateListeners.forEach((listener) => {
|
|
408
|
+
listener(data)
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function createGamificationService(config: GamificationServiceConfig): GamificationServiceInstance {
|
|
415
|
+
const {
|
|
416
|
+
client,
|
|
417
|
+
webSocketUrl = null,
|
|
418
|
+
webSocketFactory = createDefaultWebSocket,
|
|
419
|
+
reconnectIntervalMs = 5000,
|
|
420
|
+
logger = console,
|
|
421
|
+
debug = false,
|
|
422
|
+
} = config
|
|
423
|
+
|
|
424
|
+
return new GamificationService(
|
|
425
|
+
client,
|
|
426
|
+
webSocketUrl,
|
|
427
|
+
webSocketFactory,
|
|
428
|
+
reconnectIntervalMs,
|
|
429
|
+
logger,
|
|
430
|
+
debug,
|
|
431
|
+
)
|
|
432
|
+
}
|