This commit is contained in:
didier committed 2026-10-01 13:59:36 -04:00
1 parent d72ef520cd
commit b96e1e19df
3 files changed
+90 -1

No files matched your search

+18
View File
@@ -131,6 +131,24 @@ func main() {
} }
writeJSON(w, http.StatusOK, map[string][]flashcardSummary{"flashcards": summaries}) writeJSON(w, http.StatusOK, map[string][]flashcardSummary{"flashcards": summaries})
}))) })))
mux.Handle("/api/v1/flashcards/search", withAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
return
}
query := r.URL.Query().Get("q")
if query == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing query parameter 'q'"})
return
}
results, err := searchEngine.Search(r.Context(), query, 50)
if err != nil {
logger.Error("search flashcards", zap.Error(err))
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"results": results, "query": query})
})))
mux.Handle("/api/v1/flashcards/", withAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mux.Handle("/api/v1/flashcards/", withAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"}) writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
+71
View File
@@ -1,6 +1,7 @@
import m from "https://esm.sh/mithril@2.3.8"; import m from "https://esm.sh/mithril@2.3.8";
const API = "/api/v1/flashcards"; const API = "/api/v1/flashcards";
const SEARCH_API = "/api/v1/flashcards/search";
const AUTH = "/api/v1/auth"; const AUTH = "/api/v1/auth";
const USERS = "/api/v1/users"; const USERS = "/api/v1/users";
const SESSIONS = "/api/v1/sessions"; const SESSIONS = "/api/v1/sessions";
@@ -221,6 +222,8 @@ const List = {
oninit: async function (v) { oninit: async function (v) {
v.state.cards = []; v.state.cards = [];
v.state.loading = true; v.state.loading = true;
v.state.query = "";
v.state.searching = false;
const res = await fetch(API); const res = await fetch(API);
const data = await res.json(); const data = await res.json();
v.state.cards = data.flashcards || []; v.state.cards = data.flashcards || [];
@@ -235,10 +238,78 @@ const List = {
m("p.cyber-text-muted.cyber-mt-md", "loading\u2026"), m("p.cyber-text-muted.cyber-mt-md", "loading\u2026"),
]); ]);
} }
const doSearch = async () => {
const q = v.state.query.trim();
if (!q) {
v.state.searching = true;
const res = await fetch(API);
const data = await res.json();
v.state.cards = data.flashcards || [];
v.state.searching = false;
v.state.page = 1;
m.redraw();
return;
}
v.state.searching = true;
m.redraw();
const res = await fetch(SEARCH_API + "?q=" + encodeURIComponent(q));
const data = await res.json();
v.state.cards = (data.results || []).map((r) => ({
id: r.id,
version: r.version,
note: r.note,
tags: r.tags,
host: r.host,
user: r.user,
score: r.score,
}));
v.state.searching = false;
v.state.page = 1;
m.redraw();
};
const totalPages = Math.max(1, Math.ceil(v.state.cards.length / PAGE_SIZE)); const totalPages = Math.max(1, Math.ceil(v.state.cards.length / PAGE_SIZE));
const page = Math.min(v.state.page, totalPages); const page = Math.min(v.state.page, totalPages);
const slice = v.state.cards.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); const slice = v.state.cards.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
return m(".cyber-p-lg", { style: "max-width:720px;margin:0 auto" }, [ return m(".cyber-p-lg", { style: "max-width:720px;margin:0 auto" }, [
m(
"form.cyber-flex.cyber-gap-sm.cyber-mb-md",
{
onsubmit: (e) => {
e.preventDefault();
doSearch();
},
},
[
m("input.cyber-input", {
style: "flex:1",
placeholder: "Search flashcards\u2026",
value: v.state.query,
oninput: (e) => {
v.state.query = e.target.value;
},
}),
m(
"button.cyber-btn.cyber-btn--primary",
{ type: "submit", disabled: v.state.searching },
v.state.searching ? "..." : "Search",
),
v.state.query
? m(
"button.cyber-btn.cyber-btn--ghost",
{
type: "button",
onclick: () => {
v.state.query = "";
doSearch();
},
},
"Clear",
)
: null,
],
),
m(Pager, { m(Pager, {
page, page,
total: totalPages, total: totalPages,
+1 -1
View File
@@ -216,7 +216,7 @@ func (s *sqliteStore) listAll(ctx context.Context) ([]flashcardSummary, error) {
WHERE f.version = (SELECT MAX(f2.version) FROM flashcard f2 WHERE f2.public_id = f.public_id) WHERE f.version = (SELECT MAX(f2.version) FROM flashcard f2 WHERE f2.public_id = f.public_id)
AND f.delete_timestamp IS NULL AND f.delete_timestamp IS NULL
AND f.public_id != 0 AND f.public_id != 0
ORDER BY f.public_id`, ORDER BY f.created DESC`,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("list: %w", err) return nil, fmt.Errorf("list: %w", err)