24f9a69022
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>
138 lines
5.5 KiB
TypeScript
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 }
|