feat: add Mithril frontend for flashcard visualization (nes.css)
This commit is contained in:
1 parent
a40542f7b4
commit
9e21732d00
3 files changed
+115
No files matched your search
@@ -2,8 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -19,6 +21,9 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed public/*
|
||||
var publicFiles embed.FS
|
||||
|
||||
var version = "0.1.0"
|
||||
|
||||
func main() {
|
||||
@@ -72,6 +77,8 @@ func main() {
|
||||
})
|
||||
mux.Handle("/mcp", authMiddleware(token, streamableServer))
|
||||
registerAPI(mux, sqliteStore, logger)
|
||||
publicFS, _ := fs.Sub(publicFiles, "public")
|
||||
mux.Handle("/", http.FileServer(http.FS(publicFS)))
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: ":" + port,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import m from 'https://unpkg.com/mithril@2/mithril.esm.js';
|
||||
|
||||
const API = '/api/v1/flashcards';
|
||||
|
||||
const List = {
|
||||
oninit: async function (v) {
|
||||
v.state.ids = [];
|
||||
v.state.loading = true;
|
||||
try {
|
||||
const res = await fetch(API);
|
||||
const data = await res.json();
|
||||
v.state.ids = (data.flashcards || []).map(u => u.split('/').pop());
|
||||
} catch { /* ignore */ }
|
||||
v.state.loading = false;
|
||||
},
|
||||
view: function (v) {
|
||||
if (v.state.loading) return m('.loading', m('i.nes-icon.nes-logo.is-small'));
|
||||
return [
|
||||
m('.header', [
|
||||
m('h1', 'St\u2011Pierre Flashcards'),
|
||||
m('span.nes-badge', m('span', String(v.state.ids.length))),
|
||||
]),
|
||||
v.state.ids.length === 0 && m('p', 'No flashcards yet.'),
|
||||
v.state.ids.map(id => m('.card-preview.nes-container.is-rounded.with-title', {
|
||||
onclick: () => m.route.set('/flashcard/' + id),
|
||||
}, [
|
||||
m('p.title', '#' + id),
|
||||
m('p', 'Click to view details \u2192'),
|
||||
])),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
const Detail = {
|
||||
oninit: async function (v) {
|
||||
v.state.entry = null;
|
||||
v.state.loading = true;
|
||||
try {
|
||||
const res = await fetch(API + '/' + v.attrs.id);
|
||||
if (res.ok) v.state.entry = await res.json();
|
||||
} catch { /* ignore */ }
|
||||
v.state.loading = false;
|
||||
},
|
||||
view: function (v) {
|
||||
if (v.state.loading) return m('.loading', m('i.nes-icon.nes-logo.is-small'));
|
||||
const e = v.state.entry;
|
||||
if (!e) return [
|
||||
m('.back', m('button.nes-btn', { onclick: () => m.route.set('/') }, '\u2B05 Back')),
|
||||
m('p', 'Flashcard not found.'),
|
||||
];
|
||||
return [
|
||||
m('.back', m('button.nes-btn.is-warning', { onclick: () => m.route.set('/') }, '\u2B05 Back')),
|
||||
m('.detail-card.nes-container.is-rounded.with-title', [
|
||||
m('p.title', '#' + e.public_id + ' v' + e.version),
|
||||
m('.field', [ m('.field-label', 'Note'), m('p', e.note) ]),
|
||||
e.host ? m('.field', [ m('.field-label', 'Host'), m('p', e.host) ]) : null,
|
||||
e.user ? m('.field', [ m('.field-label', 'User'), m('p', e.user) ]) : null,
|
||||
e.context ? m('.field', [ m('.field-label', 'Context'), m('p', e.context) ]) : null,
|
||||
e.repos?.length ? m('.field', [
|
||||
m('.field-label', 'Repos'),
|
||||
m('div', e.repos.map(r => m('span.nes-badge', m('span', r)))),
|
||||
]) : null,
|
||||
e.tags?.length ? m('.field', [
|
||||
m('.field-label', 'Tags'),
|
||||
m('div', e.tags.map(t => m('span.nes-badge', m('span', t)))),
|
||||
]) : null,
|
||||
m('.field', [ m('.field-label', 'Created'), m('p', e.created) ]),
|
||||
e.delete_timestamp ? m('.field', [ m('.field-label', 'Deleted'), m('p', e.delete_timestamp) ]) : null,
|
||||
]),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
m.route(document.getElementById('app'), '/', {
|
||||
'/': List,
|
||||
'/flashcard/:id': Detail,
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>St‑Pierre Flashcards</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css">
|
||||
<style>
|
||||
html, body { background: #212529; min-height: 100vh; }
|
||||
body { padding: 1rem; }
|
||||
.flashcard-body { max-width: 800px; margin: 0 auto; }
|
||||
.card-preview { cursor: pointer; margin-bottom: .75rem; }
|
||||
.card-preview:hover { filter: brightness(1.2); }
|
||||
.card-preview .note { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
|
||||
.tags-row { margin-top: .5rem; }
|
||||
.meta-row { display: flex; flex-wrap: wrap; gap: .25rem .75rem; font-size: .85rem; margin-top: .25rem; }
|
||||
.back { margin-bottom: 1.5rem; }
|
||||
.detail-card .nes-badge { margin-right: .25rem; }
|
||||
.detail-card .field { margin-bottom: 1rem; }
|
||||
.detail-card .field-label { font-size: .75rem; opacity: .7; margin-bottom: .25rem; }
|
||||
.loading { text-align: center; padding: 4rem 0; }
|
||||
.header { margin-bottom: 2rem; display: flex; align-items: center; gap: .75rem; }
|
||||
.nes-badge.is-icon span { position: static; }
|
||||
.vote-summary { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #fff3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flashcard-body" id="app"></div>
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user