b53c7fb736
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>
170 lines
7.3 KiB
C#
170 lines
7.3 KiB
C#
using Backend.Common;
|
|
using Backend.Contracts;
|
|
using Backend.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Backend.Endpoints;
|
|
|
|
public static partial class PublicEndpoints
|
|
{
|
|
private static async Task<IResult> GetOverview(AwardsDbContext db)
|
|
{
|
|
var siteSettings = await db.SiteSettings.AsNoTracking().FirstOrDefaultAsync(item => item.Id == 1);
|
|
var season = await db.Seasons
|
|
.AsNoTracking()
|
|
.Include(item => item.Categories.OrderBy(category => category.SortOrder))
|
|
.ThenInclude(category => category.Candidates)
|
|
.OrderByDescending(item => item.Year)
|
|
.FirstOrDefaultAsync(item => item.IsCurrent);
|
|
|
|
if (season is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
if (siteSettings is null)
|
|
{
|
|
return Results.Problem("Site settings are missing.");
|
|
}
|
|
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var showactApplicationsOpenNow = ShowactApplicationSchedule.IsOpenNow(siteSettings, today);
|
|
|
|
var canExposeCurrentSeasonWinners = CanExposeCurrentSeasonWinners(season.CurrentPhase);
|
|
|
|
var winnerPreviewRows = await db.Results
|
|
.AsNoTracking()
|
|
.Include(result => result.Season)
|
|
.Include(result => result.Candidate)
|
|
.Where(result =>
|
|
result.Season.Year < season.Year
|
|
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
|
|
.OrderByDescending(result => result.Season.Year)
|
|
.ThenBy(result => result.CategoryName)
|
|
.Take(8)
|
|
.Select(result => new
|
|
{
|
|
Year = result.Season.Year,
|
|
result.CategoryName,
|
|
WinnerName = result.Candidate.DisplayName,
|
|
WinnerSlug = result.Candidate.ChannelSlug,
|
|
WinnerPlatform = result.Candidate.Platform,
|
|
ClipUrl = result.Candidate.ClipCompilationUrl,
|
|
ClipTitle = result.Candidate.ClipCompilationTitle,
|
|
ClipPlatform = result.Candidate.ClipCompilationPlatform,
|
|
ClipEmbedStatus = result.Candidate.ClipEmbedStatus,
|
|
})
|
|
.ToArrayAsync();
|
|
|
|
var winnerPreviewItems = winnerPreviewRows
|
|
.Select(result => new WinnerPreviewDto(
|
|
result.Year,
|
|
result.CategoryName,
|
|
result.WinnerName,
|
|
result.WinnerSlug,
|
|
result.WinnerPlatform,
|
|
SeasonMappings.BuildProfileUrl(result.WinnerPlatform, result.WinnerSlug),
|
|
result.ClipUrl,
|
|
result.ClipTitle,
|
|
result.ClipPlatform,
|
|
result.ClipEmbedStatus))
|
|
.ToArray();
|
|
|
|
var archiveYearRows = await db.Results
|
|
.AsNoTracking()
|
|
.Where(result =>
|
|
result.Season.Year < season.Year
|
|
|| canExposeCurrentSeasonWinners && result.Season.Year == season.Year)
|
|
.GroupBy(result => result.Season.Year)
|
|
.Select(group => new
|
|
{
|
|
Year = group.Key,
|
|
WinnerCount = group.Count(),
|
|
})
|
|
.OrderByDescending(item => item.Year)
|
|
.ToArrayAsync();
|
|
|
|
var archiveYears = archiveYearRows
|
|
.Select(item => new ArchiveYearDto(item.Year, item.WinnerCount))
|
|
.ToArray();
|
|
|
|
var phaseKey = SeasonMappings.NormalizePhaseKey(season.CurrentPhase);
|
|
var publicCategories = season.Categories
|
|
.Where(category => ShouldExposePublicCategory(phaseKey, category.Candidates.Count))
|
|
.ToArray();
|
|
var featuredCategories = publicCategories
|
|
.GroupBy(category => category.GroupName.Trim(), StringComparer.OrdinalIgnoreCase)
|
|
.Select(group =>
|
|
{
|
|
var ordered = group
|
|
.OrderBy(category => category.SortOrder)
|
|
.ThenBy(category => category.Name, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
var first = ordered[0];
|
|
var maxNomineesPerUser = ordered
|
|
.Select(category => category.MaxNomineesPerUser)
|
|
.Where(value => value > 0)
|
|
.DefaultIfEmpty(3)
|
|
.Max();
|
|
|
|
return new FeaturedCategoryDto(
|
|
first.Id,
|
|
first.GroupName,
|
|
first.GroupName,
|
|
first.Description,
|
|
maxNomineesPerUser);
|
|
})
|
|
.OrderBy(category => publicCategories
|
|
.Where(item => string.Equals(item.GroupName, category.GroupName, StringComparison.OrdinalIgnoreCase))
|
|
.Min(item => item.SortOrder))
|
|
.ThenBy(category => category.GroupName, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
var response = new OverviewResponse(
|
|
season.Id,
|
|
season.Year,
|
|
season.Name,
|
|
season.ShowDate,
|
|
season.ShowStartsAt,
|
|
SeasonMappings.NormalizeSeasonStreamUrl(season.ShowStreamUrl),
|
|
season.CurrentPhase,
|
|
season.IsCommunityOnly,
|
|
"Twitch",
|
|
new[]
|
|
{
|
|
new TimelineItem("nomination", "Nominierung", season.NominationStartsAt, season.NominationEndsAt, SeasonMappings.ResolveTimelineState("nomination", phaseKey)),
|
|
new TimelineItem("voting", "Voting", season.VotingStartsAt, season.VotingEndsAt, SeasonMappings.ResolveTimelineState("voting", phaseKey)),
|
|
new TimelineItem("preparation", "Aufbereitung", season.ReviewStartsAt, season.ReviewEndsAt, SeasonMappings.ResolveTimelineState("preparation", phaseKey)),
|
|
new TimelineItem("show", "Award Show", season.ShowDate, season.ShowDate, SeasonMappings.ResolveTimelineState("show", phaseKey)),
|
|
},
|
|
featuredCategories,
|
|
winnerPreviewItems,
|
|
archiveYears,
|
|
new PublicSiteContentDto(
|
|
siteSettings.HostDisplayName,
|
|
siteSettings.HostTagline,
|
|
siteSettings.NewsletterUrl,
|
|
siteSettings.ShareXUrl,
|
|
siteSettings.ShareDiscordUrl,
|
|
siteSettings.PrivacyEmail,
|
|
siteSettings.PrivacyPolicyContent,
|
|
SeasonMappings.ReadSocialLinks(siteSettings),
|
|
SeasonMappings.BuildFooterLinks(siteSettings)),
|
|
new PublicFeatureFlagsDto(
|
|
siteSettings.ClipSubmissionsEnabled,
|
|
siteSettings.ClipReviewEnabled,
|
|
string.IsNullOrWhiteSpace(siteSettings.ClipSubmissionDisabledMessage)
|
|
? "Clip-Einreichungen sind aktuell geschlossen."
|
|
: siteSettings.ClipSubmissionDisabledMessage,
|
|
showactApplicationsOpenNow,
|
|
siteSettings.ShowactApplicationStartsAt,
|
|
siteSettings.ShowactApplicationEndsAt,
|
|
string.IsNullOrWhiteSpace(siteSettings.ShowactApplicationDisabledMessage)
|
|
? "Showact-Bewerbungen sind aktuell geschlossen."
|
|
: siteSettings.ShowactApplicationDisabledMessage,
|
|
siteSettings.SponsorsVisible,
|
|
siteSettings.ShowactFormSchemaJson ?? "[]"),
|
|
SeasonMappings.ReadFaqItems(siteSettings));
|
|
|
|
return Results.Ok(response);
|
|
}
|
|
}
|