all repos — searchix @ 70340c792a3c956bb73337a51970cc16dfff391d

Search engine for NixOS, nix-darwin, home-manager and NUR users

feat: Port modals to no-js variant

Kaya
commit

70340c792a3c956bb73337a51970cc16dfff391d

parent

02efd4e911618272b5b4863a730c64654a34914a

M frontend/static/style.cssfrontend/static/style.css
@@ -171,9 +171,30 @@
dialog { max-width: unset; width: min(var(--min-width), 90%); + border: none; + outline: var(--border-width) solid var(--border); } -dialog > button { +dialog[open]:not(:modal) { + position: fixed; + inset: 0; + max-height: 85vh; + margin: auto; + z-index: 1000; + overflow: auto; +} + +dialog[open]:not(:modal)::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + background-color: var(--bg); + opacity: 0.8; +} + +dialog > button, +dialog > a.button { float: right; }
@@ -343,9 +364,11 @@ width: 100%;
padding: 1rem 1ch; } - dialog > button { + dialog > button, + dialog > a.button { float: none; display: block; + width: fit-content; margin-left: auto; }
M internal/components/combined.gointernal/components/combined.go
@@ -20,7 +20,7 @@
return g.Text("No description Available") } -func Combined(result *index.Result) g.Node { +func Combined(result *index.Result, qs string) g.Node { return Table( THead( Tr(
@@ -35,7 +35,7 @@ TBody(
g.MapIter(result.Hits, func(hit index.DocumentMatch) g.Node { return Tr( Td( - openCombinedDialogLink(hit.ID), + openCombinedDialogLink(hit.ID, qs), ), Td(Class("description"), CombinedData(hit.Data),
M internal/components/data.gointernal/components/data.go
@@ -5,6 +5,8 @@ "net/url"
"slices" "strconv" + g "alin.ovh/gomponents" + "alin.ovh/searchix/frontend" "alin.ovh/searchix/internal/config" search "alin.ovh/searchix/internal/index"
@@ -23,6 +25,8 @@ Description string
BaseURL config.URL Assets *frontend.AssetCollection SearchNav SearchNav + DetailContent g.Node + SearchURL string } type ResultData struct {
M internal/components/options.gointernal/components/options.go
@@ -9,7 +9,7 @@ g "alin.ovh/gomponents"
. "alin.ovh/gomponents/html" ) -func Options(result *index.Result) g.Node { +func Options(result *index.Result, qs string) g.Node { return Table( THead( Tr(
@@ -23,7 +23,7 @@ ),
TBody( g.MapIter(result.Hits, func(hit index.DocumentMatch) g.Node { if m, ok := hit.Data.(nix.Option); ok { - return optionRow(hit, m) + return optionRow(hit, m, qs) } return emptyOptionRow(hit)
@@ -49,10 +49,10 @@ ),
) } -func optionRow(hit index.DocumentMatch, o nix.Option) g.Node { +func optionRow(hit index.DocumentMatch, o nix.Option, qs string) g.Node { return Tr( Td( - openDialogLink(o.Name), + openDialogLink(o.Name, qs), ), Td(Class("description"), firstSentence(o.Description),
M internal/components/packages.gointernal/components/packages.go
@@ -9,7 +9,7 @@ g "alin.ovh/gomponents"
. "alin.ovh/gomponents/html" ) -func Packages(result *index.Result) g.Node { +func Packages(result *index.Result, qs string) g.Node { return Table( THead( Tr(
@@ -23,19 +23,19 @@ ),
TBody( g.MapIter(result.Hits, func(hit index.DocumentMatch) g.Node { if m, ok := hit.Data.(nix.Package); ok { - return packageRow(hit, m) + return packageRow(hit, m, qs) } - return emptyPackageRow(hit) + return emptyPackageRow(hit, qs) }), ), ) } -func emptyPackageRow(hit index.DocumentMatch) g.Node { +func emptyPackageRow(hit index.DocumentMatch, qs string) g.Node { return Tr( Td( - openDialogLink(hit.ID), + openDialogLink(hit.ID, qs), ), Td(Class("description"), g.Text("No description available"),
@@ -48,10 +48,10 @@ ),
) } -func packageRow(hit index.DocumentMatch, p nix.Package) g.Node { +func packageRow(hit index.DocumentMatch, p nix.Package, qs string) g.Node { return Tr( Td( - openDialogLink(p.Attribute), + openDialogLink(p.Attribute, qs), ), Td(Class("description"), g.Text(p.Description),
M internal/components/results.gointernal/components/results.go
@@ -22,14 +22,15 @@ if r.Results.Total == 0 {
return Span(Role("status"), g.Text("Nothing found")) } + qs := r.SearchNav.URL.RawQuery var content g.Node switch r.Source.Importer { case config.Options: - content = Options(r.Results) + content = Options(r.Results, qs) case config.Packages: - content = Packages(r.Results) + content = Packages(r.Results, qs) case config.All: - content = Combined(r.Results) + content = Combined(r.Results, qs) } return g.Group([]g.Node{
@@ -124,10 +125,20 @@ func ResultsPage(r ResultData) g.Node {
return SearchPage(r.TemplateData, r, Results(r)) } -func openDialogLink(attr string) g.Node { - return A(Class("open-dialog"), Href(attr), g.Text(attr)) +func openDialogLink(attr string, qs string) g.Node { + href := attr + if qs != "" { + href += "?" + qs + } + + return A(Class("open-dialog"), Href(href), g.Text(attr)) } -func openCombinedDialogLink(attr string) g.Node { - return A(Class("open-dialog"), Href("/"+attr), g.Text(attr)) +func openCombinedDialogLink(attr string, qs string) g.Node { + href := "/" + attr + if qs != "" { + href += "?" + qs + } + + return A(Class("open-dialog"), Href(href), g.Text(attr)) }
M internal/components/search.gointernal/components/search.go
@@ -34,7 +34,7 @@ MinLength("2"),
Name("query"), Type("search"), Value(r.Query), - AutoFocus(), + g.If(tdata.DetailContent == nil, AutoFocus()), g.Attr("spellcheck", "false"), g.Attr("autocapitalize", "none"), ),
@@ -113,7 +113,12 @@ ),
), Dialog( ID("dialog"), - Button(AutoFocus(), g.Text("Close")), + g.If(tdata.DetailContent != nil, g.Attr("open")), + g.If(tdata.DetailContent != nil, + A(Class("button"), Href(tdata.SearchURL), AutoFocus(), g.Text("Close")), + Button(AutoFocus(), g.Text("Close")), + ), + g.If(tdata.DetailContent != nil, tdata.DetailContent), ), NoScript( P(
M internal/server/global.gointernal/server/global.go
@@ -56,100 +56,115 @@
return h } -func (g *GlobalHandler) Search(source config.Source, w http.ResponseWriter, r *http.Request) { +func (g *GlobalHandler) ExecuteSearch( + source config.Source, + r *http.Request, +) (*components.ResultData, int, error) { facets := r.URL.Query() facets.Del("query") facets.Del("page") - var err error - if r.URL.Query().Has("query") || len(facets) > 0 { - qs := r.URL.Query().Get("query") - ctx := r.Context() - if qs != "" { - ctx = fctx.WithMeta(ctx, "search_query", qs) - } + if !r.URL.Query().Has("query") && len(facets) == 0 { + return nil, 0, nil + } - if len(qs) < 2 && len(facets) == 0 { - g.errorHandler(w, r, "Query too short", http.StatusBadRequest) + qs := r.URL.Query().Get("query") + ctx := r.Context() + if qs != "" { + ctx = fctx.WithMeta(ctx, "search_query", qs) + } - return + if len(qs) < 2 && len(facets) == 0 { + return nil, http.StatusBadRequest, fmt.Errorf("query too short") + } + + pageSize := index.DefaultPageSize + pageNumber := 1 + if pg := r.URL.Query().Get("page"); pg != "" { + var err error + pageNumber, err = strconv.Atoi(pg) + if err != nil || pageNumber > math.MaxInt { + return nil, http.StatusBadRequest, fmt.Errorf("bad query string") } + if pageNumber == 0 { + pageNumber = 1 + pageSize = config.MaxResultsShowAll + } + } + page := pagination.New(pageNumber, pageSize) - pageSize := index.DefaultPageSize - pageNumber := 1 - if pg := r.URL.Query().Get("page"); pg != "" { - pageNumber, err = strconv.Atoi(pg) - if err != nil || pageNumber > math.MaxInt { - g.errorHandler(w, r, "Bad query string", http.StatusBadRequest) + ctx, cancel := context.WithTimeout(ctx, g.cfg.Web.SearchTimeout.Duration) + results, err := g.index.Search(ctx, index.SearchRequest{ + Source: source, + Keyword: qs, + From: page.From, + PageSize: page.Size, + Facets: facets, + }) + cancel() - return - } - if pageNumber == 0 { - pageNumber = 1 - pageSize = config.MaxResultsShowAll - } + if err != nil { + if err == context.DeadlineExceeded { + return nil, http.StatusInternalServerError, fmt.Errorf("Search timed out") } - page := pagination.New(pageNumber, pageSize) + meta := fctx.Unwrap(err) + g.log.Error("search error", "error", err, "search_query", meta["search_query"]) + + return nil, http.StatusInternalServerError, fmt.Errorf("search error: %v", err) + } + if pageSize == config.MaxResultsShowAll && + results.Total > config.MaxResultsShowAll { + return nil, http.StatusBadRequest, fmt.Errorf("too many results, use pagination") + } + page.SetResults(results.Total) - ctx, cancel := context.WithTimeout(ctx, g.cfg.Web.SearchTimeout.Duration) - results, err := g.index.Search(ctx, index.SearchRequest{ - Source: source, - Keyword: qs, - From: page.From, - PageSize: page.Size, - Facets: facets, - }) - cancel() + searchDesc := fmt.Sprintf("%d results for %s", results.Total, qs) + if source.Importer != config.All { + searchDesc += " in " + source.Name + } - if err != nil { - if err == context.DeadlineExceeded { - g.errorHandler(w, r, "Search timed out", http.StatusInternalServerError) + searchURL := *source.LocalURL() + searchURL.Path += "search" + searchURL.RawQuery = r.URL.Query().Encode() - return - } - meta := fctx.Unwrap(err) - g.log.Error("search error", "error", err, "search_query", meta["search_query"]) - g.errorHandler(w, r, err.Error(), http.StatusInternalServerError) + tdata := &components.ResultData{ + TemplateData: components.TemplateData{ + ExtraHeadHTML: g.cfg.Web.ExtraHeadHTML, + Source: source, + Sources: sources, + Assets: g.assets, + Query: qs, + Title: g.getTitle(source, qs), + Description: searchDesc, + BaseURL: g.cfg.Web.BaseURL, + SearchNav: components.NewSearchNav(searchURL).WithPagination(page), + }, + Query: qs, + FacetQueries: facets, + Results: results, + } - return - } - if pageSize == config.MaxResultsShowAll && - results.Total > config.MaxResultsShowAll { - g.errorHandler(w, r, "Too many results, use pagination", http.StatusBadRequest) - } - page.SetResults(results.Total) + return tdata, 0, nil +} - searchDesc := fmt.Sprintf("%d results for %s", results.Total, qs) - if source.Importer != config.All { - searchDesc += " in " + source.Name - } +func (g *GlobalHandler) Search(source config.Source, w http.ResponseWriter, r *http.Request) { + tdata, statusCode, err := g.ExecuteSearch(source, r) + if err != nil { + g.errorHandler(w, r, err.Error(), statusCode) - tdata := components.ResultData{ - TemplateData: components.TemplateData{ - ExtraHeadHTML: g.cfg.Web.ExtraHeadHTML, - Source: source, - Sources: sources, - Assets: g.assets, - Query: qs, - Title: g.getTitle(source, qs), - Description: searchDesc, - BaseURL: g.cfg.Web.BaseURL, - SearchNav: components.NewSearchNav(*r.URL).WithPagination(page), - }, - Query: qs, - FacetQueries: facets, - Results: results, - } + return + } + if tdata != nil { w.Header().Add("Cache-Control", "max-age=300") w.Header().Add("Vary", "Fetch") var baseErr error if r.Header.Get("Fetch") == "true" { w.Header().Add("Content-Type", "text/html; charset=utf-8") - w.Header().Add("Title", g.getTitle(source, qs)) - baseErr = components.Results(tdata).Render(w) + w.Header().Add("Title", g.getTitle(source, tdata.Query)) + baseErr = components.Results(*tdata).Render(w) } else { - baseErr = components.ResultsPage(tdata).Render(w) + baseErr = components.ResultsPage(*tdata).Render(w) } if baseErr != nil { g.log.Error("template error", "template", source.Importer, "error", baseErr)
M internal/server/source.gointernal/server/source.go
@@ -73,21 +73,26 @@
return } - tdata := components.TemplateData{ - ExtraHeadHTML: h.global.cfg.Web.ExtraHeadHTML, - Source: h.source, - Sources: sources, - Assets: h.global.assets, - Title: h.getTitle(doc), - Description: components.MetaDescription(doc), - BaseURL: h.global.cfg.Web.BaseURL, - } var baseErr error if r.Header.Get("Fetch") == "true" { w.Header().Add("Content-Type", "text/html; charset=utf-8") w.Header().Add("Title", h.getTitle(doc)) baseErr = components.Detail(doc).Render(w) + } else if searchResult, _, searchErr := h.global.ExecuteSearch(h.source, r); searchErr == nil && searchResult != nil { + searchResult.Title = h.getTitle(doc) + searchResult.DetailContent = components.Detail(doc) + searchResult.SearchURL = searchResult.SearchNav.URL.String() + baseErr = components.ResultsPage(*searchResult).Render(w) } else { + tdata := components.TemplateData{ + ExtraHeadHTML: h.global.cfg.Web.ExtraHeadHTML, + Source: h.source, + Sources: sources, + Assets: h.global.assets, + Title: h.getTitle(doc), + Description: components.MetaDescription(doc), + BaseURL: h.global.cfg.Web.BaseURL, + } baseErr = components.DetailPage(tdata, doc).Render(w) } if baseErr != nil {