diff --git a/RpgRoller/Components/App.razor b/RpgRoller/Components/App.razor
new file mode 100644
index 0000000..781007e
--- /dev/null
+++ b/RpgRoller/Components/App.razor
@@ -0,0 +1,18 @@
+@using Microsoft.AspNetCore.Components.Web
+
+
+
+
+
+
+
+ RpgRoller
+
+
+
+
+
+
+
+
+
diff --git a/RpgRoller/Components/Layout/MainLayout.razor b/RpgRoller/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..a098840
--- /dev/null
+++ b/RpgRoller/Components/Layout/MainLayout.razor
@@ -0,0 +1,4 @@
+@inherits LayoutComponentBase
+@attribute [ExcludeFromCodeCoverage]
+
+@Body
diff --git a/RpgRoller/Components/Pages/Home.razor b/RpgRoller/Components/Pages/Home.razor
new file mode 100644
index 0000000..c892341
--- /dev/null
+++ b/RpgRoller/Components/Pages/Home.razor
@@ -0,0 +1,7 @@
+@page "/"
+@attribute [ExcludeFromCodeCoverage]
+
+
+ RpgRoller
+ Frontend migration in progress: Blazor shell is active.
+
diff --git a/RpgRoller/Components/Routes.razor b/RpgRoller/Components/Routes.razor
new file mode 100644
index 0000000..dd87796
--- /dev/null
+++ b/RpgRoller/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/RpgRoller/Components/_Imports.razor b/RpgRoller/Components/_Imports.razor
new file mode 100644
index 0000000..e928c57
--- /dev/null
+++ b/RpgRoller/Components/_Imports.razor
@@ -0,0 +1,9 @@
+@using System.Diagnostics.CodeAnalysis
+@using RpgRoller
+@using RpgRoller.Contracts
+@using Microsoft.AspNetCore.Components
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.JSInterop
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
diff --git a/RpgRoller/Program.cs b/RpgRoller/Program.cs
index bc2ee7c..8ed41b9 100644
--- a/RpgRoller/Program.cs
+++ b/RpgRoller/Program.cs
@@ -3,14 +3,17 @@ using RpgRoller.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
var app = builder.Build();
app.InitializeRpgRollerState();
-app.UseDefaultFiles();
app.UseStaticFiles();
app.MapRpgRollerApi();
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
app.Run();
public partial class Program;
diff --git a/RpgRoller/wwwroot/js/rpgroller-api.js b/RpgRoller/wwwroot/js/rpgroller-api.js
new file mode 100644
index 0000000..a30b436
--- /dev/null
+++ b/RpgRoller/wwwroot/js/rpgroller-api.js
@@ -0,0 +1,57 @@
+window.rpgRollerApi = (() => {
+ async function request(method, url, body) {
+ const options = {
+ method,
+ credentials: "same-origin",
+ headers: {
+ Accept: "application/json"
+ }
+ };
+
+ if (body !== null && body !== undefined) {
+ options.headers["Content-Type"] = "application/json";
+ options.body = JSON.stringify(body);
+ }
+
+ let response;
+ try {
+ response = await fetch(url, options);
+ }
+ catch (error) {
+ return {
+ ok: false,
+ status: 0,
+ error: "Network error. Check your connection and retry."
+ };
+ }
+
+ let parsed = null;
+ const text = await response.text();
+ if (text) {
+ try {
+ parsed = JSON.parse(text);
+ }
+ catch {
+ parsed = null;
+ }
+ }
+
+ if (!response.ok) {
+ return {
+ ok: false,
+ status: response.status,
+ error: parsed && typeof parsed.error === "string" ? parsed.error : "Request failed."
+ };
+ }
+
+ return {
+ ok: true,
+ status: response.status,
+ data: parsed
+ };
+ }
+
+ return {
+ request
+ };
+})();