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:
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Sis custom styles — layered on top of Neo Brutalism */
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import m from "mithril";
|
||||
|
||||
import { App } from "./components/App";
|
||||
|
||||
const root = document.getElementById("app");
|
||||
if (root) {
|
||||
m.mount(root, App);
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user