diff --git a/.editorconfig b/.editorconfig
index 2a1f655..0229143 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,2 +1,5 @@
[*.cs]
+# We use PostgresSQL which doesn't recommend more specific string types
resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none
+# This is raised for every single property of records returned by endpoints
+resharper_not_accessed_positional_property_local_highlighting = none
diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs
index d43749e..451960e 100644
--- a/Foxnouns.Backend/Controllers/MetaController.cs
+++ b/Foxnouns.Backend/Controllers/MetaController.cs
@@ -14,8 +14,13 @@ public class MetaController(DatabaseContext db) : ApiControllerBase
var userCount = await db.Users.CountAsync();
var memberCount = await db.Members.CountAsync();
- return Ok(new MetaResponse(userCount, memberCount, BuildInfo.Version, BuildInfo.Hash));
+ return Ok(new MetaResponse(
+ BuildInfo.Version, BuildInfo.Hash, memberCount,
+ new UserInfo(userCount, 0, 0, 0))
+ );
}
- private record MetaResponse(int Users, int Members, string Version, string Hash);
+ private record MetaResponse(string Version, string Hash, int Members, UserInfo Users);
+
+ private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay);
}
\ No newline at end of file
diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html
index 77a5ff5..562d998 100644
--- a/Foxnouns.Frontend/src/app.html
+++ b/Foxnouns.Frontend/src/app.html
@@ -4,6 +4,13 @@
+
%sveltekit.head%
diff --git a/Foxnouns.Frontend/src/lib/api/meta.ts b/Foxnouns.Frontend/src/lib/api/meta.ts
new file mode 100644
index 0000000..89f1aa3
--- /dev/null
+++ b/Foxnouns.Frontend/src/lib/api/meta.ts
@@ -0,0 +1,11 @@
+export default interface Meta {
+ version: string;
+ hash: string;
+ users: {
+ total: number;
+ active_month: number;
+ active_week: number;
+ active_day: number;
+ };
+ members: number;
+}
diff --git a/Foxnouns.Frontend/src/lib/api/user.ts b/Foxnouns.Frontend/src/lib/api/user.ts
new file mode 100644
index 0000000..3832872
--- /dev/null
+++ b/Foxnouns.Frontend/src/lib/api/user.ts
@@ -0,0 +1,9 @@
+export type User = {
+ id: string;
+ username: string;
+ display_name: string | null;
+ bio: string | null;
+ member_title: string | null;
+ avatar_url: string | null;
+ links: string[];
+};
diff --git a/Foxnouns.Frontend/src/lib/nav/Logo.svelte b/Foxnouns.Frontend/src/lib/nav/Logo.svelte
new file mode 100644
index 0000000..9da9d99
--- /dev/null
+++ b/Foxnouns.Frontend/src/lib/nav/Logo.svelte
@@ -0,0 +1,34 @@
+
diff --git a/Foxnouns.Frontend/src/lib/nav/Navbar.svelte b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte
new file mode 100644
index 0000000..588bb44
--- /dev/null
+++ b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Foxnouns.Frontend/src/lib/request.ts b/Foxnouns.Frontend/src/lib/request.ts
new file mode 100644
index 0000000..9536ca9
--- /dev/null
+++ b/Foxnouns.Frontend/src/lib/request.ts
@@ -0,0 +1,72 @@
+import { PUBLIC_API_BASE } from "$env/static/public";
+
+export type RequestParams = {
+ token?: string;
+ body?: any;
+ headers?: Record;
+};
+
+/**
+ * Fetch a path from the API and parse the response.
+ * To make sure the request is authenticated in load functions,
+ * pass `fetch` from the request object into opts.
+ *
+ * @param fetchFn A function like `fetch`, such as from the `load` function
+ * @param method The HTTP method, i.e. GET, POST, PATCH
+ * @param path The path to request, minus the leading `/api/v2`
+ * @param params Extra options for this request
+ * @returns T
+ * @throws APIError
+ */
+export default async function request(
+ fetchFn: typeof fetch,
+ method: string,
+ path: string,
+ params: RequestParams = {},
+) {
+ const url = `${PUBLIC_API_BASE}/v2${path}`;
+ const resp = await fetchFn(url, {
+ method,
+ body: params.body ? JSON.stringify(params.body) : undefined,
+ headers: {
+ ...params.headers,
+ ...(params.token ? { Authorization: params.token } : {}),
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (resp.status < 200 || resp.status >= 400) throw await resp.json();
+ return (await resp.json()) as T;
+}
+
+/**
+ * Fetch a path from the API and discard the response.
+ * To make sure the request is authenticated in load functions,
+ * pass `fetch` from the request object into opts.
+ *
+ * @param fetchFn A function like `fetch`, such as from the `load` function
+ * @param method The HTTP method, i.e. GET, POST, PATCH
+ * @param path The path to request, minus the leading `/api/v2`
+ * @param params Extra options for this request
+ * @returns T
+ * @throws APIError
+ */
+export async function fastRequest(
+ fetchFn: typeof fetch,
+ method: string,
+ path: string,
+ params: RequestParams = {},
+): Promise {
+ const url = `${PUBLIC_API_BASE}/v2${path}`;
+ const resp = await fetchFn(url, {
+ method,
+ body: params.body ? JSON.stringify(params.body) : undefined,
+ headers: {
+ ...params.headers,
+ ...(params.token ? { Authorization: params.token } : {}),
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (resp.status < 200 || resp.status >= 400) throw await resp.json();
+}
diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts
new file mode 100644
index 0000000..2d1e4ba
--- /dev/null
+++ b/Foxnouns.Frontend/src/routes/+layout.server.ts
@@ -0,0 +1,13 @@
+import type Meta from "$lib/api/meta";
+import type { User } from "$lib/api/user";
+import request from "$lib/request";
+
+export async function load({ fetch }) {
+ const meta = await request(fetch, "GET", "/meta");
+ let user: User | undefined;
+ try {
+ user = await request(fetch, "GET", "/users/@me");
+ } catch {}
+
+ return { meta, user };
+}
diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte
new file mode 100644
index 0000000..64a204f
--- /dev/null
+++ b/Foxnouns.Frontend/src/routes/+layout.svelte
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte
index 5982b0a..0f5264b 100644
--- a/Foxnouns.Frontend/src/routes/+page.svelte
+++ b/Foxnouns.Frontend/src/routes/+page.svelte
@@ -1,2 +1,19 @@
+
+