This commit is contained in:
2025-12-14 02:38:35 +07:00
commit 5343a8f2c3
84 changed files with 7406 additions and 0 deletions

30
frontend/src/api/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
import client from './client'
import type { TokenResponse, User } from '@/types'
export interface RegisterData {
login: string
password: string
nickname: string
}
export interface LoginData {
login: string
password: string
}
export const authApi = {
register: async (data: RegisterData): Promise<TokenResponse> => {
const response = await client.post<TokenResponse>('/auth/register', data)
return response.data
},
login: async (data: LoginData): Promise<TokenResponse> => {
const response = await client.post<TokenResponse>('/auth/login', data)
return response.data
},
me: async (): Promise<User> => {
const response = await client.get<User>('/auth/me')
return response.data
},
}

View File

@@ -0,0 +1,34 @@
import axios, { AxiosError } from 'axios'
const API_URL = import.meta.env.VITE_API_URL || '/api/v1'
const client = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
})
// Request interceptor to add auth token
client.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Response interceptor to handle errors
client.interceptors.response.use(
(response) => response,
(error: AxiosError<{ detail: string }>) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default client

11
frontend/src/api/feed.ts Normal file
View File

@@ -0,0 +1,11 @@
import client from './client'
import type { FeedResponse } from '@/types'
export const feedApi = {
get: async (marathonId: number, limit = 20, offset = 0): Promise<FeedResponse> => {
const response = await client.get<FeedResponse>(`/marathons/${marathonId}/feed`, {
params: { limit, offset },
})
return response.data
},
}

70
frontend/src/api/games.ts Normal file
View File

@@ -0,0 +1,70 @@
import client from './client'
import type { Game, Challenge } from '@/types'
export interface CreateGameData {
title: string
download_url: string
genre?: string
cover_url?: string
}
export interface CreateChallengeData {
title: string
description: string
type: string
difficulty: string
points: number
estimated_time?: number
proof_type: string
proof_hint?: string
}
export const gamesApi = {
list: async (marathonId: number): Promise<Game[]> => {
const response = await client.get<Game[]>(`/marathons/${marathonId}/games`)
return response.data
},
get: async (id: number): Promise<Game> => {
const response = await client.get<Game>(`/games/${id}`)
return response.data
},
create: async (marathonId: number, data: CreateGameData): Promise<Game> => {
const response = await client.post<Game>(`/marathons/${marathonId}/games`, data)
return response.data
},
delete: async (id: number): Promise<void> => {
await client.delete(`/games/${id}`)
},
uploadCover: async (id: number, file: File): Promise<Game> => {
const formData = new FormData()
formData.append('file', file)
const response = await client.post<Game>(`/games/${id}/cover`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
},
// Challenges
getChallenges: async (gameId: number): Promise<Challenge[]> => {
const response = await client.get<Challenge[]>(`/games/${gameId}/challenges`)
return response.data
},
createChallenge: async (gameId: number, data: CreateChallengeData): Promise<Challenge> => {
const response = await client.post<Challenge>(`/games/${gameId}/challenges`, data)
return response.data
},
deleteChallenge: async (id: number): Promise<void> => {
await client.delete(`/challenges/${id}`)
},
generateChallenges: async (marathonId: number): Promise<{ message: string }> => {
const response = await client.post<{ message: string }>(`/marathons/${marathonId}/generate-challenges`)
return response.data
},
}

View File

@@ -0,0 +1,5 @@
export { authApi } from './auth'
export { marathonsApi } from './marathons'
export { gamesApi } from './games'
export { wheelApi } from './wheel'
export { feedApi } from './feed'

View File

@@ -0,0 +1,64 @@
import client from './client'
import type { Marathon, MarathonListItem, LeaderboardEntry, ParticipantInfo, User } from '@/types'
export interface CreateMarathonData {
title: string
description?: string
start_date: string
duration_days?: number
}
export interface ParticipantWithUser extends ParticipantInfo {
user: User
}
export const marathonsApi = {
list: async (): Promise<MarathonListItem[]> => {
const response = await client.get<MarathonListItem[]>('/marathons')
return response.data
},
get: async (id: number): Promise<Marathon> => {
const response = await client.get<Marathon>(`/marathons/${id}`)
return response.data
},
create: async (data: CreateMarathonData): Promise<Marathon> => {
const response = await client.post<Marathon>('/marathons', data)
return response.data
},
update: async (id: number, data: Partial<CreateMarathonData>): Promise<Marathon> => {
const response = await client.patch<Marathon>(`/marathons/${id}`, data)
return response.data
},
delete: async (id: number): Promise<void> => {
await client.delete(`/marathons/${id}`)
},
start: async (id: number): Promise<Marathon> => {
const response = await client.post<Marathon>(`/marathons/${id}/start`)
return response.data
},
finish: async (id: number): Promise<Marathon> => {
const response = await client.post<Marathon>(`/marathons/${id}/finish`)
return response.data
},
join: async (inviteCode: string): Promise<Marathon> => {
const response = await client.post<Marathon>('/marathons/join', { invite_code: inviteCode })
return response.data
},
getParticipants: async (id: number): Promise<ParticipantWithUser[]> => {
const response = await client.get<ParticipantWithUser[]>(`/marathons/${id}/participants`)
return response.data
},
getLeaderboard: async (id: number): Promise<LeaderboardEntry[]> => {
const response = await client.get<LeaderboardEntry[]>(`/marathons/${id}/leaderboard`)
return response.data
},
}

41
frontend/src/api/wheel.ts Normal file
View File

@@ -0,0 +1,41 @@
import client from './client'
import type { SpinResult, Assignment, CompleteResult, DropResult } from '@/types'
export const wheelApi = {
spin: async (marathonId: number): Promise<SpinResult> => {
const response = await client.post<SpinResult>(`/marathons/${marathonId}/spin`)
return response.data
},
getCurrentAssignment: async (marathonId: number): Promise<Assignment | null> => {
const response = await client.get<Assignment | null>(`/marathons/${marathonId}/current-assignment`)
return response.data
},
complete: async (
assignmentId: number,
data: { proof_url?: string; comment?: string; proof_file?: File }
): Promise<CompleteResult> => {
const formData = new FormData()
if (data.proof_url) formData.append('proof_url', data.proof_url)
if (data.comment) formData.append('comment', data.comment)
if (data.proof_file) formData.append('proof_file', data.proof_file)
const response = await client.post<CompleteResult>(`/assignments/${assignmentId}/complete`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
},
drop: async (assignmentId: number): Promise<DropResult> => {
const response = await client.post<DropResult>(`/assignments/${assignmentId}/drop`)
return response.data
},
getHistory: async (marathonId: number, limit = 20, offset = 0): Promise<Assignment[]> => {
const response = await client.get<Assignment[]>(`/marathons/${marathonId}/my-history`, {
params: { limit, offset },
})
return response.data
},
}