First crack at the app. Still lots of bugs to squash.
This commit is contained in:
92
src/lib/api/client.ts
Normal file
92
src/lib/api/client.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {clearTokens, getAccessToken, getRefreshToken, setAccessToken} from '../stores/auth.svelte'
|
||||
|
||||
export const BASE_URL = 'http://localhost:8080'
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public status: number, message: string) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
async function attemptRefresh(): Promise<boolean> {
|
||||
const rt = getRefreshToken()
|
||||
if (!rt) return false
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken: rt }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
clearTokens()
|
||||
return false
|
||||
}
|
||||
const data: { accessToken: string } = await res.json()
|
||||
setAccessToken(data.accessToken)
|
||||
return true
|
||||
} catch {
|
||||
clearTokens()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function rawRequest(path: string, init: RequestInit = {}, retry = true): Promise<Response> {
|
||||
const token = getAccessToken()
|
||||
const headers = new Headers(init.headers)
|
||||
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||
if (!(init.body instanceof FormData) && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}${path}`, { ...init, headers })
|
||||
|
||||
if (res.status === 401 && retry) {
|
||||
const ok = await attemptRefresh()
|
||||
if (ok) return rawRequest(path, init, false)
|
||||
clearTokens()
|
||||
window.location.hash = '/login'
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const res = await rawRequest(path, init)
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => res.statusText)
|
||||
throw new ApiError(res.status, text)
|
||||
}
|
||||
const ct = res.headers.get('content-type') ?? ''
|
||||
if (ct.includes('application/json')) return res.json() as Promise<T>
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
export const client = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
|
||||
post: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, {
|
||||
method: 'POST',
|
||||
body: body != null ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
patch: <T>(path: string, body?: unknown) =>
|
||||
request<T>(path, {
|
||||
method: 'PATCH',
|
||||
body: body != null ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
|
||||
postFile: <T>(path: string, file: File) => {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
return request<T>(path, { method: 'POST', body: form })
|
||||
},
|
||||
|
||||
getBlob: async (path: string): Promise<Blob> => {
|
||||
const res = await rawRequest(path)
|
||||
if (!res.ok) throw new ApiError(res.status, res.statusText)
|
||||
return res.blob()
|
||||
},
|
||||
}
|
||||
106
src/lib/api/index.ts
Normal file
106
src/lib/api/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import {BASE_URL, client} from './client'
|
||||
import type {
|
||||
HunterLeaderboardResponse,
|
||||
HuntResponse,
|
||||
ItemResponse,
|
||||
LoginResponse,
|
||||
PhotoResponse,
|
||||
TeamItemResponse,
|
||||
TeamLeaderboardResponse,
|
||||
TeamResponse,
|
||||
} from './types'
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiLogin = (email: string, password: string) =>
|
||||
client.post<LoginResponse>('/auth/login', { email, password })
|
||||
|
||||
export const apiLogout = (refreshToken: string) =>
|
||||
client.post<void>('/auth/logout', { refreshToken })
|
||||
|
||||
export const apiSignup = (name: string, email: string, password: string) =>
|
||||
client.post<void>('/signup', { name, email, password })
|
||||
|
||||
// ── Hunts ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiGetHunt = (huntId: string) =>
|
||||
client.get<HuntResponse>(`/hunt/${huntId}`)
|
||||
|
||||
export const apiGetUnstartedHunts = () =>
|
||||
client.get<HuntResponse[]>('/hunt/unstarted')
|
||||
|
||||
export const apiGetAllHunts = (status?: 'UNSTARTED' | 'ONGOING' | 'CLOSED') =>
|
||||
client.get<HuntResponse[]>(`/hunt${status ? `?status=${status}` : ''}`)
|
||||
|
||||
export const apiCreateHunt = (title: string, startDateTime: string, endDateTime: string) =>
|
||||
client.post<HuntResponse>('/hunt', { title, startDateTime, endDateTime })
|
||||
|
||||
// ── Hunter ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiGetOngoingHunts = () =>
|
||||
client.get<HuntResponse[]>('/hunter/hunt/ongoing')
|
||||
|
||||
export const apiGetHunterTeam = (huntId: string) =>
|
||||
client.get<TeamResponse>(`/hunter/hunt/${huntId}/team`)
|
||||
|
||||
export const apiJoinTeam = (huntId: string, teamId: string) =>
|
||||
client.post<void>(`/hunter/hunt/${huntId}/team/${teamId}`)
|
||||
|
||||
// ── Teams ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiListTeams = (huntId: string) =>
|
||||
client.get<TeamResponse[]>(`/hunt/${huntId}/team`)
|
||||
|
||||
export const apiCreateTeam = (huntId: string, name: string) =>
|
||||
client.post<void>(`/hunt/${huntId}/team`, { name })
|
||||
|
||||
export const apiGetTeam = (huntId: string, teamId: string) =>
|
||||
client.get<TeamResponse>(`/hunt/${huntId}/team/${teamId}`)
|
||||
|
||||
// ── Items ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiGetItems = (huntId: string) =>
|
||||
client.get<ItemResponse[]>(`/hunt/${huntId}/item`)
|
||||
|
||||
export const apiAddItem = (huntId: string, name: string, points: number) =>
|
||||
client.post<ItemResponse>(`/hunt/${huntId}/item`, { name, points })
|
||||
|
||||
export const apiUpdateItem = (huntId: string, itemId: string, name: string, points: number) =>
|
||||
client.patch<ItemResponse>(`/hunt/${huntId}/item/${itemId}`, { name, points })
|
||||
|
||||
export const apiDeleteItem = (huntId: string, itemId: string) =>
|
||||
client.delete<void>(`/hunt/${huntId}/item/${itemId}`)
|
||||
|
||||
export const apiGetTeamItem = (huntId: string, teamId: string, itemId: string) =>
|
||||
client.get<TeamItemResponse>(`/hunt/${huntId}/team/${teamId}/item/${itemId}`)
|
||||
|
||||
// ── Photos ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiGetItemPhotos = (huntId: string, teamId: string, itemId: string) =>
|
||||
client.get<PhotoResponse[]>(`/hunt/${huntId}/team/${teamId}/item/${itemId}/photo`)
|
||||
|
||||
export const apiSubmitPhoto = (huntId: string, teamId: string, itemId: string, file: File) =>
|
||||
client.postFile<void>(`/hunt/${huntId}/team/${teamId}/item/${itemId}/photo`, file)
|
||||
|
||||
export const apiRemovePhoto = (huntId: string, teamId: string, itemId: string, photoId: string) =>
|
||||
client.patch<void>(`/hunt/${huntId}/team/${teamId}/item/${itemId}/photo/${photoId}`)
|
||||
|
||||
export const apiGetPhotoBlob = (photoId: string, version: 'ORIGINAL' | 'LARGE' | 'MEDIUM' | 'SMALL' = 'MEDIUM') =>
|
||||
client.getBlob(`/photo/${photoId}/file?version=${version}`)
|
||||
|
||||
export function photoUrl(photoId: string, version: 'ORIGINAL' | 'LARGE' | 'MEDIUM' | 'SMALL' = 'MEDIUM') {
|
||||
return `${BASE_URL}/photo/${photoId}/file?version=${version}`
|
||||
}
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiReviewPhoto = (photoId: string, status: 'SUBMITTED' | 'APPROVED' | 'REJECTED' | 'REMOVED') =>
|
||||
client.patch<void>(`/admin/photo/${photoId}`, { status })
|
||||
|
||||
// ── Stats ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const apiGetTeamLeaderboard = (huntId: string) =>
|
||||
client.get<TeamLeaderboardResponse[]>(`/stats/lead/hunt/${huntId}/team`)
|
||||
|
||||
export const apiGetHunterLeaderboard = (huntId: string) =>
|
||||
client.get<HunterLeaderboardResponse[]>(`/stats/lead/hunt/${huntId}/hunter`)
|
||||
53
src/lib/api/types.ts
Normal file
53
src/lib/api/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface HuntResponse {
|
||||
id: string
|
||||
title: string
|
||||
startDateTime: string
|
||||
endDateTime: string
|
||||
isTerminated: boolean
|
||||
}
|
||||
|
||||
export interface TeamResponse {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ItemResponse {
|
||||
id: string
|
||||
name: string
|
||||
points: number
|
||||
}
|
||||
|
||||
export interface TeamItemResponse {
|
||||
id: string
|
||||
itemFoundStatus: 'NOT_FOUND' | 'SUBMITTED' | 'APPROVED' | 'REJECTED'
|
||||
}
|
||||
|
||||
export interface PhotoResponse {
|
||||
id: string
|
||||
hunterName: string
|
||||
photoUploadDateTime: string
|
||||
photoStatus: 'SUBMITTED' | 'APPROVED' | 'REJECTED' | 'REMOVED'
|
||||
photoStatusChangeDateTime: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface RefreshResponse {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface TeamLeaderboardResponse {
|
||||
rank: number
|
||||
teamName: string
|
||||
score: number
|
||||
}
|
||||
|
||||
export interface HunterLeaderboardResponse {
|
||||
rank: number
|
||||
hunterName: string
|
||||
score: number
|
||||
}
|
||||
Reference in New Issue
Block a user