From 1a11668db4fb1c9c7e86313326f12c73856f1021 Mon Sep 17 00:00:00 2001 From: samantha Date: Sun, 27 Sep 2026 13:29:22 +0200 Subject: [PATCH] bookmark search --- .gitignore | 2 +- data.db | Bin 0 -> 12288 bytes database/main.go | 84 +++++++++++++++ go.mod | 13 +++ go.sum | 20 ++++ main.go | 126 +++++++++++++++++++++++ source/templates/pages/search.html | 158 +++++++++++++++++++++++++++++ 7 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 data.db create mode 100644 database/main.go create mode 100644 source/templates/pages/search.html diff --git a/.gitignore b/.gitignore index c2e483c..ac3268a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .env tailwindcss-* -site \ No newline at end of file +site diff --git a/data.db b/data.db new file mode 100644 index 0000000000000000000000000000000000000000..5cfe69a2077f6752b3c20e97196954e49f4e960a GIT binary patch literal 12288 zcmeI&zityT7yxiz{uCur8WxnHLoBhhYMm2843ve?o**Jk({PoD7?5*z5-UEtwS5g4 z%RBHGya`XhTd*+WyEc(p1~w$n_bEhM$a5Cs8xi7`T` z>POj+bp(D=Sugx&zW!5GQRmCIhR-T(|3u9<{tW^IKmY_l00ck)1V8`;KmY_l00jP; zz_7H{+TRbxj!q=Em(m}BKU#j7ah2R(mXAhJH;!=JebtZfz1&?zQ-0tp_pYA>4_fVZ z@cDVsbE5SkrDkC_lhU0&n{B*Xuhj&@H_;o!(d%f0hojy}cQnRtqcQHD$EUr4uRe(e z@qy2F!bzUz$I<&34^I6!@Ava7nUObre!G%T$7kBC?$*?6d->VCvi%)3zcz=>x}QLR z00@8p2!H?xfB*=900@8p2!OypCD5r<(c>38jx)BxQ^W0i?%ahv48vlp*y$uX4X4!3 zg;vMqDtZ!yRLU$BN@OX%FnTH^UzaVH%MO*=$X9MmCqmC9`{VhmeP1wYq1zYIR zbz^6y-R5C<)>-%r>!>OE`nzLU;%unIZ0;-(YRZk$Bo#~$yAZ~aG?Px;K6QTn51K!a ne?fo%2!H?xfB*=900@8p2!H?xfB*>mF9MCA+zvK>KdAfyRcXwK literal 0 HcmV?d00001 diff --git a/database/main.go b/database/main.go new file mode 100644 index 0000000..c72bcab --- /dev/null +++ b/database/main.go @@ -0,0 +1,84 @@ +package database + +import ( + "database/sql" + "fmt" + + _ "modernc.org/sqlite" +) + +var db *sql.DB + +// InitDB opens the sqlite file at path and ensures the schema exists. +// Call this once at startup before AddBookmark/RetrieveBookmarks are used. +func InitDB(path string) error { + var err error + db, err = sql.Open("sqlite", path) + if err != nil { + return fmt.Errorf("open db: %w", err) + } + + if err := db.Ping(); err != nil { + return fmt.Errorf("ping db: %w", err) + } + + schema := ` + CREATE TABLE IF NOT EXISTS bookmarks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + url TEXT NOT NULL, + category TEXT NOT NULL + );` + + if _, err := db.Exec(schema); err != nil { + return fmt.Errorf("create schema: %w", err) + } + + return nil +} + +type Bookmark struct { + ID uint64 + Title string + URL string + Category string // kebab-case, e.g. "market-data-and-filling-dbs" +} + +func AddBookmark(b Bookmark) (id uint64, err error) { + res, err := db.Exec( + `INSERT INTO bookmarks (title, url, category) VALUES (?, ?, ?)`, + b.Title, b.URL, b.Category, + ) + if err != nil { + return 0, fmt.Errorf("insert bookmark: %w", err) + } + + lastID, err := res.LastInsertId() + if err != nil { + return 0, fmt.Errorf("get inserted id: %w", err) + } + + return uint64(lastID), nil +} + +func RetrieveBookmarks() (marks []Bookmark, err error) { + rows, err := db.Query(`SELECT id, title, url, category FROM bookmarks ORDER BY id DESC`) + if err != nil { + return nil, fmt.Errorf("query bookmarks: %w", err) + } + defer rows.Close() + + for rows.Next() { + var b Bookmark + if err := rows.Scan(&b.ID, &b.Title, &b.URL, &b.Category); err != nil { + return nil, fmt.Errorf("scan bookmark: %w", err) + } + marks = append(marks, b) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return marks, nil +} diff --git a/go.mod b/go.mod index 50c690c..5f30e85 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,20 @@ require ( github.com/pquerna/otp v1.5.0 ) +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.47.0 // indirect + modernc.org/libc v1.75.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.12.1 // indirect +) + require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect golang.org/x/crypto v0.52.0 + modernc.org/sqlite v1.59.0 ) diff --git a/go.sum b/go.sum index 6717ca8..630628f 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,32 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew= +modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= +modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A= +modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k= diff --git a/main.go b/main.go index 0c55caf..6d9ab4a 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,8 @@ import ( "io/fs" "log" "net/http" + "site/database" + "strings" ) //go:embed source @@ -22,12 +24,18 @@ func main() { log.Fatal(err) } + database.InitDB("data.db") + mux := http.NewServeMux() mux.HandleFunc("GET /{$}", servePage("home", "templates/pages/index.html", sourceRoot, false)) mux.HandleFunc("GET /research", servePage("research", "templates/pages/research.html", sourceRoot, true)) mux.HandleFunc("GET /bookkeeping", servePage("bookkeeping", "templates/pages/bookkeeping.html", sourceRoot, true)) mux.HandleFunc("GET /personal", servePage("personal", "templates/pages/interests.html", sourceRoot, true)) + mux.HandleFunc("GET /search", search(sourceRoot)) + mux.HandleFunc("GET /api/bookmarks/search", bookmarksearch()) + mux.HandleFunc("POST /api/bookmarks", bookmarkAdd()) + staticRoot, err := fs.Sub(sourceFS, "source/static") if err != nil { log.Fatal(err) @@ -70,3 +78,121 @@ func servePage(name string, templatefile string, sourcefile fs.FS, sub bool) htt } } } + +func search(sourcefile fs.FS) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data, err := fs.ReadFile(sourcefile, "templates/pages/search.html") + if err != nil { + log.Println("read error:", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(data) + } +} + +var resultsTmpl = template.Must(template.New("results").Parse(` +{{if .}} + {{range .}} + +

{{.Title}}

+

{{.CategoryLabel}}

+
+ {{end}} +{{else}} +

No bookmarks match yet.

+{{end}} +`)) + +var addTmpl = template.Must(template.New("results").Parse(` + +

{{.Title}}

+

{{.CategoryLabel}}

+
+`)) + +// CategoryLabel gives the template a human-readable label without +// needing a helper func registered on the template itself. +type bookmarkView struct { + database.Bookmark + CategoryLabel string +} + +func bookmarksearch() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q"))) + category := r.URL.Query().Get("category") + + var matches []bookmarkView + + bookmarks, err := database.RetrieveBookmarks() + if err != nil { + + } + + for _, b := range bookmarks { + if category != "" && category != "all" && b.Category != category { + continue + } + if q != "" && !strings.Contains(strings.ToLower(b.Title), q) { + continue + } + matches = append(matches, bookmarkView{ + Bookmark: b, + CategoryLabel: strings.ReplaceAll(b.Category, "-", " "), + }) + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := resultsTmpl.Execute(w, matches); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} + +func bookmarkAdd() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form data", http.StatusBadRequest) + return + } + + title := strings.TrimSpace(r.FormValue("title")) + url := strings.TrimSpace(r.FormValue("url")) + category := r.FormValue("category") + + if title == "" || url == "" || category == "" { + http.Error(w, "title, url and category are required", http.StatusBadRequest) + return + } + + b := database.Bookmark{ + Title: title, + URL: url, + Category: category, + } + + id, err := database.AddBookmark(database.Bookmark{ + Title: title, + URL: url, + Category: category, + }) + if err != nil { + http.Error(w, "could not save bookmark", http.StatusInternalServerError) + return + } + b = database.Bookmark{ID: id, Title: title, URL: url, Category: category} + + view := bookmarkView{ + Bookmark: b, + CategoryLabel: strings.ReplaceAll(b.Category, "-", " "), + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := addTmpl.Execute(w, view); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} diff --git a/source/templates/pages/search.html b/source/templates/pages/search.html new file mode 100644 index 0000000..b37973b --- /dev/null +++ b/source/templates/pages/search.html @@ -0,0 +1,158 @@ + + + + + +Bookmark Search — samantha42.xyz + + + + + + + + + + +
+
+

bookmarked · crawled data

+

Personal web
Value self reliance +

+
+ + + + + +
+ + + + +
+ +
+ + + + + +
+ + +
+
+ + + + + \ No newline at end of file