Files
vtuber-awards/Backend/Endpoints/PublicUserParticipationEndpoints.cs
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

92 lines
3.1 KiB
C#

using Backend.Contracts;
using Backend.Data;
using Backend.Services;
using Microsoft.EntityFrameworkCore;
namespace Backend.Endpoints;
public static partial class PublicEndpoints
{
private static async Task<IResult> GetUserParticipation(
HttpContext context,
int year,
AwardsDbContext db,
IUserSessionService userSessionService)
{
var session = await userSessionService.ResolveSessionAsync(context, context.RequestAborted);
if (session is null)
{
return Results.Unauthorized();
}
var season = await db.Seasons
.AsNoTracking()
.FirstOrDefaultAsync(item => item.Year == year);
if (season is null)
{
return Results.NotFound();
}
var nominations = await db.Nominations
.AsNoTracking()
.Where(item => item.SeasonId == season.Id && item.SubmittedByTwitchId == session.TwitchUserId)
.OrderBy(item => item.CategoryId)
.ThenBy(item => item.Id)
.Select(item => new
{
item.CategoryId,
item.CategoryGroupName,
item.Status,
Nominee = item.CandidateId != null
? item.Candidate!.DisplayName
: item.CandidateText ?? item.StreamUrl,
})
.ToArrayAsync();
var groupedNominations = nominations
.Where(item => item.Status != "rejected" && item.Status != "superseded")
.Where(item => !string.IsNullOrWhiteSpace(item.Nominee))
.GroupBy(item => new { item.CategoryId, item.CategoryGroupName })
.Select(group => new UserNominationStateDto(
group.Key.CategoryId,
group.Key.CategoryGroupName,
group.Select(item => item.Nominee!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray()))
.ToArray();
var votes = await db.VoteEntries
.AsNoTracking()
.Where(item => item.Ballot.SeasonId == season.Id && item.Ballot.SubmittedByTwitchId == session.TwitchUserId)
.OrderBy(item => item.CategoryId)
.Select(item => new UserVoteStateDto(item.CategoryId, item.CandidateId))
.ToArrayAsync();
var clips = await db.ClipSubmissions
.AsNoTracking()
.Where(item => item.SeasonId == season.Id && item.SubmittedByTwitchId == session.TwitchUserId)
.OrderByDescending(item => item.CreatedAt)
.Take(12)
.Select(item => new UserClipSubmissionStateDto(
item.Id,
item.CategoryId,
item.ClipUrl,
item.Title,
item.Creator,
item.Platform,
item.Status,
item.CreatedAt,
item.ReviewNote,
item.ReviewedAt))
.ToArrayAsync();
return Results.Ok(new UserParticipationResponse(
season.Id,
season.Year,
groupedNominations,
votes,
clips));
}
}