Upgrade to .NET 10 and finalize foundation scaffold
This commit is contained in:
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Build outputs
|
||||
bin/
|
||||
obj/
|
||||
out/
|
||||
publish/
|
||||
|
||||
# IDE
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# User secrets / configs
|
||||
appsettings.Development.json
|
||||
*.user
|
||||
*.suo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# SQLite data
|
||||
App_Data/
|
||||
*.db
|
||||
|
||||
# OS cruft
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
@@ -7,7 +7,7 @@ This repo is a tiny, purpose-built web app for a closed Discord group to:
|
||||
4) reveal totals sorted by score
|
||||
|
||||
Tech constraints:
|
||||
- .NET 8
|
||||
- .NET 10
|
||||
- ASP.NET Core Minimal API
|
||||
- Static HTML/CSS/JS (no Razor Pages, no Blazor, no HTMX)
|
||||
- SQLite via EF Core
|
||||
@@ -76,3 +76,4 @@ Do not introduce MVC controllers, Razor Pages, Blazor, or SPA frameworks.
|
||||
- Implement API first, UI second
|
||||
- Keep changes small and testable
|
||||
- Prefer clarity over abstraction
|
||||
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
|
||||
61
Data/AppDbContext.cs
Normal file
61
Data/AppDbContext.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using GameList.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GameList.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Player> Players => Set<Player>();
|
||||
public DbSet<Suggestion> Suggestions => Set<Suggestion>();
|
||||
public DbSet<Vote> Votes => Set<Vote>();
|
||||
public DbSet<AppState> AppState => Set<AppState>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Player>(builder =>
|
||||
{
|
||||
builder.HasKey(p => p.Id);
|
||||
builder.Property(p => p.DisplayName).HasMaxLength(64);
|
||||
builder.HasMany(p => p.Suggestions)
|
||||
.WithOne(s => s.Player!)
|
||||
.HasForeignKey(s => s.PlayerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasMany(p => p.Votes)
|
||||
.WithOne(v => v.Player!)
|
||||
.HasForeignKey(v => v.PlayerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Suggestion>(builder =>
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.Name).IsRequired().HasMaxLength(100);
|
||||
builder.Property(s => s.Genre).HasMaxLength(50);
|
||||
builder.Property(s => s.Description).HasMaxLength(500);
|
||||
builder.Property(s => s.ScreenshotUrl).HasMaxLength(2048);
|
||||
builder.Property(s => s.YoutubeUrl).HasMaxLength(2048);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Vote>(builder =>
|
||||
{
|
||||
builder.HasKey(v => v.Id);
|
||||
builder.Property(v => v.Score).IsRequired();
|
||||
builder.HasIndex(v => new { v.PlayerId, v.SuggestionId }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AppState>(builder =>
|
||||
{
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.HasData(new AppState
|
||||
{
|
||||
Id = 1,
|
||||
CurrentPhase = Phase.Suggest,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
8
Domain/AppState.cs
Normal file
8
Domain/AppState.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace GameList.Domain;
|
||||
|
||||
public class AppState
|
||||
{
|
||||
public int Id { get; set; } = 1;
|
||||
public Phase CurrentPhase { get; set; } = Phase.Suggest;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
9
Domain/Phase.cs
Normal file
9
Domain/Phase.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace GameList.Domain;
|
||||
|
||||
public enum Phase
|
||||
{
|
||||
Suggest = 0,
|
||||
Reveal = 1,
|
||||
Vote = 2,
|
||||
Results = 3
|
||||
}
|
||||
16
Domain/Player.cs
Normal file
16
Domain/Player.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GameList.Domain;
|
||||
|
||||
public class Player
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[MaxLength(64)]
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public ICollection<Suggestion> Suggestions { get; set; } = new List<Suggestion>();
|
||||
public ICollection<Vote> Votes { get; set; } = new List<Vote>();
|
||||
}
|
||||
32
Domain/Suggestion.cs
Normal file
32
Domain/Suggestion.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GameList.Domain;
|
||||
|
||||
public class Suggestion
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid PlayerId { get; set; }
|
||||
public Player? Player { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? Genre { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string? ScreenshotUrl { get; set; }
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string? YoutubeUrl { get; set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public ICollection<Vote> Votes { get; set; } = new List<Vote>();
|
||||
}
|
||||
21
Domain/Vote.cs
Normal file
21
Domain/Vote.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace GameList.Domain;
|
||||
|
||||
public class Vote
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid PlayerId { get; set; }
|
||||
public Player? Player { get; set; }
|
||||
|
||||
[Required]
|
||||
public int SuggestionId { get; set; }
|
||||
public Suggestion? Suggestion { get; set; }
|
||||
|
||||
[Range(0, 10)]
|
||||
public int Score { get; set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
17
GameList.csproj
Normal file
17
GameList.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
45
Program.cs
Normal file
45
Program.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using GameList.Data;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var dataDirectory = Path.Combine(builder.Environment.ContentRootPath, "App_Data");
|
||||
Directory.CreateDirectory(dataDirectory);
|
||||
|
||||
var configuredConnection = builder.Configuration.GetConnectionString("Default");
|
||||
var dbPath = Path.Combine(dataDirectory, "gamelist.db");
|
||||
var connectionBuilder = new SqliteConnectionStringBuilder();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configuredConnection))
|
||||
{
|
||||
connectionBuilder.DataSource = dbPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionBuilder = new SqliteConnectionStringBuilder(configuredConnection);
|
||||
|
||||
if (connectionBuilder.DataSource.Contains("App_Data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fileName = Path.GetFileName(connectionBuilder.DataSource);
|
||||
connectionBuilder.DataSource = Path.Combine(dataDirectory, fileName);
|
||||
}
|
||||
else if (!Path.IsPathRooted(connectionBuilder.DataSource))
|
||||
{
|
||||
connectionBuilder.DataSource = Path.GetFullPath(connectionBuilder.DataSource, dataDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionString = connectionBuilder.ToString();
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseSqlite(connectionString));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||
|
||||
app.Run();
|
||||
38
Properties/launchSettings.json
Normal file
38
Properties/launchSettings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:42257",
|
||||
"sslPort": 44362
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5116",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7103;http://localhost:5116",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
TASKS.md
10
TASKS.md
@@ -3,11 +3,11 @@
|
||||
- [x] Initialize git repository.
|
||||
|
||||
## Foundation
|
||||
- [ ] Add `.gitignore` for .NET / IIS publish artifacts.
|
||||
- [ ] Scaffold .NET 8 minimal API project with static file hosting (`wwwroot`).
|
||||
- [ ] Configure SQLite connection pointing to `App_Data` with EF Core migrations folder under `Data/`.
|
||||
- [ ] Define domain models in `Domain/`: `Player`, `Suggestion`, `Vote`, `AppState`, `Phase` enum.
|
||||
- [ ] Implement `AppDbContext` in `Data/` with DbSets and simple seeding of `AppState`.
|
||||
- [x] Add `.gitignore` for .NET / IIS publish artifacts.
|
||||
- [x] Scaffold .NET 10 minimal API project with static file hosting (`wwwroot`).
|
||||
- [x] Configure SQLite connection pointing to `App_Data` with EF Core migrations folder under `Data/`.
|
||||
- [x] Define domain models in `Domain/`: `Player`, `Suggestion`, `Vote`, `AppState`, `Phase` enum.
|
||||
- [x] Implement `AppDbContext` in `Data/` with DbSets and simple seeding of `AppState`.
|
||||
|
||||
## Identity & Middleware
|
||||
- [ ] Middleware to issue/read HttpOnly `player` cookie with Guid; SameSite=Strict; secure in production.
|
||||
|
||||
12
appsettings.json
Normal file
12
appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Default": "Data Source=App_Data/gamelist.db"
|
||||
}
|
||||
}
|
||||
1
wwwroot/app.js
Normal file
1
wwwroot/app.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log("CoopGameChooser client placeholder");
|
||||
16
wwwroot/index.html
Normal file
16
wwwroot/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CoopGameChooser</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<h1>CoopGameChooser</h1>
|
||||
<p>MVP is on the way. API and UI will land here.</p>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
20
wwwroot/styles.css
Normal file
20
wwwroot/styles.css
Normal file
@@ -0,0 +1,20 @@
|
||||
:root {
|
||||
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
margin: 10vh auto;
|
||||
padding: 24px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #1e293b;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
Reference in New Issue
Block a user