Scaffold full-stack solution and CI baseline
This commit is contained in:
5
RpgRoller/Contracts/ApiContracts.cs
Normal file
5
RpgRoller/Contracts/ApiContracts.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace RpgRoller.Contracts;
|
||||
|
||||
public sealed record HealthResponse(string Status);
|
||||
public sealed record RollResponse(int Sides, int Value);
|
||||
public sealed record ApiError(string Error);
|
||||
31
RpgRoller/Program.cs
Normal file
31
RpgRoller/Program.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddSingleton<IDiceRoller, RandomDiceRoller>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.MapGet("/api/health", () => TypedResults.Ok(new HealthResponse("ok")));
|
||||
|
||||
app.MapGet(
|
||||
"/api/roll/{sides:int}",
|
||||
Results<Ok<RollResponse>, BadRequest<ApiError>> (int sides, IDiceRoller diceRoller) =>
|
||||
{
|
||||
var validationError = DiceRules.ValidateSides(sides);
|
||||
if (validationError is not null)
|
||||
{
|
||||
return TypedResults.BadRequest(new ApiError(validationError));
|
||||
}
|
||||
|
||||
var value = diceRoller.Roll(sides);
|
||||
return TypedResults.Ok(new RollResponse(sides, value));
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program;
|
||||
23
RpgRoller/Properties/launchSettings.json
Normal file
23
RpgRoller/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5175",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7271;http://localhost:5175",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
RpgRoller/RpgRoller.csproj
Normal file
9
RpgRoller/RpgRoller.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
19
RpgRoller/Services/DiceRules.cs
Normal file
19
RpgRoller/Services/DiceRules.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class DiceRules
|
||||
{
|
||||
public static string? ValidateSides(int sides)
|
||||
{
|
||||
if (sides < 2)
|
||||
{
|
||||
return "Dice must have at least 2 sides.";
|
||||
}
|
||||
|
||||
if (sides > 1000)
|
||||
{
|
||||
return "Dice must have at most 1000 sides.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
6
RpgRoller/Services/IDiceRoller.cs
Normal file
6
RpgRoller/Services/IDiceRoller.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public interface IDiceRoller
|
||||
{
|
||||
int Roll(int sides);
|
||||
}
|
||||
9
RpgRoller/Services/RandomDiceRoller.cs
Normal file
9
RpgRoller/Services/RandomDiceRoller.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public sealed class RandomDiceRoller : IDiceRoller
|
||||
{
|
||||
public int Roll(int sides)
|
||||
{
|
||||
return Random.Shared.Next(1, sides + 1);
|
||||
}
|
||||
}
|
||||
9
RpgRoller/appsettings.json
Normal file
9
RpgRoller/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
31
RpgRoller/wwwroot/app.js
Normal file
31
RpgRoller/wwwroot/app.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { getHealth, rollDice } from "./generated/api-client.js";
|
||||
|
||||
const healthElement = document.getElementById("health");
|
||||
const resultElement = document.getElementById("result");
|
||||
const formElement = document.getElementById("roll-form");
|
||||
const sidesInput = document.getElementById("sides");
|
||||
|
||||
async function refreshHealth() {
|
||||
try {
|
||||
const health = await getHealth();
|
||||
healthElement.textContent = `API status: ${health.status}`;
|
||||
}
|
||||
catch (error) {
|
||||
healthElement.textContent = `API status check failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
formElement.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const sides = Number.parseInt(sidesInput.value, 10);
|
||||
try {
|
||||
const roll = await rollDice(sides);
|
||||
resultElement.textContent = `Rolled d${roll.sides}: ${roll.value}`;
|
||||
}
|
||||
catch (error) {
|
||||
resultElement.textContent = `Roll failed: ${error.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
await refreshHealth();
|
||||
37
RpgRoller/wwwroot/generated/api-client.js
Normal file
37
RpgRoller/wwwroot/generated/api-client.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/* This file is generated by scripts/generate-api-client.mjs. */
|
||||
|
||||
export { apiOperations };
|
||||
|
||||
const apiOperations = {
|
||||
getHealth: { method: "GET", path: "/api/health" },
|
||||
rollDice: { method: "GET", path: "/api/roll/{sides}" }
|
||||
};
|
||||
|
||||
async function send(operation, pathParams = {}) {
|
||||
let resolvedPath = operation.path;
|
||||
for (const [key, value] of Object.entries(pathParams)) {
|
||||
resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(String(value)));
|
||||
}
|
||||
|
||||
const response = await fetch(resolvedPath, {
|
||||
method: operation.method,
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorPayload = await response.json().catch(() => ({ error: "Unknown API error." }));
|
||||
throw new Error(errorPayload.error ?? `Request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getHealth() {
|
||||
return send(apiOperations.getHealth, {});
|
||||
}
|
||||
|
||||
export async function rollDice(sides) {
|
||||
return send(apiOperations.rollDice, { sides: sides });
|
||||
}
|
||||
25
RpgRoller/wwwroot/index.html
Normal file
25
RpgRoller/wwwroot/index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>RpgRoller</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="layout">
|
||||
<h1>RpgRoller</h1>
|
||||
<p id="health" class="status">Checking API status...</p>
|
||||
|
||||
<form id="roll-form" class="panel">
|
||||
<label for="sides">Sides</label>
|
||||
<input id="sides" name="sides" type="number" min="2" max="1000" value="20" required>
|
||||
<button type="submit">Roll</button>
|
||||
</form>
|
||||
|
||||
<p id="result" class="result">No roll yet.</p>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
42
RpgRoller/wwwroot/styles.css
Normal file
42
RpgRoller/wwwroot/styles.css
Normal file
@@ -0,0 +1,42 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(165deg, #f2f4f8 0%, #e6ebf5 100%);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.layout {
|
||||
max-width: 32rem;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 1.25rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font: inherit;
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 0.4rem;
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status,
|
||||
.result {
|
||||
font-weight: 600;
|
||||
}
|
||||
Reference in New Issue
Block a user