Refactor project structure and integrate Redux for state management. Update configuration files for new project name and add Webpack plugins for environment variables. Implement user authentication with Keycloak and create a context for challenge management. Add various components for user interaction, including dashboards and task workspaces. Enhance API integration and add error handling utilities. Introduce analytics and polling mechanisms for improved user experience.
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
Some checks failed
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit
This commit is contained in:
237
src/context/ChallengeContext.tsx
Normal file
237
src/context/ChallengeContext.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
|
||||
import {
|
||||
useAuthUserMutation,
|
||||
useGetChainsQuery,
|
||||
useLazyGetUserStatsQuery,
|
||||
} from '../__data__/api/api'
|
||||
import type { ChallengeChain, PersonalDashboard, UserStats } from '../__data__/types'
|
||||
import { BehaviorTracker, MetricsCollector, buildPersonalDashboard } from '../utils/analytics'
|
||||
import { ChallengeEventEmitter } from '../utils/events'
|
||||
import { clearDraft, loadDraft, saveDraft } from '../utils/drafts'
|
||||
import { PollingManager } from '../utils/polling'
|
||||
|
||||
const isBrowser = () => typeof window !== 'undefined'
|
||||
|
||||
class ChallengeCache {
|
||||
private cache = new Map<string, { data: unknown; expires: number }>()
|
||||
|
||||
set(key: string, data: unknown, ttl = 60_000) {
|
||||
this.cache.set(key, { data, expires: Date.now() + ttl })
|
||||
}
|
||||
|
||||
get<T>(key: string): T | null {
|
||||
const entry = this.cache.get(key)
|
||||
if (!entry) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Date.now() > entry.expires) {
|
||||
this.cache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return entry.data as T
|
||||
}
|
||||
|
||||
clear(key?: string) {
|
||||
if (key) {
|
||||
this.cache.delete(key)
|
||||
return
|
||||
}
|
||||
|
||||
this.cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
interface ChallengeContextValue {
|
||||
userId: string | null
|
||||
nickname: string | null
|
||||
stats: UserStats | null
|
||||
personalDashboard: PersonalDashboard | null
|
||||
chains: ChallengeChain[]
|
||||
isAuthenticated: boolean
|
||||
isAuthLoading: boolean
|
||||
isStatsLoading: boolean
|
||||
login: (nickname: string) => Promise<void>
|
||||
logout: () => void
|
||||
refreshStats: () => Promise<void>
|
||||
eventEmitter: ChallengeEventEmitter
|
||||
pollingManager: PollingManager
|
||||
metricsCollector: MetricsCollector
|
||||
behaviorTracker: BehaviorTracker
|
||||
saveDraft: (taskId: string, value: string) => void
|
||||
loadDraft: (taskId: string) => string | null
|
||||
clearDraft: (taskId: string) => void
|
||||
}
|
||||
|
||||
const ChallengeContext = createContext<ChallengeContextValue | undefined>(undefined)
|
||||
|
||||
const USER_ID_KEY = 'challengeUserId'
|
||||
const USER_NICKNAME_KEY = 'challengeNickname'
|
||||
|
||||
export const ChallengeProvider = ({ children }: PropsWithChildren) => {
|
||||
const cacheRef = useRef(new ChallengeCache())
|
||||
const metricsCollector = useMemo(() => new MetricsCollector(), [])
|
||||
const behaviorTracker = useMemo(() => new BehaviorTracker(), [])
|
||||
const eventEmitter = useMemo(() => new ChallengeEventEmitter(), [])
|
||||
const pollingManager = useMemo(() => new PollingManager(), [])
|
||||
|
||||
const [userId, setUserId] = useState<string | null>(() =>
|
||||
isBrowser() ? window.localStorage.getItem(USER_ID_KEY) : null,
|
||||
)
|
||||
const [nickname, setNickname] = useState<string | null>(() =>
|
||||
isBrowser() ? window.localStorage.getItem(USER_NICKNAME_KEY) : null,
|
||||
)
|
||||
const [stats, setStats] = useState<UserStats | null>(null)
|
||||
const [personalDashboard, setPersonalDashboard] = useState<PersonalDashboard | null>(null)
|
||||
const [chains, setChains] = useState<ChallengeChain[]>(() => {
|
||||
const cached = cacheRef.current.get<ChallengeChain[]>('chains')
|
||||
return cached ?? []
|
||||
})
|
||||
|
||||
const [authUser, { isLoading: isAuthLoading }] = useAuthUserMutation()
|
||||
const [triggerStats, statsResult] = useLazyGetUserStatsQuery()
|
||||
|
||||
const { data: chainsData, isLoading: isChainsLoading } = useGetChainsQuery(undefined, {
|
||||
skip: !userId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (chainsData) {
|
||||
setChains(chainsData)
|
||||
cacheRef.current.set('chains', chainsData, 5 * 60_000)
|
||||
}
|
||||
}, [chainsData])
|
||||
|
||||
const refreshStatsById = useCallback(
|
||||
async (id: string) => {
|
||||
const cachedStats = cacheRef.current.get<UserStats>(`stats_${id}`)
|
||||
if (cachedStats) {
|
||||
setStats(cachedStats)
|
||||
const cachedChains = cacheRef.current.get<ChallengeChain[]>('chains')
|
||||
if (cachedChains) {
|
||||
setPersonalDashboard(buildPersonalDashboard(cachedStats, cachedChains))
|
||||
}
|
||||
}
|
||||
|
||||
const result = await triggerStats(id, true)
|
||||
if ('data' in result && result.data) {
|
||||
cacheRef.current.set(`stats_${id}`, result.data, 60_000)
|
||||
setStats(result.data)
|
||||
|
||||
const cachedChains = cacheRef.current.get<ChallengeChain[]>('chains') ?? chains
|
||||
setPersonalDashboard(buildPersonalDashboard(result.data, cachedChains))
|
||||
}
|
||||
},
|
||||
[chains, triggerStats],
|
||||
)
|
||||
|
||||
const refreshStats = useCallback(async () => {
|
||||
if (!userId) return
|
||||
await refreshStatsById(userId)
|
||||
}, [refreshStatsById, userId])
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
refreshStats()
|
||||
} else {
|
||||
setStats(null)
|
||||
setPersonalDashboard(null)
|
||||
}
|
||||
}, [refreshStats, userId])
|
||||
|
||||
const login = useCallback(
|
||||
async (nicknameValue: string) => {
|
||||
const response = await authUser({ nickname: nicknameValue }).unwrap()
|
||||
setUserId(response.userId)
|
||||
setNickname(nicknameValue)
|
||||
|
||||
if (isBrowser()) {
|
||||
window.localStorage.setItem(USER_ID_KEY, response.userId)
|
||||
window.localStorage.setItem(USER_NICKNAME_KEY, nicknameValue)
|
||||
}
|
||||
|
||||
cacheRef.current.clear('chains')
|
||||
await refreshStatsById(response.userId)
|
||||
},
|
||||
[authUser, refreshStatsById],
|
||||
)
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setUserId(null)
|
||||
setNickname(null)
|
||||
setStats(null)
|
||||
setPersonalDashboard(null)
|
||||
cacheRef.current.clear()
|
||||
|
||||
if (isBrowser()) {
|
||||
window.localStorage.removeItem(USER_ID_KEY)
|
||||
window.localStorage.removeItem(USER_NICKNAME_KEY)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isStatsLoading = statsResult.isLoading || statsResult.isFetching || isChainsLoading
|
||||
|
||||
const value = useMemo<ChallengeContextValue>(
|
||||
() => ({
|
||||
userId,
|
||||
nickname,
|
||||
stats,
|
||||
personalDashboard,
|
||||
chains,
|
||||
isAuthenticated: Boolean(userId),
|
||||
isAuthLoading,
|
||||
isStatsLoading,
|
||||
login,
|
||||
logout,
|
||||
refreshStats,
|
||||
eventEmitter,
|
||||
pollingManager,
|
||||
metricsCollector,
|
||||
behaviorTracker,
|
||||
saveDraft,
|
||||
loadDraft,
|
||||
clearDraft,
|
||||
}),
|
||||
[
|
||||
behaviorTracker,
|
||||
chains,
|
||||
clearDraft,
|
||||
eventEmitter,
|
||||
isAuthLoading,
|
||||
isStatsLoading,
|
||||
loadDraft,
|
||||
login,
|
||||
logout,
|
||||
metricsCollector,
|
||||
nickname,
|
||||
personalDashboard,
|
||||
pollingManager,
|
||||
refreshStats,
|
||||
saveDraft,
|
||||
stats,
|
||||
userId,
|
||||
],
|
||||
)
|
||||
|
||||
return <ChallengeContext.Provider value={value}>{children}</ChallengeContext.Provider>
|
||||
}
|
||||
|
||||
export const useChallenge = () => {
|
||||
const context = useContext(ChallengeContext)
|
||||
if (!context) {
|
||||
throw new Error('useChallenge must be used within ChallengeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user