Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season

Features:
- Category viewer ranges + subcategory templates (admin group modal, tree workspace)
- Nomination enrichment via TwitchTracker API (NominationEnrichmentService,
  TwitchTrackerViewerStatsProvider) with admin tracking rules editor
- Nomination group tracker: CategoryGroupName as primary identifier,
  CategoryId stays as nullable legacy field; StreamerIdentity table
- Dynamic showact application form builder (AdminShowactFormBuilder,
  ShowactApplicationSchedule)
- Session idle timeout setting (AdminSessionTimeoutCard)
- Share URLs for X and Discord (SiteSettings, public extras)
- Workflow rules now stored per season (falls back to global SiteSettings)
- New admin routes: settings/access, settings/workflows, tracking-rules
- New admin review workspace with subcategory tabs
- AdminCategoriesView rebuilt with group/subcategory modals

Migrations (all additive):
- AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges,
  AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates,
  AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule,
  AddSeasonWorkflowRulesJson

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-28 23:32:21 +02:00
parent 4b2c5fa15d
commit b53c7fb736
178 changed files with 28933 additions and 1908 deletions
@@ -3,7 +3,7 @@ import { computed, type ComputedRef, type Ref } from 'vue'
import { buildClipEmbed } from '../../lib/clipEmbeds'
import { useAuthStore } from '../../stores/auth'
import { useAwardsStore } from '../../stores/awards'
import type { HomeDisplayCategory, HomeInteractionModalKind } from './homeLandingTypes'
import type { HomeDisplayCategory, HomeInteractionModalKind, HomeLandingCategory } from './homeLandingTypes'
type AwardsStore = ReturnType<typeof useAwardsStore>
type AuthStore = ReturnType<typeof useAuthStore>
@@ -31,11 +31,52 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
const displayCategories = computed<HomeDisplayCategory[]>(() =>
store.categories.categories.map((category, index) => ({
id: String(category.id),
groupName: category.groupName,
name: category.name,
icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦',
maxNomineesPerUser: category.maxNomineesPerUser,
candidates: category.candidates,
})),
)
const landingCategories = computed<HomeLandingCategory[]>(() => {
if (store.overview.featuredCategories.length > 0) {
return store.overview.featuredCategories.map((category, index) => {
const matchingSubcategories = displayCategories.value.filter((item) => item.groupName === category.groupName)
const candidateCount = matchingSubcategories.reduce((sum, item) => sum + item.candidates.length, 0)
return {
id: String(category.id),
groupName: category.groupName,
name: category.groupName,
icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦',
description: category.description,
maxNomineesPerUser: category.maxNomineesPerUser,
candidateCount,
subcategoryCount: matchingSubcategories.length,
}
})
}
const seenGroups = new Set<string>()
return displayCategories.value.flatMap((category, index) => {
const groupKey = category.groupName.trim().toLowerCase()
if (seenGroups.has(groupKey)) {
return []
}
seenGroups.add(groupKey)
const matchingSubcategories = displayCategories.value.filter((item) => item.groupName.trim().toLowerCase() === groupKey)
return [{
id: category.id,
groupName: category.groupName,
name: category.groupName,
icon: CATEGORY_ICONS[index % CATEGORY_ICONS.length] ?? '✦',
description: '',
maxNomineesPerUser: category.maxNomineesPerUser,
candidateCount: matchingSubcategories.reduce((sum, item) => sum + item.candidates.length, 0),
subcategoryCount: matchingSubcategories.length,
}]
})
})
const candidateCount = computed(() =>
displayCategories.value.reduce((sum, category) => sum + category.candidates.length, 0),
)
@@ -82,6 +123,7 @@ export function useHomeLandingOverviewPresentation(store: AwardsStore, authStore
showStartsAt,
currentYear,
displayCategories,
landingCategories,
candidateCount,
bootstrapArchiveYears,
formatRange,
@@ -194,9 +236,12 @@ function buildCatList(
const done = votes.value[category.id] != null
return {
name: category.name,
groupName: category.groupName,
displayName: `${category.groupName} · ${category.name}`,
icon: category.icon,
idx: index,
done,
candidateCount: category.candidates.length,
onClick: () => setCat(index),
rowStyle: baseRow + (active ? 'background:#f1ecfb;border-color:#d8c9f2;color:#5f44ad;' : 'border-color:transparent;color:#6f6685;'),
iconStyle: "flex:none;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;font-size:14px;" + (active ? 'background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;' : 'background:#efe7fb;color:#9a7fce;'),