diff --git a/main.go b/main.go index db31471..39d084b 100644 --- a/main.go +++ b/main.go @@ -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, diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..ea78f3a --- /dev/null +++ b/public/app.js @@ -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, +}); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..6fe7d9f --- /dev/null +++ b/public/index.html @@ -0,0 +1,31 @@ + + +
+ + +