about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2024-05-04 12:54:31 +0200
committerAlan Pearce2024-05-04 14:51:00 +0200
commit5b9e67fd5129dec75169a1a070c70f910dff6da2 (patch)
tree73a4656b60387556462e7d5384cac0919bc1da66
parenta2c97c10ee0a01277c51f85b15bdf8ee821f96db (diff)
downloadsearchix-5b9e67fd5129dec75169a1a070c70f910dff6da2.tar.lz
searchix-5b9e67fd5129dec75169a1a070c70f910dff6da2.tar.zst
searchix-5b9e67fd5129dec75169a1a070c70f910dff6da2.zip
feat: frontend search implementation
-rw-r--r--config.toml1
-rw-r--r--default.nix2
-rw-r--r--frontend/static/search.js26
-rw-r--r--frontend/templates/blocks/options.gotmpl13
-rw-r--r--internal/server/option.go15
-rw-r--r--internal/server/server.go110
6 files changed, 152 insertions, 15 deletions
diff --git a/config.toml b/config.toml
index 8c66184..9b6a35a 100644
--- a/config.toml
+++ b/config.toml
@@ -8,6 +8,7 @@ image-src = [
   "http://gc.zgo.at",
 ]
 script-src = [
+  "'self'",
   "http://gc.zgo.at",
 ]
 require-trusted-types-for = [
diff --git a/default.nix b/default.nix
index d3585e0..4face77 100644
--- a/default.nix
+++ b/default.nix
@@ -54,7 +54,7 @@ in
       editorconfig-checker.enable = true;
       prettier = {
         enable = true;
-        types_or = [ "plain-text" "yaml" "gotmpl" ];
+        types_or = [ "plain-text" "yaml" "gotmpl" "javascript" ];
         settings = {
           plugins = with pkgs.nodePackages; [
             "${prettier-plugin-go-template}/lib/node_modules/prettier-plugin-go-template/lib/index.js"
diff --git a/frontend/static/search.js b/frontend/static/search.js
new file mode 100644
index 0000000..20f1a7d
--- /dev/null
+++ b/frontend/static/search.js
@@ -0,0 +1,26 @@
+const search = document.getElementById("search");
+const results = document.getElementById("results");
+search.addEventListener("submit", function (ev) {
+  const url = new URL(this.action);
+  url.search = new URLSearchParams(new FormData(this)).toString();
+  const res = fetch(url, {
+    headers: {
+      fetch: "true",
+    },
+  })
+    .then(function (res) {
+      window.history.pushState(null, null, url);
+      if (res.ok) {
+        return res.text();
+      } else {
+        throw new Error(res.statusText);
+      }
+    })
+    .then(function (html) {
+      results.innerHTML = html;
+    })
+    .catch(function (error) {
+      console.error("fetch failed", error);
+    });
+  ev.preventDefault();
+});
diff --git a/frontend/templates/blocks/options.gotmpl b/frontend/templates/blocks/options.gotmpl
new file mode 100644
index 0000000..3451eb3
--- /dev/null
+++ b/frontend/templates/blocks/options.gotmpl
@@ -0,0 +1,13 @@
+{{ define "results" }}
+  {{ range $opt, $data := .Results }}
+  <details>
+    <summary>
+      {{ $opt }}
+    </summary>
+    <p>
+      {{ $data.Description }}
+    </p>
+  </details>
+  {{ end }}
+</div>
+{{ end }}
diff --git a/internal/server/option.go b/internal/server/option.go
new file mode 100644
index 0000000..be42689
--- /dev/null
+++ b/internal/server/option.go
@@ -0,0 +1,15 @@
+package server
+
+type NixValue struct {
+	Kind  string `json:"_type"`
+	Value string `json:"text"`
+}
+
+type Option struct {
+	Declarations []string
+	Default      NixValue
+	Description  string
+	Example      NixValue
+	ReadOnly     bool
+	Kind         string `json:"type"`
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index bb29ff3..09928b4 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -2,6 +2,7 @@ package server
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"html/template"
 	"io"
@@ -11,7 +12,9 @@ import (
 	"net/http"
 	"os"
 	"path"
+	"path/filepath"
 	"slices"
+	"strings"
 	"time"
 
 	cfg "searchix/internal/config"
@@ -54,7 +57,18 @@ const jsSnippet = template.HTML(livereload.JsSnippet) // #nosec G203
 
 type TemplateData struct {
 	LiveReload template.HTML
-	Data       map[string]interface{}
+	Query      string
+}
+
+type OptionResultData struct {
+	TemplateData
+	Query   string
+	Results map[string]Option
+}
+
+type TemplateCollection struct {
+	Pages  map[string]*template.Template
+	Blocks map[string]*template.Template
 }
 
 func applyDevModeOverrides(config *cfg.Config) {
@@ -62,6 +76,46 @@ func applyDevModeOverrides(config *cfg.Config) {
 	config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'")
 }
 
+const dummyTemplate = `{{ block "results" . }}{{ end }}`
+
+func loadTemplates() (*TemplateCollection, error) {
+	templateDir := path.Join("frontend", "templates")
+	templates := &TemplateCollection{
+		Pages:  make(map[string]*template.Template),
+		Blocks: make(map[string]*template.Template),
+	}
+	indexText, err := os.ReadFile(path.Join(templateDir, "index.gotmpl"))
+	if err != nil {
+		return nil, errors.WithMessage(err, "could not read index template")
+	}
+	index, err := template.New("index").Parse(string(indexText))
+	if err != nil {
+		return nil, errors.WithMessage(err, "could not parse index template")
+	}
+	templates.Pages["index"] = index
+	templates.Blocks = make(map[string]*template.Template)
+
+	templatePaths, err := filepath.Glob(path.Join(templateDir, "blocks", "*.gotmpl"))
+	if err != nil {
+		return nil, errors.WithMessage(err, "could not glob block templates")
+	}
+	for _, fullname := range templatePaths {
+		name, _ := strings.CutSuffix(path.Base(fullname), ".gotmpl")
+		content, err := os.ReadFile(fullname)
+		if err != nil {
+			return nil, errors.WithMessagef(err, "could not read template file %s", fullname)
+		}
+		tpl, err := template.New(name).Parse(string(content))
+		if err != nil {
+			return nil, errors.WithMessagef(err, "could not parse template file %s", fullname)
+		}
+		templates.Blocks[name] = template.Must(template.Must(tpl.Clone()).New("index").Parse(dummyTemplate))
+		templates.Pages[name] = template.Must(template.Must(tpl.Clone()).New("index").Parse(string(indexText)))
+	}
+
+	return templates, nil
+}
+
 func New(runtimeConfig *Config) (*Server, error) {
 	var err error
 	config, err = cfg.GetConfig()
@@ -88,18 +142,48 @@ func New(runtimeConfig *Config) (*Server, error) {
 		Repanic: true,
 	})
 
-	templatePaths := path.Join("frontend", "templates", "*.gotmpl")
-	tpl := template.Must(template.ParseGlob(templatePaths))
+	templates, err := loadTemplates()
+	if err != nil {
+		log.Panicf("could not load templates: %v", err)
+	}
 
 	top := http.NewServeMux()
 	mux := http.NewServeMux()
-	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
-		tdata := TemplateData{
-			LiveReload: jsSnippet,
-			Data:       make(map[string]interface{}),
+	indexData := TemplateData{
+		LiveReload: jsSnippet,
+	}
+	mux.HandleFunc("/{$}", func(w http.ResponseWriter, _ *http.Request) {
+		err := templates.Pages["index"].Execute(w, indexData)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	})
+
+	nixosOptions := make(map[string]Option)
+	jsonFile, err := os.ReadFile(path.Join("data", "test.json"))
+	if err != nil {
+		slog.Error(fmt.Sprintf("error reading json file: %v", err))
+	}
+	err = json.Unmarshal(jsonFile, &nixosOptions)
+	if err != nil {
+		slog.Error(fmt.Sprintf("error parsing json file: %v", err))
+	}
+	mux.HandleFunc("/options/results", func(w http.ResponseWriter, r *http.Request) {
+		tdata := OptionResultData{
+			TemplateData: indexData,
+			Query:        r.URL.Query().Get("query"),
+			Results:      nixosOptions,
+		}
+		var err error
+		if r.Header.Get("Fetch") == "true" {
+			slog.Debug("rendering template", "block", true)
+			err = templates.Blocks["options"].ExecuteTemplate(w, "index", tdata)
+		} else {
+			slog.Debug("rendering template", "block", false)
+			err = templates.Pages["options"].ExecuteTemplate(w, "index", tdata)
 		}
-		err := tpl.Execute(w, tdata)
 		if err != nil {
+			slog.Error(fmt.Sprintf("template error: %v", err))
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 		}
 	})
@@ -115,18 +199,16 @@ func New(runtimeConfig *Config) (*Server, error) {
 		if err != nil {
 			return nil, errors.WithMessage(err, "could not create file watcher")
 		}
-		err = fw.AddRecursive("frontend")
+		err = fw.AddRecursive(path.Join("frontend", "templates"))
 		if err != nil {
 			return nil, errors.WithMessage(err, "could not add directory to file watcher")
 		}
 		go fw.Start(func() {
-			t, err := template.ParseGlob(path.Join("frontend", "templates", "*.tmpl"))
+			templates, err = loadTemplates()
 			if err != nil {
-				slog.Error(fmt.Sprintf("could not parse template: %v", err))
-			} else {
-				tpl = t
-				liveReload.Reload()
+				slog.Error(fmt.Sprintf("could not reload templates: %v", err))
 			}
+			liveReload.Reload()
 		})
 	}