Limit player name lengths and fix vote UI defaults
This commit is contained in:
@@ -19,9 +19,9 @@ public class AppDbContext : DbContext
|
|||||||
modelBuilder.Entity<Player>(builder =>
|
modelBuilder.Entity<Player>(builder =>
|
||||||
{
|
{
|
||||||
builder.HasKey(p => p.Id);
|
builder.HasKey(p => p.Id);
|
||||||
builder.Property(p => p.DisplayName).HasMaxLength(64);
|
builder.Property(p => p.DisplayName).HasMaxLength(16);
|
||||||
builder.Property(p => p.Username).IsRequired().HasMaxLength(64);
|
builder.Property(p => p.Username).IsRequired().HasMaxLength(24);
|
||||||
builder.Property(p => p.NormalizedUsername).IsRequired().HasMaxLength(64);
|
builder.Property(p => p.NormalizedUsername).IsRequired().HasMaxLength(24);
|
||||||
builder.HasIndex(p => p.NormalizedUsername).IsUnique();
|
builder.HasIndex(p => p.NormalizedUsername).IsUnique();
|
||||||
builder.Property(p => p.PasswordHash).IsRequired();
|
builder.Property(p => p.PasswordHash).IsRequired();
|
||||||
builder.Property(p => p.PasswordSalt).IsRequired();
|
builder.Property(p => p.PasswordSalt).IsRequired();
|
||||||
|
|||||||
217
Data/Migrations/20260202183354_LimitPlayerNameLengths.Designer.cs
generated
Normal file
217
Data/Migrations/20260202183354_LimitPlayerNameLengths.Designer.cs
generated
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
// <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("20260202183354_LimitPlayerNameLengths")]
|
||||||
|
partial class LimitPlayerNameLengths
|
||||||
|
{
|
||||||
|
/// <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<int>("CurrentPhase")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("AppState");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
CurrentPhase = 0,
|
||||||
|
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<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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
Data/Migrations/20260202183354_LimitPlayerNameLengths.cs
Normal file
80
Data/Migrations/20260202183354_LimitPlayerNameLengths.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace GameList.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class LimitPlayerNameLengths : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Username",
|
||||||
|
table: "Players",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 24,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 64);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "NormalizedUsername",
|
||||||
|
table: "Players",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 24,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 64);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "DisplayName",
|
||||||
|
table: "Players",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 16,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 64,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Username",
|
||||||
|
table: "Players",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 24);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "NormalizedUsername",
|
||||||
|
table: "Players",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 24);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "DisplayName",
|
||||||
|
table: "Players",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 16,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ namespace GameList.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("AppState", (string)null);
|
b.ToTable("AppState");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -52,7 +52,7 @@ namespace GameList.Data.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("DisplayName")
|
b.Property<string>("DisplayName")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(16)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<bool>("IsAdmin")
|
b.Property<bool>("IsAdmin")
|
||||||
@@ -65,7 +65,7 @@ namespace GameList.Data.Migrations
|
|||||||
|
|
||||||
b.Property<string>("NormalizedUsername")
|
b.Property<string>("NormalizedUsername")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(24)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<byte[]>("PasswordHash")
|
b.Property<byte[]>("PasswordHash")
|
||||||
@@ -78,7 +78,7 @@ namespace GameList.Data.Migrations
|
|||||||
|
|
||||||
b.Property<string>("Username")
|
b.Property<string>("Username")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(24)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
@@ -86,7 +86,7 @@ namespace GameList.Data.Migrations
|
|||||||
b.HasIndex("NormalizedUsername")
|
b.HasIndex("NormalizedUsername")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Players", (string)null);
|
b.ToTable("Players");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
||||||
@@ -136,7 +136,7 @@ namespace GameList.Data.Migrations
|
|||||||
|
|
||||||
b.HasIndex("PlayerId");
|
b.HasIndex("PlayerId");
|
||||||
|
|
||||||
b.ToTable("Suggestions", (string)null);
|
b.ToTable("Suggestions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GameList.Domain.Vote", b =>
|
modelBuilder.Entity("GameList.Domain.Vote", b =>
|
||||||
@@ -164,7 +164,7 @@ namespace GameList.Data.Migrations
|
|||||||
b.HasIndex("PlayerId", "SuggestionId")
|
b.HasIndex("PlayerId", "SuggestionId")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Votes", (string)null);
|
b.ToTable("Votes");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
modelBuilder.Entity("GameList.Domain.Suggestion", b =>
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ public class Player
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
[MaxLength(64)]
|
[MaxLength(16)]
|
||||||
public string? DisplayName { get; set; }
|
public string? DisplayName { get; set; }
|
||||||
|
|
||||||
[MaxLength(64)]
|
[MaxLength(24)]
|
||||||
public string Username { get; set; } = string.Empty;
|
public string Username { get; set; } = string.Empty;
|
||||||
|
|
||||||
[MaxLength(64)]
|
[MaxLength(24)]
|
||||||
public string NormalizedUsername { get; set; } = string.Empty;
|
public string NormalizedUsername { get; set; } = string.Empty;
|
||||||
|
|
||||||
public byte[] PasswordHash { get; set; } = Array.Empty<byte>();
|
public byte[] PasswordHash { get; set; } = Array.Empty<byte>();
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ public static class AuthEndpoints
|
|||||||
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
|
||||||
{
|
{
|
||||||
var username = request.Username?.Trim();
|
var username = request.Username?.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(username) || username.Length > 64)
|
if (string.IsNullOrWhiteSpace(username) || username.Length > 24)
|
||||||
return Results.BadRequest(new { error = "Username is required and must be <= 64 characters." });
|
return Results.BadRequest(new { error = "Username is required and must be <= 24 characters." });
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Password))
|
if (string.IsNullOrWhiteSpace(request.Password))
|
||||||
return Results.BadRequest(new { error = "Password is required." });
|
return Results.BadRequest(new { error = "Password is required." });
|
||||||
|
|
||||||
var displayName = EndpointHelpers.TrimTo(request.DisplayName, 64);
|
var displayName = EndpointHelpers.TrimTo(request.DisplayName, 16);
|
||||||
if (string.IsNullOrWhiteSpace(displayName))
|
if (string.IsNullOrWhiteSpace(displayName))
|
||||||
return Results.BadRequest(new { error = "Display name is required." });
|
return Results.BadRequest(new { error = "Display name is required." });
|
||||||
var normalized = username.ToLowerInvariant();
|
var normalized = username.ToLowerInvariant();
|
||||||
@@ -69,6 +69,8 @@ public static class AuthEndpoints
|
|||||||
var username = request.Username?.Trim();
|
var username = request.Username?.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
|
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password))
|
||||||
return Results.BadRequest(new { error = "Username and password are required." });
|
return Results.BadRequest(new { error = "Username and password are required." });
|
||||||
|
if (username.Length > 24)
|
||||||
|
return Results.BadRequest(new { error = "Username must be <= 24 characters." });
|
||||||
|
|
||||||
var normalized = username.ToLowerInvariant();
|
var normalized = username.ToLowerInvariant();
|
||||||
var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalized);
|
var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalized);
|
||||||
@@ -77,7 +79,7 @@ public static class AuthEndpoints
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(player.DisplayName))
|
if (string.IsNullOrWhiteSpace(player.DisplayName))
|
||||||
{
|
{
|
||||||
player.DisplayName = player.Username;
|
player.DisplayName = EndpointHelpers.TrimTo(player.Username, 16);
|
||||||
}
|
}
|
||||||
player.LastLoginAt = DateTimeOffset.UtcNow;
|
player.LastLoginAt = DateTimeOffset.UtcNow;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|||||||
@@ -32,15 +32,16 @@ public static class StateEndpoints
|
|||||||
|
|
||||||
app.MapPost("/api/me/name", async ([FromBody] SetNameRequest request, HttpContext ctx, AppDbContext db) =>
|
app.MapPost("/api/me/name", async ([FromBody] SetNameRequest request, HttpContext ctx, AppDbContext db) =>
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 64)
|
var name = EndpointHelpers.TrimTo(request.Name, 16);
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return Results.BadRequest(new { error = "Name is required and must be <= 64 characters." });
|
return Results.BadRequest(new { error = "Name is required and must be <= 16 characters." });
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
player.DisplayName = request.Name.Trim();
|
player.DisplayName = name;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Results.Ok(new { player.Id, player.DisplayName });
|
return Results.Ok(new { player.Id, player.DisplayName });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ function renderVotes() {
|
|||||||
const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score]));
|
const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score]));
|
||||||
state.allSuggestions.forEach((s) => {
|
state.allSuggestions.forEach((s) => {
|
||||||
const li = buildCard(s, { showAuthor: true, allowEdit: !!state.me?.isAdmin });
|
const li = buildCard(s, { showAuthor: true, allowEdit: !!state.me?.isAdmin });
|
||||||
const current = votesMap[s.id] ?? 0;
|
const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id);
|
||||||
|
const current = hasVote ? votesMap[s.id] : 5; // start neutral when no prior vote
|
||||||
const footer = document.createElement("div");
|
const footer = document.createElement("div");
|
||||||
footer.className = "vote-controls";
|
footer.className = "vote-controls";
|
||||||
footer.innerHTML = `
|
footer.innerHTML = `
|
||||||
@@ -339,6 +340,7 @@ function setupHandlers() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const username = $("login-username").value.trim();
|
const username = $("login-username").value.trim();
|
||||||
const password = $("login-password").value;
|
const password = $("login-password").value;
|
||||||
|
if (username.length > 24) return toast("Username must be 24 characters or fewer.", true);
|
||||||
if (!username || !password) return toast(t("auth.needCredentials"), true);
|
if (!username || !password) return toast(t("auth.needCredentials"), true);
|
||||||
try {
|
try {
|
||||||
await api.login({ username, password });
|
await api.login({ username, password });
|
||||||
@@ -362,6 +364,9 @@ function setupHandlers() {
|
|||||||
const password = $("register-password").value;
|
const password = $("register-password").value;
|
||||||
const displayName = $("register-displayName").value.trim();
|
const displayName = $("register-displayName").value.trim();
|
||||||
const adminKey = $("register-adminkey").value.trim();
|
const adminKey = $("register-adminkey").value.trim();
|
||||||
|
if (!displayName) return toast(t("toast.displayNameRequired") || "Display name is required.", true);
|
||||||
|
if (username.length > 24) return toast("Username must be 24 characters or fewer.", true);
|
||||||
|
if (displayName.length > 16) return toast("Display name must be 16 characters or fewer.", true);
|
||||||
if (!username || !password) return toast(t("auth.needCredentials"), true);
|
if (!username || !password) return toast(t("auth.needCredentials"), true);
|
||||||
try {
|
try {
|
||||||
await api.register({ username, password, displayName, adminKey });
|
await api.register({ username, password, displayName, adminKey });
|
||||||
@@ -496,7 +501,7 @@ function buildCard(s, { showAuthor = false, allowDelete = false, allowEdit = fal
|
|||||||
${visual}
|
${visual}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title-row">
|
<div class="card-title-row">
|
||||||
<h3>${s.name}</h3>
|
<h3 class="card-title" title="${s.name}">${s.name}</h3>
|
||||||
<div class="title-meta">
|
<div class="title-meta">
|
||||||
${showAuthor && s.author ? `<span class="chip">${s.author}</span>` : ""}
|
${showAuthor && s.author ? `<span class="chip">${s.author}</span>` : ""}
|
||||||
${allowEdit ? `<button class="chip" data-edit="${s.id}" type="button">${t("card.edit")}</button>` : ""}
|
${allowEdit ? `<button class="chip" data-edit="${s.id}" type="button">${t("card.edit")}</button>` : ""}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<form id="login-form" class="stack auth-form" data-mode="login">
|
<form id="login-form" class="stack auth-form" data-mode="login">
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label" data-i18n="auth.username">Username</span>
|
<span class="label" data-i18n="auth.username">Username</span>
|
||||||
<input id="login-username" name="username" maxlength="64" autocomplete="username" required />
|
<input id="login-username" name="username" maxlength="24" autocomplete="username" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label" data-i18n="auth.password">Password</span>
|
<span class="label" data-i18n="auth.password">Password</span>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<form id="register-form" class="stack auth-form hidden" data-mode="register">
|
<form id="register-form" class="stack auth-form hidden" data-mode="register">
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label" data-i18n="auth.username">Username</span>
|
<span class="label" data-i18n="auth.username">Username</span>
|
||||||
<input id="register-username" name="username" maxlength="64" autocomplete="username" required />
|
<input id="register-username" name="username" maxlength="24" autocomplete="username" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label" data-i18n="auth.password">Password</span>
|
<span class="label" data-i18n="auth.password">Password</span>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label" data-i18n="auth.displayName">Display name (shows to group)</span>
|
<span class="label" data-i18n="auth.displayName">Display name (shows to group)</span>
|
||||||
<input id="register-displayName" name="displayName" maxlength="64" required />
|
<input id="register-displayName" name="displayName" maxlength="16" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="label" data-i18n="auth.adminKey">Admin key (optional)</span>
|
<span class="label" data-i18n="auth.adminKey">Admin key (optional)</span>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const translations = {
|
|||||||
"toast.suggestionDeleted": "Suggestion deleted",
|
"toast.suggestionDeleted": "Suggestion deleted",
|
||||||
"toast.savedChanges": "Saved changes",
|
"toast.savedChanges": "Saved changes",
|
||||||
"toast.nameRequired": "Name required",
|
"toast.nameRequired": "Name required",
|
||||||
|
"toast.displayNameRequired": "Display name is required",
|
||||||
"toast.invalidImageUrl": "Screenshot URL must be http(s) and end with an image file.",
|
"toast.invalidImageUrl": "Screenshot URL must be http(s) and end with an image file.",
|
||||||
|
|
||||||
"modal.editTitle": "Edit game",
|
"modal.editTitle": "Edit game",
|
||||||
@@ -196,6 +197,7 @@ const translations = {
|
|||||||
"toast.suggestionDeleted": "Vorschlag gelöscht",
|
"toast.suggestionDeleted": "Vorschlag gelöscht",
|
||||||
"toast.savedChanges": "Änderungen gespeichert",
|
"toast.savedChanges": "Änderungen gespeichert",
|
||||||
"toast.nameRequired": "Name erforderlich",
|
"toast.nameRequired": "Name erforderlich",
|
||||||
|
"toast.displayNameRequired": "Anzeigename ist erforderlich",
|
||||||
"toast.invalidImageUrl": "Screenshot-URL muss mit http(s) beginnen und auf eine Bilddatei enden.",
|
"toast.invalidImageUrl": "Screenshot-URL muss mit http(s) beginnen und auf eine Bilddatei enden.",
|
||||||
|
|
||||||
"modal.editTitle": "Spiel bearbeiten",
|
"modal.editTitle": "Spiel bearbeiten",
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ button.ghost { background: transparent; border-color: #d5c7b5; color: #2c1c0d; }
|
|||||||
|
|
||||||
h3 { margin: 0; font-size: 18px; }
|
h3 { margin: 0; font-size: 18px; }
|
||||||
|
|
||||||
.card-title-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
.card-title-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; }
|
||||||
|
.card-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.title-meta { display: flex; align-items: center; gap: 8px; }
|
.title-meta { display: flex; align-items: center; gap: 8px; }
|
||||||
p { margin: 0; }
|
p { margin: 0; }
|
||||||
.muted { color: #7a6a53; margin: 0; }
|
.muted { color: #7a6a53; margin: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user