Initial project skeleton

Haskell backend: Orb HTTP framework, JSON-only API
Frontend: Mithril.js SPA with TypeScript, Neo Brutalism CSS
Dockerized build via flipstone/haskell-tools image

Routes:
  GET /api/health — health check

Build: ./hs stack build
Test:  ./hs stack test
Run:   ./scripts/run
This commit is contained in:
2026-07-15 14:51:27 -04:00
commit 72d94170b4
24 changed files with 964 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sis — Chore Tracker</title>
<link rel="stylesheet" href="https://unpkg.com/neobrutalismcss@latest">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="app"></div>
<script src="/index.js"></script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
{
"name": "sis-frontend",
"version": "0.1.0",
"private": true,
"description": "Sis chore tracker SPA frontend",
"scripts": {
"build": "tsc && cp -r public/* dist/",
"dev": "tsc --watch",
"serve": "npx serve dist"
},
"dependencies": {
"mithril": "^2.2.13"
},
"devDependencies": {
"@types/mithril": "^2.2.7",
"typescript": "^5.7.0",
"serve": "^14.2.0"
}
}
+9
View File
@@ -0,0 +1,9 @@
/* Sis custom styles — layered on top of Neo Brutalism */
body {
min-height: 100vh;
}
#app {
padding: 1rem;
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Thin API client for the Sis backend.
*/
const BASE = "/api";
async function request<T>(path: string): Promise<T> {
const resp = await fetch(BASE + path);
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
}
return resp.json() as Promise<T>;
}
export interface HealthResponse {
message: string;
}
export function checkHealth(): Promise<string> {
return request<HealthResponse>("/health").then((r) => r.message);
}
+35
View File
@@ -0,0 +1,35 @@
import m, { Vnode } from "mithril";
import { checkHealth } from "../api";
interface AppAttrs {}
interface AppState {
health: string;
}
export const App: m.Component<AppAttrs, AppState> = {
oninit(vnode: Vnode<AppAttrs, AppState>) {
vnode.state.health = "loading...";
checkHealth()
.then((msg) => {
vnode.state.health = msg;
m.redraw();
})
.catch((err) => {
vnode.state.health = "error: " + String(err);
m.redraw();
});
},
view(vnode: Vnode<AppAttrs, AppState>) {
return m("main.container", { style: { maxWidth: "720px", margin: "2rem auto" } }, [
m("h1", "Sis"),
m("p", "Shared household chore tracker."),
m("p", [
m("strong", "API status: "),
m("span", vnode.state.health),
]),
]);
},
};
+8
View File
@@ -0,0 +1,8 @@
import m from "mithril";
import { App } from "./components/App";
const root = document.getElementById("app");
if (root) {
m.mount(root, App);
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src",
"sourceMap": true,
"jsx": "react",
"jsxFactory": "m",
"jsxFragmentFactory": "m.Fragment"
},
"include": ["src/**/*.ts"]
}