feat: add homepage components and API for settings and spielplan options

- Introduced new Vue components for homepage teasers: HomeLinksTeaser, HomeSpielplanTeamWidget, HomeTrainingTeaser, and HomeVereinsmeisterschaftenTeaser.
- Created XML layout for tablet app window dump.
- Implemented API endpoints for fetching and updating homepage settings.
- Added API for retrieving spielplan options, including team extraction logic.
This commit is contained in:
Torsten Schulz (local)
2026-05-29 15:37:45 +02:00
parent 1ea9596006
commit b8bdbf0a8d
39 changed files with 3867 additions and 163 deletions

View File

@@ -0,0 +1,59 @@
import { getUserFromToken } from '../../utils/auth.js'
function normalizeConfig(config) {
if (!config || typeof config !== 'object') return undefined
const normalized = {
season: config.season ? String(config.season) : undefined,
teamName: config.teamName ? String(config.teamName) : undefined,
teamAgeGroup: config.teamAgeGroup ? String(config.teamAgeGroup) : undefined
}
if (!normalized.season && !normalized.teamName && !normalized.teamAgeGroup) {
return undefined
}
return normalized
}
function parseSections(value) {
if (!value || typeof value !== 'string') return []
try {
const parsed = JSON.parse(value)
if (!Array.isArray(parsed)) return []
return parsed
.filter(section => section?.id)
.map((section, index) => ({
key: section.key ? String(section.key) : `${String(section.id)}-${index}`,
id: String(section.id),
enabled: section.enabled !== false,
config: normalizeConfig(section.config)
}))
} catch {
return []
}
}
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
const user = token ? await getUserFromToken(token) : null
const rawCookieSections = getCookie(event, 'homepage_sections')
const cookieSections = parseSections(rawCookieSections)
const userSections = Array.isArray(user?.homepageSettings?.sections)
? user.homepageSettings.sections
.filter(section => section?.id)
.map((section, index) => ({
key: section.key ? String(section.key) : `${String(section.id)}-${index}`,
id: String(section.id),
enabled: section.enabled !== false,
config: normalizeConfig(section.config)
}))
: []
const isLoggedIn = !!user
return {
isLoggedIn,
storage: isLoggedIn ? 'user' : 'cookie',
sections: isLoggedIn ? userSections : cookieSections
}
})

View File

@@ -0,0 +1,81 @@
import { getUserFromToken, readUsers, writeUsers } from '../../utils/auth.js'
function normalizeConfig(config) {
if (!config || typeof config !== 'object') return undefined
const normalized = {
season: config.season ? String(config.season) : undefined,
teamName: config.teamName ? String(config.teamName) : undefined,
teamAgeGroup: config.teamAgeGroup ? String(config.teamAgeGroup) : undefined
}
if (!normalized.season && !normalized.teamName && !normalized.teamAgeGroup) {
return undefined
}
return normalized
}
function normalizeSections(sections) {
if (!Array.isArray(sections)) return []
const seenKeys = new Set()
return sections
.filter(section => section?.id)
.map((section, index) => ({
key: section.key ? String(section.key) : `${String(section.id)}-${index}`,
id: String(section.id),
enabled: section.enabled !== false,
config: normalizeConfig(section.config)
}))
.filter(section => {
if (seenKeys.has(section.key)) return false
seenKeys.add(section.key)
return true
})
}
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const sections = normalizeSections(body?.sections)
const token = getCookie(event, 'auth_token') || getHeader(event, 'authorization')?.replace(/^Bearer\s+/i, '')
const authUser = token ? await getUserFromToken(token) : null
if (!authUser) {
setCookie(event, 'homepage_sections', JSON.stringify(sections), {
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
maxAge: 60 * 60 * 24 * 180
})
return {
success: true,
storage: 'cookie',
sections
}
}
const users = await readUsers()
const userIndex = users.findIndex(user => user.id === authUser.id)
if (userIndex < 0) {
throw createError({
statusCode: 404,
message: 'Benutzer nicht gefunden.'
})
}
const current = users[userIndex]
users[userIndex] = {
...current,
homepageSettings: {
sections,
updatedAt: new Date().toISOString()
}
}
await writeUsers(users)
return {
success: true,
storage: 'user',
sections
}
})

View File

@@ -0,0 +1,61 @@
import { listSpielplanSeasons, readSpielplanData, validateSeasonSlug } from '../../utils/spielplan-data.js'
function teamLabel(teamName, teamAgeGroup) {
const name = String(teamName || '').trim()
const age = String(teamAgeGroup || '').trim()
if (!name) return ''
const isYouth = age.toLowerCase().includes('jugend') || name.toLowerCase().includes('jugend')
return isYouth ? `(J) ${name}` : name
}
function extractHarheimerTeams(rows) {
const seen = new Set()
const teams = []
const addTeam = (teamName, teamAgeGroup) => {
const name = String(teamName || '').trim()
if (!name) return
const age = String(teamAgeGroup || '').trim()
const key = `${name}||${age}`
if (seen.has(key)) return
seen.add(key)
teams.push({
key,
label: teamLabel(name, age),
teamName: name,
teamAgeGroup: age
})
}
for (const row of rows || []) {
if (String(row.HeimVereinName || '').trim() === 'Harheimer TC') {
addTeam(row.HeimMannschaft, row.HeimMannschaftAltersklasse)
}
if (String(row.GastVereinName || '').trim() === 'Harheimer TC') {
addTeam(row.GastMannschaft, row.GastMannschaftAltersklasse)
}
}
return teams.sort((a, b) => a.label.localeCompare(b.label, 'de'))
}
export default defineEventHandler(async (event) => {
const query = getQuery(event)
if (query.season && !validateSeasonSlug(query.season)) {
throw createError({
statusCode: 400,
message: 'Ungültiger Saison-Slug.'
})
}
const seasons = await listSpielplanSeasons()
const selectedSeason = String(query.season || seasons[0]?.slug || '')
const dataResult = await readSpielplanData(selectedSeason ? { season: selectedSeason } : {})
return {
success: true,
selectedSeason,
seasons,
teams: extractHarheimerTeams(dataResult.data)
}
})