feat(frontend): add three-column results layout with left sidebar navigation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2e7075adf1
commit
0e79b729fe
3 changed files with 122 additions and 23 deletions
|
|
@ -112,7 +112,8 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Format == contracts.FormatHTML {
|
if req.Format == contracts.FormatHTML {
|
||||||
pd := views.FromResponse(resp, req.Query, req.Pageno)
|
pd := views.FromResponse(resp, req.Query, req.Pageno,
|
||||||
|
r.FormValue("category"), r.FormValue("time"), r.FormValue("type"))
|
||||||
if err := views.RenderSearchAuto(w, r, pd); err != nil {
|
if err := views.RenderSearchAuto(w, r, pd); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,75 @@
|
||||||
{{define "title"}}{{if .Query}}{{.Query}} — {{end}}{{end}}
|
{{define "title"}}{{if .Query}}{{.Query}} — {{end}}{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="results-layout">
|
<div class="results-layout">
|
||||||
<!-- Compact search bar -->
|
<!-- Left Sidebar -->
|
||||||
<div class="search-compact">
|
<aside class="left-sidebar" id="left-sidebar">
|
||||||
<div class="search-box">
|
<nav class="sidebar-nav">
|
||||||
<form method="GET" action="/search" role="search" id="search-form">
|
<div class="sidebar-nav-title">Categories</div>
|
||||||
<input type="text" name="q" id="q" value="{{.Query}}" autocomplete="off" autofocus
|
{{range .Categories}}
|
||||||
hx-get="/search" hx-target="#results" hx-trigger="keyup changed delay:500ms" hx-include="this">
|
<a href="/search?q={{$.Query | urlquery}}&category={{.}}" class="sidebar-nav-item {{if eq $.ActiveCategory .}}active{{end}}">
|
||||||
<button type="submit" class="search-box-submit" aria-label="Search">
|
<span class="sidebar-nav-item-icon">{{index $.CategoryIcons .}}</span>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<span>{{.}}</span>
|
||||||
<circle cx="11" cy="11" r="8"/>
|
</a>
|
||||||
<path d="m21 21-4.35-4.35"/>
|
{{end}}
|
||||||
</svg>
|
</nav>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results -->
|
<div class="sidebar-filters">
|
||||||
<div class="results-column" id="results">
|
<div class="sidebar-filter-group">
|
||||||
|
<div class="sidebar-filter-label">Time</div>
|
||||||
|
{{range .TimeFilters}}
|
||||||
|
<label class="sidebar-filter-option">
|
||||||
|
<input type="radio" name="time" value="{{.Value}}" {{if eq $.ActiveTime .Value}}checked{{end}}>
|
||||||
|
<span>{{.Label}}</span>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-filter-group">
|
||||||
|
<div class="sidebar-filter-label">Type</div>
|
||||||
|
{{range .TypeFilters}}
|
||||||
|
<label class="sidebar-filter-option">
|
||||||
|
<input type="radio" name="type" value="{{.Value}}" {{if eq $.ActiveType .Value}}checked{{end}}>
|
||||||
|
<span>{{.Label}}</span>
|
||||||
|
</label>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Center Column -->
|
||||||
|
<div class="results-column">
|
||||||
|
<!-- Compact search bar -->
|
||||||
|
<div class="search-compact">
|
||||||
|
<div class="search-box">
|
||||||
|
<form method="GET" action="/search" role="search" id="search-form">
|
||||||
|
<input type="text" name="q" id="q" value="{{.Query}}" autocomplete="off" autofocus
|
||||||
|
hx-get="/search" hx-target="#results" hx-trigger="keyup changed delay:500ms" hx-include="this">
|
||||||
|
<button type="submit" class="search-box-submit" aria-label="Search">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<path d="m21 21-4.35-4.35"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile filter chips -->
|
||||||
|
<div class="mobile-filter-chips">
|
||||||
|
<a href="/search?q={{.Query | urlquery}}" class="mobile-filter-chip {{if not .ActiveCategory}}active{{end}}">All</a>
|
||||||
|
{{range .Categories}}
|
||||||
|
<a href="/search?q={{$.Query | urlquery}}&category={{.}}" class="mobile-filter-chip {{if eq $.ActiveCategory .}}active{{end}}">{{.}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results inner -->
|
||||||
{{template "results_inner" .}}
|
{{template "results_inner" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Right Sidebar -->
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="right-sidebar" id="sidebar">
|
||||||
{{if .Suggestions}}
|
{{if .Suggestions}}
|
||||||
<div class="sidebar-card">
|
<div class="sidebar-card">
|
||||||
<div class="sidebar-title">Suggestions</div>
|
<div class="sidebar-title">Related Searches</div>
|
||||||
<div class="suggestion-list">
|
<div class="suggestion-list">
|
||||||
{{range .Suggestions}}<span class="suggestion"><a href="/search?q={{. | urlquery}}">{{.}}</a></span>{{end}}
|
{{range .Suggestions}}<span class="suggestion"><a href="/search?q={{. | urlquery}}">{{.}}</a></span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,4 +86,4 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -50,6 +50,15 @@ type PageData struct {
|
||||||
UnresponsiveEngines [][2]string
|
UnresponsiveEngines [][2]string
|
||||||
PageNumbers []PageNumber
|
PageNumbers []PageNumber
|
||||||
ShowHeader bool
|
ShowHeader bool
|
||||||
|
// New fields for three-column layout
|
||||||
|
Categories []string
|
||||||
|
CategoryIcons map[string]string
|
||||||
|
DisabledCategories []string
|
||||||
|
ActiveCategory string
|
||||||
|
TimeFilters []FilterOption
|
||||||
|
TypeFilters []FilterOption
|
||||||
|
ActiveTime string
|
||||||
|
ActiveType string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultView is a template-friendly wrapper around a MainResult.
|
// ResultView is a template-friendly wrapper around a MainResult.
|
||||||
|
|
@ -73,6 +82,12 @@ type InfoboxView struct {
|
||||||
ImgSrc string
|
ImgSrc string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterOption represents a filter radio option for the sidebar.
|
||||||
|
type FilterOption struct {
|
||||||
|
Label string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tmplFull *template.Template
|
tmplFull *template.Template
|
||||||
tmplIndex *template.Template
|
tmplIndex *template.Template
|
||||||
|
|
@ -116,12 +131,52 @@ func OpenSearchXML(baseURL string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromResponse builds PageData from a search response and request params.
|
// FromResponse builds PageData from a search response and request params.
|
||||||
func FromResponse(resp contracts.SearchResponse, query string, pageno int) PageData {
|
func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData {
|
||||||
|
// Set defaults
|
||||||
|
if activeCategory == "" {
|
||||||
|
activeCategory = "all"
|
||||||
|
}
|
||||||
|
|
||||||
pd := PageData{
|
pd := PageData{
|
||||||
Query: query,
|
Query: query,
|
||||||
Pageno: pageno,
|
Pageno: pageno,
|
||||||
NumberOfResults: resp.NumberOfResults,
|
NumberOfResults: resp.NumberOfResults,
|
||||||
UnresponsiveEngines: resp.UnresponsiveEngines,
|
UnresponsiveEngines: resp.UnresponsiveEngines,
|
||||||
|
|
||||||
|
// New: categories with icons
|
||||||
|
Categories: []string{"all", "news", "images", "videos", "maps"},
|
||||||
|
DisabledCategories: []string{"shopping", "music", "weather"},
|
||||||
|
CategoryIcons: map[string]string{
|
||||||
|
"all": "🌐",
|
||||||
|
"news": "📰",
|
||||||
|
"images": "🖼️",
|
||||||
|
"videos": "🎬",
|
||||||
|
"maps": "🗺️",
|
||||||
|
"shopping": "🛒",
|
||||||
|
"music": "🎵",
|
||||||
|
"weather": "🌤️",
|
||||||
|
},
|
||||||
|
ActiveCategory: activeCategory,
|
||||||
|
|
||||||
|
// Time filters
|
||||||
|
TimeFilters: []FilterOption{
|
||||||
|
{Label: "Any time", Value: ""},
|
||||||
|
{Label: "Past hour", Value: "h"},
|
||||||
|
{Label: "Past 24 hours", Value: "d"},
|
||||||
|
{Label: "Past week", Value: "w"},
|
||||||
|
{Label: "Past month", Value: "m"},
|
||||||
|
{Label: "Past year", Value: "y"},
|
||||||
|
},
|
||||||
|
ActiveTime: activeTime,
|
||||||
|
|
||||||
|
// Type filters
|
||||||
|
TypeFilters: []FilterOption{
|
||||||
|
{Label: "All results", Value: ""},
|
||||||
|
{Label: "News", Value: "news"},
|
||||||
|
{Label: "Videos", Value: "video"},
|
||||||
|
{Label: "Images", Value: "image"},
|
||||||
|
},
|
||||||
|
ActiveType: activeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert results.
|
// Convert results.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue