Add winner archive, host image upload and live demo data
Deliver the demo-ready feature set and seed data so the live site can be presented end to end: - Winner archive: ArchivedWinner domain, admin CRUD endpoints/view/manager and public archive surface, backed by AddArchivedWinners migration. - Host presentation: host image upload and artist name on SiteSettings with public image endpoint and supporting migrations. - Clip submissions: idempotent table-ensure migration plus current-season demo clips for review workflows. - Demo seed data: sponsors, share links and 2025 archived winners, with a guarded RemoveDemoSeasons cleanup; all seeds guard against real data. - EnsureRuntimeSchemaParity migration to align runtime schema defensively. - Admin/home UI refinements; remove unused team role permissions modal and dead share-quick-links code. All seed and schema migrations are idempotent (IF NOT EXISTS / ON CONFLICT) and skip when real season data is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,11 +23,11 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
.FirstOrDefaultAsync(item => item.Id == 1);
|
||||
var trackingRules = TrackingRulesSettings.Read(settings);
|
||||
|
||||
var candidates = await db.Candidates
|
||||
var candidateRows = await db.Candidates
|
||||
.AsNoTracking()
|
||||
.Where(item => item.SeasonId == seasonId)
|
||||
.OrderBy(item => item.DisplayName)
|
||||
.Select(item => new AdminCandidateItemDto(
|
||||
.Select(item => new AdminCandidateRow(
|
||||
item.Id,
|
||||
item.CategoryId,
|
||||
item.StreamerIdentityId,
|
||||
@@ -43,7 +43,7 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
item.ClipEmbedStatus))
|
||||
.ToArrayAsync();
|
||||
|
||||
var candidateCounts = candidates
|
||||
var candidateCounts = candidateRows
|
||||
.GroupBy(item => item.CategoryId)
|
||||
.ToDictionary(grouping => grouping.Key, grouping => grouping.Count());
|
||||
|
||||
@@ -217,6 +217,11 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
item.CategoryId,
|
||||
item.CandidateId))
|
||||
.ToArrayAsync();
|
||||
var candidates = BuildCandidateItems(
|
||||
candidateRows,
|
||||
pendingNominationRows,
|
||||
reviewedNominationRows,
|
||||
votingEntryRows);
|
||||
var votingWorkspace = BuildVotingWorkspace(
|
||||
categories,
|
||||
candidates,
|
||||
@@ -281,6 +286,21 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
int CategoryId,
|
||||
int CandidateId);
|
||||
|
||||
private sealed record AdminCandidateRow(
|
||||
int Id,
|
||||
int CategoryId,
|
||||
int? StreamerIdentityId,
|
||||
string DisplayName,
|
||||
string ChannelSlug,
|
||||
string Platform,
|
||||
int NominationTally,
|
||||
string AcceptanceStatus,
|
||||
string? AcceptanceNote,
|
||||
string? ClipCompilationUrl,
|
||||
string? ClipCompilationTitle,
|
||||
string? ClipCompilationPlatform,
|
||||
string ClipEmbedStatus);
|
||||
|
||||
private sealed record AdminNominationRow(
|
||||
int Id,
|
||||
int? CategoryId,
|
||||
@@ -314,6 +334,79 @@ public static partial class AdminSeasonManagementEndpoints
|
||||
string? ReviewedByTwitchId,
|
||||
DateTimeOffset? ReviewedAt);
|
||||
|
||||
private static AdminCandidateItemDto[] BuildCandidateItems(
|
||||
AdminCandidateRow[] candidateRows,
|
||||
AdminNominationRow[] pendingNominationRows,
|
||||
AdminNominationRow[] reviewedNominationRows,
|
||||
AdminVotingEntryRow[] votingEntryRows)
|
||||
{
|
||||
var allNominationRows = pendingNominationRows
|
||||
.Concat(reviewedNominationRows)
|
||||
.ToArray();
|
||||
var voteCountByCandidate = votingEntryRows
|
||||
.GroupBy(item => item.CandidateId)
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
|
||||
return candidateRows
|
||||
.Select(item => new AdminCandidateItemDto(
|
||||
item.Id,
|
||||
item.CategoryId,
|
||||
item.StreamerIdentityId,
|
||||
item.DisplayName,
|
||||
item.ChannelSlug,
|
||||
item.Platform,
|
||||
ResolveCandidateAvgViewers(item, allNominationRows),
|
||||
voteCountByCandidate.GetValueOrDefault(item.Id, 0),
|
||||
item.NominationTally,
|
||||
item.AcceptanceStatus,
|
||||
item.AcceptanceNote,
|
||||
item.ClipCompilationUrl,
|
||||
item.ClipCompilationTitle,
|
||||
item.ClipCompilationPlatform,
|
||||
item.ClipEmbedStatus))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static int? ResolveCandidateAvgViewers(AdminCandidateRow candidate, AdminNominationRow[] nominationRows)
|
||||
{
|
||||
var directMatch = nominationRows
|
||||
.Where(item => item.CandidateId == candidate.Id && item.AvgViewers.HasValue)
|
||||
.OrderByDescending(item => item.TrackerCheckedAt ?? item.ReviewedAt ?? item.CreatedAt)
|
||||
.Select(item => item.AvgViewers)
|
||||
.FirstOrDefault();
|
||||
if (directMatch.HasValue)
|
||||
{
|
||||
return directMatch.Value;
|
||||
}
|
||||
|
||||
if (candidate.StreamerIdentityId.HasValue)
|
||||
{
|
||||
var identityMatch = nominationRows
|
||||
.Where(item => item.StreamerIdentityId == candidate.StreamerIdentityId && item.AvgViewers.HasValue)
|
||||
.OrderByDescending(item => item.TrackerCheckedAt ?? item.ReviewedAt ?? item.CreatedAt)
|
||||
.Select(item => item.AvgViewers)
|
||||
.FirstOrDefault();
|
||||
if (identityMatch.HasValue)
|
||||
{
|
||||
return identityMatch.Value;
|
||||
}
|
||||
}
|
||||
|
||||
var normalizedChannel = candidate.ChannelSlug.Trim().TrimStart('@').ToLowerInvariant();
|
||||
if (string.IsNullOrWhiteSpace(normalizedChannel))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return nominationRows
|
||||
.Where(item =>
|
||||
item.AvgViewers.HasValue
|
||||
&& string.Equals(item.ResolvedChannel?.Trim().TrimStart('@'), normalizedChannel, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(item => item.TrackerCheckedAt ?? item.ReviewedAt ?? item.CreatedAt)
|
||||
.Select(item => item.AvgViewers)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static AdminNominationReviewItemDto ToNominationReviewItem(
|
||||
AdminNominationRow item,
|
||||
IEnumerable<dynamic> categoryRows,
|
||||
|
||||
Reference in New Issue
Block a user