Add votes-final flag, warn on missing votes, and sync phases with results toggle
This commit is contained in:
@@ -5,3 +5,4 @@ public record SuggestionRequest(string Name, string? Genre, string? Description,
|
|||||||
public record SuggestionDto(int Id, string Name, string? Genre, string? Description, string? ScreenshotUrl, string? YoutubeUrl, string? GameUrl, int? MinPlayers, int? MaxPlayers);
|
public record SuggestionDto(int Id, string Name, string? Genre, string? Description, string? ScreenshotUrl, string? YoutubeUrl, string? GameUrl, int? MinPlayers, int? MaxPlayers);
|
||||||
public record VoteRequest(int SuggestionId, int Score);
|
public record VoteRequest(int SuggestionId, int Score);
|
||||||
public record ResultsOpenRequest(bool ResultsOpen);
|
public record ResultsOpenRequest(bool ResultsOpen);
|
||||||
|
public record VoteFinalizeRequest(bool Final);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class AppDbContext : DbContext
|
|||||||
builder.Property(p => p.PasswordSalt).IsRequired();
|
builder.Property(p => p.PasswordSalt).IsRequired();
|
||||||
builder.Property(p => p.IsAdmin).HasDefaultValue(false);
|
builder.Property(p => p.IsAdmin).HasDefaultValue(false);
|
||||||
builder.Property(p => p.CurrentPhase).HasDefaultValue(Phase.Suggest);
|
builder.Property(p => p.CurrentPhase).HasDefaultValue(Phase.Suggest);
|
||||||
|
builder.Property(p => p.VotesFinal).HasDefaultValue(false);
|
||||||
builder.HasMany(p => p.Suggestions)
|
builder.HasMany(p => p.Suggestions)
|
||||||
.WithOne(s => s.Player!)
|
.WithOne(s => s.Player!)
|
||||||
.HasForeignKey(s => s.PlayerId)
|
.HasForeignKey(s => s.PlayerId)
|
||||||
|
|||||||
227
Data/Migrations/20260204215138_VotesFinalFlag.Designer.cs
generated
Normal file
227
Data/Migrations/20260204215138_VotesFinalFlag.Designer.cs
generated
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using GameList.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GameList.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260204215138_VotesFinalFlag")]
|
||||||
|
partial class VotesFinalFlag
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.AppState", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("ResultsOpen")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("AppState");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
ResultsOpen = false,
|
||||||
|
UpdatedAt = new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Player", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("CurrentPhase")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdmin")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastLoginAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUsername")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(24)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<byte[]>("PasswordSalt")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(24)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("VotesFinal")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUsername")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Players");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("GameUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Genre")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("MaxPlayers")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("MinPlayers")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("PlayerId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ScreenshotUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("YoutubeUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PlayerId");
|
||||||
|
|
||||||
|
b.ToTable("Suggestions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Vote", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("PlayerId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Score")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("SuggestionId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SuggestionId");
|
||||||
|
|
||||||
|
b.HasIndex("PlayerId", "SuggestionId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GameList.Domain.Player", "Player")
|
||||||
|
.WithMany("Suggestions")
|
||||||
|
.HasForeignKey("PlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Player");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Vote", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("GameList.Domain.Player", "Player")
|
||||||
|
.WithMany("Votes")
|
||||||
|
.HasForeignKey("PlayerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("GameList.Domain.Suggestion", "Suggestion")
|
||||||
|
.WithMany("Votes")
|
||||||
|
.HasForeignKey("SuggestionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Player");
|
||||||
|
|
||||||
|
b.Navigation("Suggestion");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Player", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Suggestions");
|
||||||
|
|
||||||
|
b.Navigation("Votes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Votes");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Data/Migrations/20260204215138_VotesFinalFlag.cs
Normal file
29
Data/Migrations/20260204215138_VotesFinalFlag.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GameList.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class VotesFinalFlag : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "VotesFinal",
|
||||||
|
table: "Players",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "VotesFinal",
|
||||||
|
table: "Players");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,6 +86,11 @@ namespace GameList.Data.Migrations
|
|||||||
.HasMaxLength(24)
|
.HasMaxLength(24)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("VotesFinal")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedUsername")
|
b.HasIndex("NormalizedUsername")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class Player
|
|||||||
public DateTimeOffset? LastLoginAt { get; set; }
|
public DateTimeOffset? LastLoginAt { get; set; }
|
||||||
public bool IsAdmin { get; set; }
|
public bool IsAdmin { get; set; }
|
||||||
public Phase CurrentPhase { get; set; } = Phase.Suggest;
|
public Phase CurrentPhase { get; set; } = Phase.Suggest;
|
||||||
|
public bool VotesFinal { get; set; }
|
||||||
|
|
||||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ public static class AdminEndpoints
|
|||||||
state.ResultsOpen = request.ResultsOpen;
|
state.ResultsOpen = request.ResultsOpen;
|
||||||
state.UpdatedAt = DateTimeOffset.UtcNow;
|
state.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
if (request.ResultsOpen)
|
||||||
|
{
|
||||||
|
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Results));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Vote)
|
||||||
|
.SetProperty(x => x.VotesFinal, false));
|
||||||
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
var currentState = await db.AppState.AsNoTracking().FirstAsync();
|
var currentState = await db.AppState.AsNoTracking().FirstAsync();
|
||||||
return Results.Ok(new { currentState.ResultsOpen, currentState.UpdatedAt });
|
return Results.Ok(new { currentState.ResultsOpen, currentState.UpdatedAt });
|
||||||
@@ -31,7 +41,8 @@ public static class AdminEndpoints
|
|||||||
await db.Votes.ExecuteDeleteAsync();
|
await db.Votes.ExecuteDeleteAsync();
|
||||||
await db.Suggestions.ExecuteDeleteAsync();
|
await db.Suggestions.ExecuteDeleteAsync();
|
||||||
|
|
||||||
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Suggest));
|
await db.Players.ExecuteUpdateAsync(p => p.SetProperty(x => x.CurrentPhase, Phase.Suggest)
|
||||||
|
.SetProperty(x => x.VotesFinal, false));
|
||||||
var state = await db.AppState.FirstAsync();
|
var state = await db.AppState.FirstAsync();
|
||||||
state.ResultsOpen = false;
|
state.ResultsOpen = false;
|
||||||
state.UpdatedAt = DateTimeOffset.UtcNow;
|
state.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|||||||
@@ -23,13 +23,26 @@ internal static class EndpointHelpers
|
|||||||
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
var player = await db.Players.FirstOrDefaultAsync(p => p.Id == playerId);
|
||||||
if (player is null) return Phase.Suggest;
|
if (player is null) return Phase.Suggest;
|
||||||
|
|
||||||
|
var state = await db.AppState.FirstAsync();
|
||||||
|
|
||||||
// Auto-upgrade any legacy Reveal phase to Vote to avoid blank screens
|
// Auto-upgrade any legacy Reveal phase to Vote to avoid blank screens
|
||||||
if (player.CurrentPhase == Phase.Reveal)
|
if (player.CurrentPhase == Phase.Reveal)
|
||||||
{
|
{
|
||||||
player.CurrentPhase = Phase.Vote;
|
player.CurrentPhase = Phase.Vote;
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep phases aligned with results availability
|
||||||
|
if (state.ResultsOpen && player.CurrentPhase != Phase.Results)
|
||||||
|
{
|
||||||
|
player.CurrentPhase = Phase.Results;
|
||||||
|
}
|
||||||
|
else if (!state.ResultsOpen && player.CurrentPhase == Phase.Results)
|
||||||
|
{
|
||||||
|
player.CurrentPhase = Phase.Vote;
|
||||||
|
player.VotesFinal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
return player.CurrentPhase;
|
return player.CurrentPhase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public static class StateEndpoints
|
|||||||
var summary = new
|
var summary = new
|
||||||
{
|
{
|
||||||
CurrentPhase = phase,
|
CurrentPhase = phase,
|
||||||
|
player.VotesFinal,
|
||||||
state.ResultsOpen,
|
state.ResultsOpen,
|
||||||
state.UpdatedAt,
|
state.UpdatedAt,
|
||||||
Players = await db.Players.CountAsync(),
|
Players = await db.Players.CountAsync(),
|
||||||
@@ -34,7 +35,7 @@ public static class StateEndpoints
|
|||||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
if (player is null) return Results.Unauthorized();
|
if (player is null) return Results.Unauthorized();
|
||||||
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
||||||
return Results.Ok(new { player.Id, player.DisplayName, player.Username, player.IsAdmin, CurrentPhase = phase });
|
return Results.Ok(new { player.Id, player.DisplayName, player.Username, player.IsAdmin, CurrentPhase = phase, player.VotesFinal });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapPost("/api/me/phase/next", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
app.MapPost("/api/me/phase/next", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
||||||
@@ -52,6 +53,7 @@ public static class StateEndpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.CurrentPhase = next;
|
player.CurrentPhase = next;
|
||||||
|
player.VotesFinal = false; // moving forward clears any prior finalize
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Results.Ok(new { player.CurrentPhase, appState.ResultsOpen });
|
return Results.Ok(new { player.CurrentPhase, appState.ResultsOpen });
|
||||||
});
|
});
|
||||||
@@ -67,6 +69,7 @@ public static class StateEndpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.CurrentPhase = PrevPhase(player.CurrentPhase);
|
player.CurrentPhase = PrevPhase(player.CurrentPhase);
|
||||||
|
player.VotesFinal = false;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
var appState = await db.AppState.AsNoTracking().FirstAsync();
|
var appState = await db.AppState.AsNoTracking().FirstAsync();
|
||||||
return Results.Ok(new { player.CurrentPhase, appState.ResultsOpen });
|
return Results.Ok(new { player.CurrentPhase, appState.ResultsOpen });
|
||||||
|
|||||||
@@ -62,5 +62,18 @@ public static class VoteEndpoints
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Results.Ok(new { vote.Id, vote.Score });
|
return Results.Ok(new { vote.Id, vote.Score });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.MapPost("/api/votes/finalize", async ([FromBody] VoteFinalizeRequest request, HttpContext ctx, AppDbContext db) =>
|
||||||
|
{
|
||||||
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
|
if (player is null) return Results.Unauthorized();
|
||||||
|
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
||||||
|
if (phase != Phase.Vote)
|
||||||
|
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
|
||||||
|
|
||||||
|
player.VotesFinal = request.Final;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Results.Ok(new { player.VotesFinal });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const api = {
|
|||||||
|
|
||||||
myVotes: () => request("/api/votes/mine"),
|
myVotes: () => request("/api/votes/mine"),
|
||||||
vote: (suggestionId, score) => request("/api/votes", { method: "POST", body: { suggestionId, score } }),
|
vote: (suggestionId, score) => request("/api/votes", { method: "POST", body: { suggestionId, score } }),
|
||||||
|
finalizeVotes: (final) => request("/api/votes/finalize", { method: "POST", body: { final } }),
|
||||||
|
|
||||||
results: () => request("/api/results"),
|
results: () => request("/api/results"),
|
||||||
nextPhase: () => request("/api/me/phase/next", { method: "POST" }),
|
nextPhase: () => request("/api/me/phase/next", { method: "POST" }),
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ const translations = {
|
|||||||
"card.openScreenshot": "Open screenshot",
|
"card.openScreenshot": "Open screenshot",
|
||||||
|
|
||||||
"vote.saved": "Saved vote",
|
"vote.saved": "Saved vote",
|
||||||
|
"vote.missing": "Missing",
|
||||||
|
"vote.missingWarn": "You haven’t voted yet. Slide to set a score.",
|
||||||
|
|
||||||
"results.rank": "Rank",
|
"results.rank": "Rank",
|
||||||
"results.game": "Game",
|
"results.game": "Game",
|
||||||
@@ -193,6 +195,8 @@ const translations = {
|
|||||||
"card.openScreenshot": "Screenshot öffnen",
|
"card.openScreenshot": "Screenshot öffnen",
|
||||||
|
|
||||||
"vote.saved": "Stimme gespeichert",
|
"vote.saved": "Stimme gespeichert",
|
||||||
|
"vote.missing": "Fehlt",
|
||||||
|
"vote.missingWarn": "Du hast hier noch nicht abgestimmt. Schiebe den Regler.",
|
||||||
|
|
||||||
"results.rank": "Rang",
|
"results.rank": "Rang",
|
||||||
"results.game": "Spiel",
|
"results.game": "Spiel",
|
||||||
|
|||||||
@@ -145,11 +145,12 @@ export function renderVotes() {
|
|||||||
});
|
});
|
||||||
const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id);
|
const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id);
|
||||||
const current = hasVote ? votesMap[s.id] : 5; // start neutral when no prior vote
|
const current = hasVote ? votesMap[s.id] : 5; // start neutral when no prior vote
|
||||||
const displayScore = hasVote ? current : "—";
|
const displayScore = hasVote ? current : t("vote.missing");
|
||||||
const displayEmoji = hasVote ? scoreToEmoji(current) : neutralEmoji();
|
const displayEmoji = hasVote ? scoreToEmoji(current) : "⚠️";
|
||||||
const footer = document.createElement("div");
|
const footer = document.createElement("div");
|
||||||
footer.className = "vote-controls";
|
footer.className = "vote-controls";
|
||||||
footer.innerHTML = `
|
footer.innerHTML = `
|
||||||
|
<div class="warning-text ${hasVote ? "hidden" : ""}" id="warn-${s.id}">${t("vote.missingWarn")}</div>
|
||||||
<input class="full-slider" type="range" min="0" max="10" value="${current}" data-id="${s.id}">
|
<input class="full-slider" type="range" min="0" max="10" value="${current}" data-id="${s.id}">
|
||||||
<span class="score" id="score-${s.id}">${displayScore}</span>
|
<span class="score" id="score-${s.id}">${displayScore}</span>
|
||||||
<span class="score-emoji" id="emoji-${s.id}">${displayEmoji}</span>`;
|
<span class="score-emoji" id="emoji-${s.id}">${displayEmoji}</span>`;
|
||||||
@@ -162,6 +163,8 @@ export function renderVotes() {
|
|||||||
$("score-" + e.target.dataset.id).textContent = val;
|
$("score-" + e.target.dataset.id).textContent = val;
|
||||||
const emojiEl = $("emoji-" + e.target.dataset.id);
|
const emojiEl = $("emoji-" + e.target.dataset.id);
|
||||||
if (emojiEl) emojiEl.textContent = scoreToEmoji(val);
|
if (emojiEl) emojiEl.textContent = scoreToEmoji(val);
|
||||||
|
const warn = $("warn-" + e.target.dataset.id);
|
||||||
|
if (warn) warn.classList.add("hidden");
|
||||||
});
|
});
|
||||||
input.addEventListener("change", async (e) => {
|
input.addEventListener("change", async (e) => {
|
||||||
const suggestionId = Number(e.target.dataset.id);
|
const suggestionId = Number(e.target.dataset.id);
|
||||||
@@ -627,7 +630,7 @@ export function neutralEmoji() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatVotes(votes) {
|
function formatVotes(votes) {
|
||||||
if (!Array.isArray(votes) || votes.length === 0) return "—";
|
if (!Array.isArray(votes) || votes.length === 0) return "⚠️";
|
||||||
const sorted = [...votes].sort((a, b) => a - b);
|
const sorted = [...votes].sort((a, b) => a - b);
|
||||||
return sorted.map((v) => scoreToEmoji(v)).join("");
|
return sorted.map((v) => scoreToEmoji(v)).join("");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user