Files
vtuber-awards/frontend/src/lib/api.ts
T
AzuTear 24f9a69022 Align backend with frontend, make clips end-to-end, polish admin
Backend
- Add ClipSubmission entity + table (runtime bootstrapper) and
  POST /api/public/clips (server-derives platform from the link)
- Surface clip submissions in the admin season detail
- Add DELETE candidate/category/clip endpoints with audit entries

Frontend
- Clips admin: real moderation view (list, open link, delete) instead
  of placeholder; wired clipSubmissions through types/api/store
- Categories admin: add delete with confirm modal (matches Candidates)
- Voting ranking bar uses the unified purple gradient
- Fix ASCII transliterations to proper German umlauts across admin
- Remove orphan AdminView 2.vue

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:53:19 +02:00

138 lines
5.5 KiB
TypeScript

import type {
AdminDashboardResponse,
AdminSeasonDetailResponse,
AdminSeasonListItem,
AuthSession,
CreateClipPayload,
CreateNominationPayload,
CreateVotePayload,
LoginPayload,
OverviewResponse,
SeasonCategoriesResponse,
UpdateSeasonPayload,
ApproveNominationPayload,
UpsertCandidatePayload,
UpsertCategoryPayload,
WinnerArchiveResponse,
} from '../types/awards'
const API_URL = import.meta.env.VITE_API_URL ?? 'http://127.0.0.1:5084'
const AUTH_TOKEN_KEY = 'vtsa-session-token'
function getAuthToken() {
if (typeof window === 'undefined') return null
return window.localStorage.getItem(AUTH_TOKEN_KEY)
}
async function getJson<T>(path: string): Promise<T> {
const token = getAuthToken()
const response = await fetch(`${API_URL}${path}`, {
headers: token
? {
Authorization: `Bearer ${token}`,
}
: undefined,
}).catch(() => {
throw new Error(`API nicht erreichbar (${API_URL}). Bitte Backend starten.`)
})
if (!response.ok) {
throw new Error(`API request failed for ${path}`)
}
return response.json() as Promise<T>
}
async function sendDelete<TResponse>(path: string): Promise<TResponse> {
const token = getAuthToken()
const response = await fetch(`${API_URL}${path}`, {
method: 'DELETE',
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
}).catch(() => {
throw new Error(`API nicht erreichbar (${API_URL}). Bitte Backend starten.`)
})
if (!response.ok) {
const error = await response.text()
throw new Error(error || `API request failed for ${path}`)
}
return response.json() as Promise<TResponse>
}
async function sendJson<TResponse>(path: string, method: 'POST' | 'PUT', body: unknown): Promise<TResponse> {
const token = getAuthToken()
const response = await fetch(`${API_URL}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(body),
}).catch(() => {
throw new Error(`API nicht erreichbar (${API_URL}). Bitte Backend starten.`)
})
if (!response.ok) {
const error = await response.text()
throw new Error(error || `API request failed for ${path}`)
}
return response.json() as Promise<TResponse>
}
export const api = {
getOverview: () => getJson<OverviewResponse>('/api/public/overview'),
getSeasonCategories: (year: number) =>
getJson<SeasonCategoriesResponse>(`/api/public/seasons/${year}/categories`),
getWinnerArchive: (year: number) =>
getJson<WinnerArchiveResponse>(`/api/public/seasons/${year}/winners`),
getAdminDashboard: () => getJson<AdminDashboardResponse>('/api/admin/dashboard'),
getAdminSeasons: () => getJson<AdminSeasonListItem[]>('/api/admin/seasons'),
getAdminSeasonDetail: (seasonId: number) =>
getJson<AdminSeasonDetailResponse>(`/api/admin/seasons/${seasonId}`),
getSession: () => getJson<AuthSession>('/api/auth/session'),
login: (payload: LoginPayload) => sendJson<AuthSession>('/api/auth/dev-login', 'POST', payload),
logout: () => sendJson<{ loggedOut: boolean }>('/api/auth/logout', 'POST', {}),
submitNomination: (payload: CreateNominationPayload) =>
sendJson<{ saved: number; category: string }>('/api/public/nominations', 'POST', payload),
submitVote: (payload: CreateVotePayload) =>
sendJson<{ ballotId: number; entries: number }>('/api/public/votes', 'POST', payload),
submitClip: (payload: CreateClipPayload) =>
sendJson<{ saved: boolean; clipId: number }>('/api/public/clips', 'POST', payload),
updateAdminSeason: (seasonId: number, payload: UpdateSeasonPayload) =>
sendJson<{ saved: boolean; seasonId: number }>(`/api/admin/seasons/${seasonId}`, 'PUT', payload),
createAdminCategory: (seasonId: number, payload: UpsertCategoryPayload) =>
sendJson<{ saved: boolean; categoryId: number }>(`/api/admin/seasons/${seasonId}/categories`, 'POST', payload),
updateAdminCategory: (categoryId: number, payload: UpsertCategoryPayload) =>
sendJson<{ saved: boolean; categoryId: number }>(`/api/admin/categories/${categoryId}`, 'PUT', payload),
createAdminCandidate: (seasonId: number, payload: UpsertCandidatePayload) =>
sendJson<{ saved: boolean; candidateId: number }>(`/api/admin/seasons/${seasonId}/candidates`, 'POST', payload),
updateAdminCandidate: (candidateId: number, payload: UpsertCandidatePayload) =>
sendJson<{ saved: boolean; candidateId: number }>(`/api/admin/candidates/${candidateId}`, 'PUT', payload),
deleteAdminCandidate: (candidateId: number) =>
sendDelete<{ deleted: boolean; candidateId: number }>(`/api/admin/candidates/${candidateId}`),
deleteAdminCategory: (categoryId: number) =>
sendDelete<{ deleted: boolean; categoryId: number }>(`/api/admin/categories/${categoryId}`),
deleteAdminClip: (clipId: number) =>
sendDelete<{ deleted: boolean; clipId: number }>(`/api/admin/clips/${clipId}`),
approveAdminNomination: (nominationId: number, payload: ApproveNominationPayload) =>
sendJson<{ saved: boolean; nominationId: number; candidateId: number; created: boolean }>(
`/api/admin/nominations/${nominationId}/approve`,
'POST',
payload,
),
rejectAdminNomination: (nominationId: number) =>
sendJson<{ saved: boolean; nominationId: number; rejected: boolean }>(
`/api/admin/nominations/${nominationId}/reject`,
'POST',
{},
),
resolveRiskFlag: (riskFlagId: number, status = 'resolved') =>
sendJson<{ saved: boolean; riskFlagId: number; status: string }>(
`/api/admin/risk-flags/${riskFlagId}/resolve`,
'POST',
{ status },
),
}
export { AUTH_TOKEN_KEY }