Migrate frontend sources to TypeScript
This commit is contained in:
53
RpgRoller/frontend/app.ts
Normal file
53
RpgRoller/frontend/app.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { getHealth, rollDice } from "./generated/api-client.js";
|
||||
|
||||
const healthElementRaw = document.getElementById("health");
|
||||
const resultElementRaw = document.getElementById("result");
|
||||
const formElementRaw = document.getElementById("roll-form");
|
||||
const sidesInputRaw = document.getElementById("sides");
|
||||
|
||||
if (
|
||||
!(healthElementRaw instanceof HTMLElement) ||
|
||||
!(resultElementRaw instanceof HTMLElement) ||
|
||||
!(formElementRaw instanceof HTMLFormElement) ||
|
||||
!(sidesInputRaw instanceof HTMLInputElement)
|
||||
) {
|
||||
throw new Error("Required UI elements are missing from index.html.");
|
||||
}
|
||||
|
||||
const healthElement = healthElementRaw;
|
||||
const resultElement = resultElementRaw;
|
||||
const formElement = formElementRaw;
|
||||
const sidesInput = sidesInputRaw;
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
async function refreshHealth(): Promise<void> {
|
||||
try {
|
||||
const health = await getHealth();
|
||||
healthElement.textContent = `API status: ${health.status}`;
|
||||
}
|
||||
catch (error: unknown) {
|
||||
healthElement.textContent = `API status check failed: ${errorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
formElement.addEventListener("submit", async (event: SubmitEvent) => {
|
||||
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: unknown) {
|
||||
resultElement.textContent = `Roll failed: ${errorMessage(error)}`;
|
||||
}
|
||||
});
|
||||
|
||||
await refreshHealth();
|
||||
57
RpgRoller/frontend/generated/api-client.ts
Normal file
57
RpgRoller/frontend/generated/api-client.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/* This file is generated by scripts/generate-api-client.mjs. */
|
||||
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface RollResponse {
|
||||
sides: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
type ApiOperation = {
|
||||
method: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const apiOperations = {
|
||||
getHealth: { method: "GET", path: "/api/health" },
|
||||
rollDice: { method: "GET", path: "/api/roll/{sides}" }
|
||||
} as const satisfies Record<string, ApiOperation>;
|
||||
|
||||
async function send<TResult>(operation: ApiOperation, pathParams: Record<string, string | number | boolean> = {}): Promise<TResult> {
|
||||
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: unknown = await response.json().catch(() => ({ error: "Unknown API error." }));
|
||||
if (errorPayload && typeof errorPayload === "object" && "error" in errorPayload && typeof errorPayload.error === "string") {
|
||||
throw new Error(errorPayload.error);
|
||||
}
|
||||
|
||||
throw new Error(`Request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<TResult>;
|
||||
}
|
||||
|
||||
export async function getHealth(): Promise<HealthResponse> {
|
||||
return send<HealthResponse>(apiOperations.getHealth, {});
|
||||
}
|
||||
|
||||
export async function rollDice(sides: number): Promise<RollResponse> {
|
||||
return send<RollResponse>(apiOperations.rollDice, { sides: sides });
|
||||
}
|
||||
@@ -1,31 +1,42 @@
|
||||
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");
|
||||
|
||||
const healthElementRaw = document.getElementById("health");
|
||||
const resultElementRaw = document.getElementById("result");
|
||||
const formElementRaw = document.getElementById("roll-form");
|
||||
const sidesInputRaw = document.getElementById("sides");
|
||||
if (!(healthElementRaw instanceof HTMLElement) ||
|
||||
!(resultElementRaw instanceof HTMLElement) ||
|
||||
!(formElementRaw instanceof HTMLFormElement) ||
|
||||
!(sidesInputRaw instanceof HTMLInputElement)) {
|
||||
throw new Error("Required UI elements are missing from index.html.");
|
||||
}
|
||||
const healthElement = healthElementRaw;
|
||||
const resultElement = resultElementRaw;
|
||||
const formElement = formElementRaw;
|
||||
const sidesInput = sidesInputRaw;
|
||||
function errorMessage(error) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
async function refreshHealth() {
|
||||
try {
|
||||
const health = await getHealth();
|
||||
healthElement.textContent = `API status: ${health.status}`;
|
||||
}
|
||||
catch (error) {
|
||||
healthElement.textContent = `API status check failed: ${error.message}`;
|
||||
healthElement.textContent = `API status check failed: ${errorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
resultElement.textContent = `Roll failed: ${errorMessage(error)}`;
|
||||
}
|
||||
});
|
||||
|
||||
await refreshHealth();
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
/* This file is generated by scripts/generate-api-client.mjs. */
|
||||
|
||||
export { apiOperations };
|
||||
|
||||
const apiOperations = {
|
||||
export 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}`);
|
||||
if (errorPayload && typeof errorPayload === "object" && "error" in errorPayload && typeof errorPayload.error === "string") {
|
||||
throw new Error(errorPayload.error);
|
||||
}
|
||||
throw new 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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user