Improve admin candidate modal UX and add clip menu visibility toggle

- Widen AdminCandidateEditorModal to size lg for better readability
- Rename "Clip-Compilation" section to "Clip / Compilation", update copy to reflect single clips too, drop upload hint and Clip-Plattform field, rename label to "Link"
- Fix NativeSelect dropdown clipping inside overflow-y-auto modals by teleporting the menu to body with fixed positioning, flip-up logic, and dynamic maxHeight capped to viewport
- Add ClipAdminMenuVisible setting (backend domain, contracts, endpoint, migration) with matching frontend types, defaults, form wiring, and toggle in the Clip-Workflow modal — hides the Clips nav item from the admin sidebar when disabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-27 18:39:35 +02:00
parent 494eba5edd
commit 18b61bed52
119 changed files with 15638 additions and 367 deletions
+119 -2
View File
@@ -7,16 +7,21 @@ import {
createEmptyAdminDashboard,
createEmptyAdminRiskFlagsResponse,
createEmptyAdminSeasonDetail,
createEmptyAdminOptionalFeatureSettings,
createEmptyAdminSiteSettings,
createEmptyAdminWorkflowRulesResponse,
createEmptyArchive,
createEmptyDatabaseHealth,
createEmptyPublicSponsors,
normalizeSeasonDetail,
} from './awards/defaults'
import type {
ApproveNominationPayload,
AddNominationLinkBlacklistEntryPayload,
CreateSeasonPayload,
CreateClipPayload,
CreateNominationPayload,
CreateShowactApplicationPayload,
CreateVotePayload,
RejectNominationPayload,
ResolveRiskFlagPayload,
@@ -25,9 +30,14 @@ import type {
AdminRiskQueryOptions,
UpdateSeasonPayload,
UpdateClipStatusPayload,
UpdateNominationLinkBlacklistPayload,
UpdateOptionalFeatureSettingsPayload,
UpdateShowactStatusPayload,
UpdateSiteSettingsPayload,
UpdateWorkflowRulesPayload,
UpsertCandidatePayload,
UpsertCategoryPayload,
UpsertSponsorPayload,
} from '../types/awards'
interface AdminSeasonRefreshOptions {
@@ -45,6 +55,7 @@ export const useAwardsStore = defineStore('awards', {
try {
this.overview = await api.getOverview()
this.categories = await api.getSeasonCategories(this.overview.year)
await this.loadPublicSponsors(this.overview.year)
const overviewArchiveYears = this.overview.archiveYears ?? []
const initialArchiveYear = overviewArchiveYears[0]?.year
?? this.overview.winnersPreview[0]?.year
@@ -70,18 +81,31 @@ export const useAwardsStore = defineStore('awards', {
this.archive = createEmptyArchive(year)
}
},
async loadPublicSponsors(year: number) {
try {
this.publicSponsors = await api.getPublicSponsors(year)
return this.publicSponsors
} catch {
this.publicSponsors = createEmptyPublicSponsors(year)
return this.publicSponsors
}
},
async loadAdmin() {
try {
const [admin, adminSeasons, adminSiteSettings, databaseHealth] = await Promise.all([
const [admin, adminSeasons, adminSiteSettings, adminOptionalFeatureSettings, adminWorkflowRules, databaseHealth] = await Promise.all([
api.getAdminDashboard(),
api.getAdminSeasons(),
api.getAdminSiteSettings(),
api.getAdminOptionalFeatureSettings(),
api.getAdminWorkflowRules(),
api.getDatabaseHealth(),
])
this.admin = admin
this.adminSeasons = adminSeasons
this.adminSiteSettings = adminSiteSettings
this.adminOptionalFeatureSettings = adminOptionalFeatureSettings
this.adminWorkflowRules = adminWorkflowRules
this.databaseHealth = databaseHealth
if (!this.adminSelectedSeasonId || !this.adminSeasons.some((season) => season.id === this.adminSelectedSeasonId)) {
@@ -90,8 +114,16 @@ export const useAwardsStore = defineStore('awards', {
if (this.adminSelectedSeasonId) {
this.adminSeasonDetail = normalizeSeasonDetail(await api.getAdminSeasonDetail(this.adminSelectedSeasonId))
const [sponsors, showacts] = await Promise.all([
api.getAdminSponsors(this.adminSelectedSeasonId),
api.getAdminShowactApplications(this.adminSelectedSeasonId),
])
this.adminSponsors = sponsors
this.adminShowactApplications = showacts
} else {
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
this.adminSponsors = []
this.adminShowactApplications = []
}
this.apiMode = 'api'
} catch {
@@ -99,25 +131,32 @@ export const useAwardsStore = defineStore('awards', {
this.adminSeasons = []
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
this.adminSiteSettings = createEmptyAdminSiteSettings()
this.adminOptionalFeatureSettings = createEmptyAdminOptionalFeatureSettings()
this.adminWorkflowRules = createEmptyAdminWorkflowRulesResponse()
this.adminRiskHistory = []
this.adminRiskFlagsPage = createEmptyAdminRiskFlagsResponse()
this.adminRiskHistoryPage = createEmptyAdminRiskFlagsResponse()
this.adminSponsors = []
this.adminShowactApplications = []
this.databaseHealth = createEmptyDatabaseHealth()
this.adminSelectedSeasonId = null
}
},
async loadAdminContentWorkspace() {
try {
const [adminSiteSettings, databaseHealth] = await Promise.all([
const [adminSiteSettings, adminOptionalFeatureSettings, databaseHealth] = await Promise.all([
api.getAdminSiteSettings(),
api.getAdminOptionalFeatureSettings(),
api.getDatabaseHealth(),
])
this.adminSiteSettings = adminSiteSettings
this.adminOptionalFeatureSettings = adminOptionalFeatureSettings
this.databaseHealth = databaseHealth
this.apiMode = 'api'
} catch {
this.adminSiteSettings = createEmptyAdminSiteSettings()
this.adminOptionalFeatureSettings = createEmptyAdminOptionalFeatureSettings()
this.databaseHealth = createEmptyDatabaseHealth()
}
},
@@ -129,9 +168,17 @@ export const useAwardsStore = defineStore('awards', {
try {
this.adminSelectedSeasonId = seasonId
this.adminSeasonDetail = normalizeSeasonDetail(await api.getAdminSeasonDetail(seasonId))
const [sponsors, showacts] = await Promise.all([
api.getAdminSponsors(seasonId),
api.getAdminShowactApplications(seasonId),
])
this.adminSponsors = sponsors
this.adminShowactApplications = showacts
this.apiMode = 'api'
} catch {
this.adminSeasonDetail = createEmptyAdminSeasonDetail()
this.adminSponsors = []
this.adminShowactApplications = []
}
},
async initializeAdminWorkspace() {
@@ -209,6 +256,50 @@ export const useAwardsStore = defineStore('awards', {
submitClip(payload: CreateClipPayload) {
return api.submitClip(payload)
},
submitShowactApplication(payload: CreateShowactApplicationPayload) {
return api.submitShowactApplication(payload)
},
async loadAdminExtras(seasonId?: number | null) {
const resolvedSeasonId = seasonId ?? this.adminSelectedSeasonId
if (!resolvedSeasonId) {
this.adminSponsors = []
this.adminShowactApplications = []
return { sponsors: this.adminSponsors, showacts: this.adminShowactApplications }
}
const [sponsors, showacts] = await Promise.all([
api.getAdminSponsors(resolvedSeasonId),
api.getAdminShowactApplications(resolvedSeasonId),
])
this.adminSponsors = sponsors
this.adminShowactApplications = showacts
return { sponsors, showacts }
},
async createAdminSponsor(seasonId: number, payload: UpsertSponsorPayload) {
const result = await api.createAdminSponsor(seasonId, payload)
await Promise.all([this.loadAdminExtras(seasonId), this.loadPublicSponsors(this.overview.year)])
return result
},
async updateAdminSponsor(sponsorId: number, seasonId: number, payload: UpsertSponsorPayload) {
const result = await api.updateAdminSponsor(sponsorId, payload)
await Promise.all([this.loadAdminExtras(seasonId), this.loadPublicSponsors(this.overview.year)])
return result
},
async deleteAdminSponsor(sponsorId: number, seasonId: number) {
const result = await api.deleteAdminSponsor(sponsorId)
await Promise.all([this.loadAdminExtras(seasonId), this.loadPublicSponsors(this.overview.year)])
return result
},
async updateAdminShowactStatus(applicationId: number, seasonId: number, payload: UpdateShowactStatusPayload) {
const result = await api.updateAdminShowactStatus(applicationId, payload)
await this.loadAdminExtras(seasonId)
return result
},
async deleteAdminShowactApplication(applicationId: number, seasonId: number) {
const result = await api.deleteAdminShowactApplication(applicationId)
await this.loadAdminExtras(seasonId)
return result
},
async updateAdminSeason(seasonId: number, payload: UpdateSeasonPayload) {
const result = await api.updateAdminSeason(seasonId, payload)
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true, reloadHome: true })
@@ -292,6 +383,15 @@ export const useAwardsStore = defineStore('awards', {
await Promise.allSettled(nominationIds.map((id) => api.rejectAdminNomination(id, { reviewNote })))
await this.refreshAdminSeasonWorkspace(seasonId, { reloadAdmin: true })
},
loadAdminNominationLinkBlacklist() {
return api.getAdminNominationLinkBlacklist()
},
updateAdminNominationLinkBlacklist(payload: UpdateNominationLinkBlacklistPayload) {
return api.updateAdminNominationLinkBlacklist(payload)
},
addAdminNominationLinkBlacklistEntry(payload: AddNominationLinkBlacklistEntryPayload) {
return api.addAdminNominationLinkBlacklistEntry(payload)
},
async resolveRiskFlag(riskFlagId: number, payload: ResolveRiskFlagPayload) {
const result = await api.resolveRiskFlag(riskFlagId, payload)
await Promise.all([
@@ -328,6 +428,23 @@ export const useAwardsStore = defineStore('awards', {
async updateAdminRiskRules(payload: import('../types/awards').UpdateRiskRulesPayload) {
return api.updateAdminRiskRules(payload)
},
async loadAdminOptionalFeatureSettings() {
this.adminOptionalFeatureSettings = await api.getAdminOptionalFeatureSettings()
return this.adminOptionalFeatureSettings
},
async updateAdminOptionalFeatureSettings(payload: UpdateOptionalFeatureSettingsPayload) {
this.adminOptionalFeatureSettings = await api.updateAdminOptionalFeatureSettings(payload)
await this.loadHomeData()
return this.adminOptionalFeatureSettings
},
async loadAdminWorkflowRules() {
this.adminWorkflowRules = await api.getAdminWorkflowRules()
return this.adminWorkflowRules
},
async updateAdminWorkflowRules(payload: UpdateWorkflowRulesPayload) {
this.adminWorkflowRules = await api.updateAdminWorkflowRules(payload)
return this.adminWorkflowRules
},
async updateAdminSiteSettings(payload: UpdateSiteSettingsPayload) {
const result = await api.updateAdminSiteSettings(payload)
this.adminSiteSettings = await api.getAdminSiteSettings()
+55 -1
View File
@@ -1,13 +1,18 @@
import { ApiRequestError } from '../../lib/http'
import type {
AdminDashboardResponse,
AdminOptionalFeatureSettingsResponse,
AdminRiskFlag,
AdminRiskFlagsResponse,
AdminSeasonDetailResponse,
AdminSeasonListItem,
AdminShowactApplicationItem,
AdminSiteSettingsResponse,
AdminSponsorItem,
AdminWorkflowRulesResponse,
DatabaseHealthResponse,
OverviewResponse,
PublicSponsorsResponse,
SeasonCategoriesResponse,
WinnerArchiveResponse,
} from '../../types/awards'
@@ -39,10 +44,26 @@ export function createEmptyOverview(): OverviewResponse {
socialLinks: [],
footerLinks: [],
},
featureFlags: {
clipSubmissionsEnabled: false,
clipReviewEnabled: true,
clipAdminMenuVisible: true,
clipSubmissionDisabledMessage: 'Clip-Einreichungen sind aktuell geschlossen.',
showactApplicationsEnabled: false,
showactApplicationDisabledMessage: 'Showact-Bewerbungen sind aktuell geschlossen.',
sponsorsVisible: true,
},
faq: [],
}
}
export function createEmptyPublicSponsors(year = new Date().getFullYear()): PublicSponsorsResponse {
return {
year,
items: [],
}
}
export function createEmptyCategories(): SeasonCategoriesResponse {
return {
seasonId: 0,
@@ -81,6 +102,12 @@ export function createEmptyAdminRiskFlagsResponse(): AdminRiskFlagsResponse {
}
}
export function createEmptyAdminWorkflowRulesResponse(): AdminWorkflowRulesResponse {
return {
rules: [],
}
}
export function createEmptyAdminSeasonDetail(): AdminSeasonDetailResponse {
return {
id: 0,
@@ -122,11 +149,25 @@ export function createEmptyAdminSiteSettings(): AdminSiteSettingsResponse {
contactContent: '',
sponsorsUrl: '',
sponsorsContent: '',
showactsUrl: '',
showactsContent: '',
socialLinks: [],
faq: [],
}
}
export function createEmptyAdminOptionalFeatureSettings(): AdminOptionalFeatureSettingsResponse {
return {
clipSubmissionsEnabled: false,
clipReviewEnabled: true,
clipAdminMenuVisible: true,
clipSubmissionDisabledMessage: 'Clip-Einreichungen sind aktuell geschlossen.',
showactApplicationsEnabled: false,
showactApplicationDisabledMessage: 'Showact-Bewerbungen sind aktuell geschlossen.',
sponsorsVisible: true,
}
}
export function createEmptyDatabaseHealth(): DatabaseHealthResponse {
return {
provider: 'postgres',
@@ -143,10 +184,15 @@ export function createAwardsState() {
overview: createEmptyOverview(),
categories: createEmptyCategories(),
archive: createEmptyArchive(),
publicSponsors: createEmptyPublicSponsors(),
admin: createEmptyAdminDashboard(),
adminSeasons: [] as AdminSeasonListItem[],
adminSeasonDetail: createEmptyAdminSeasonDetail(),
adminSiteSettings: createEmptyAdminSiteSettings(),
adminOptionalFeatureSettings: createEmptyAdminOptionalFeatureSettings(),
adminWorkflowRules: createEmptyAdminWorkflowRulesResponse(),
adminSponsors: [] as AdminSponsorItem[],
adminShowactApplications: [] as AdminShowactApplicationItem[],
adminRiskHistory: [] as AdminRiskFlag[],
adminRiskFlagsPage: createEmptyAdminRiskFlagsResponse(),
adminRiskHistoryPage: createEmptyAdminRiskFlagsResponse(),
@@ -167,7 +213,15 @@ export function normalizeSeasonDetail(detail: AdminSeasonDetailResponse): AdminS
return {
...detail,
categories: detail.categories ?? [],
candidates: detail.candidates ?? [],
candidates: (detail.candidates ?? []).map((candidate) => ({
...candidate,
acceptanceStatus: candidate.acceptanceStatus ?? 'open',
acceptanceNote: candidate.acceptanceNote ?? null,
clipCompilationUrl: candidate.clipCompilationUrl ?? null,
clipCompilationTitle: candidate.clipCompilationTitle ?? null,
clipCompilationPlatform: candidate.clipCompilationPlatform ?? null,
clipEmbedStatus: candidate.clipEmbedStatus ?? 'unchecked',
})),
pendingNominations: detail.pendingNominations ?? [],
reviewedNominations: detail.reviewedNominations ?? [],
results: detail.results ?? [],