Files
vtuber-awards/Backend/Endpoints/PublicSeasonCategoryReadEndpoints.cs
T
AzuTear b53c7fb736 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>
2026-06-28 23:32:21 +02:00

96 lines
4.3 KiB
C#

using Backend.Contracts;
using Backend.Data;
using Backend.Common;
using Backend.Services;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static partial class PublicEndpoints
{
private static async Task<IResult> GetSeasonCategories(int year, AwardsDbContext db)
{
var season = await db.Seasons
.AsNoTracking()
.Include(item => item.Categories.OrderBy(category => category.SortOrder))
.ThenInclude(category => category.Candidates.OrderBy(candidate => candidate.DisplayName))
.FirstOrDefaultAsync(item => item.Year == year);
if (season is null)
{
return Results.NotFound();
}
var phaseKey = SeasonMappings.NormalizePhaseKey(season.CurrentPhase);
var subcategoryTemplates = SeasonSubcategoryTemplateSettings.Read(season, season.Categories);
var publicCategories = season.Categories
.Where(category => SeasonSubcategoryTemplateSettings.MatchesTemplate(category, subcategoryTemplates))
.Where(category => ShouldExposePublicCategory(phaseKey, category.Candidates.Count))
.ToArray();
var publicCategoryIds = publicCategories.Select(category => category.Id).ToArray();
var approvedClips = await db.ClipSubmissions
.AsNoTracking()
.Where(item =>
item.SeasonId == season.Id
&& item.Status == "approved"
&& item.CategoryId != null
&& publicCategoryIds.Contains(item.CategoryId.Value))
.Select(item => new PublicCandidateClip(
item.CategoryId,
item.CandidateId,
item.Creator,
item.ClipUrl,
item.Title,
item.Platform,
item.CreatedAt,
item.ReviewedAt))
.ToArrayAsync();
var clipsByCandidateId = BuildCandidateClipLookup(approvedClips);
var clipsByCreatorKey = BuildCreatorClipLookup(approvedClips);
return Results.Ok(new SeasonCategoriesResponse(
season.Id,
season.Year,
publicCategories.Select(category => new PublicCategoryDetailDto(
category.Id,
category.Name,
category.GroupName,
category.Description,
category.MaxNomineesPerUser,
category.ViewerRangeMin,
category.ViewerRangeMax,
category.Candidates
.Where(candidate => !string.Equals(candidate.AcceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase))
.Select(candidate =>
{
var clip = ResolveCandidateClip(candidate, clipsByCandidateId, clipsByCreatorKey);
var candidateClipUrl = string.IsNullOrWhiteSpace(candidate.ClipCompilationUrl)
|| string.Equals(candidate.ClipEmbedStatus, "blocked", StringComparison.OrdinalIgnoreCase)
? null
: candidate.ClipCompilationUrl.Trim();
var clipUrl = candidateClipUrl ?? clip?.ClipUrl;
var clipTitle = candidateClipUrl is not null
? string.IsNullOrWhiteSpace(candidate.ClipCompilationTitle) ? "Highlight-Clip ansehen" : candidate.ClipCompilationTitle.Trim()
: clip?.Title;
var clipPlatform = candidateClipUrl is not null
? string.IsNullOrWhiteSpace(candidate.ClipCompilationPlatform) ? candidate.Platform : candidate.ClipCompilationPlatform.Trim()
: clip?.Platform;
var clipEmbedStatus = candidateClipUrl is not null
? candidate.ClipEmbedStatus
: null;
return new CandidateSummaryDto(
candidate.Id,
candidate.DisplayName,
candidate.ChannelSlug,
SeasonMappings.BuildProfileUrl(candidate.Platform, candidate.ChannelSlug),
candidate.Platform,
clipUrl,
clipTitle,
clipPlatform,
clipEmbedStatus);
}).ToArray()))
.ToArray()));
}
}