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 {
|
||||
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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,75 @@
|
|||
{{define "title"}}{{if .Query}}{{.Query}} — {{end}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="results-layout">
|
||||
<!-- 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>
|
||||
<!-- Left Sidebar -->
|
||||
<aside class="left-sidebar" id="left-sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<div class="sidebar-nav-title">Categories</div>
|
||||
{{range .Categories}}
|
||||
<a href="/search?q={{$.Query | urlquery}}&category={{.}}" class="sidebar-nav-item {{if eq $.ActiveCategory .}}active{{end}}">
|
||||
<span class="sidebar-nav-item-icon">{{index $.CategoryIcons .}}</span>
|
||||
<span>{{.}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="results-column" id="results">
|
||||
<div class="sidebar-filters">
|
||||
<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" .}}
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<!-- Right Sidebar -->
|
||||
<aside class="right-sidebar" id="sidebar">
|
||||
{{if .Suggestions}}
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">Suggestions</div>
|
||||
<div class="sidebar-title">Related Searches</div>
|
||||
<div class="suggestion-list">
|
||||
{{range .Suggestions}}<span class="suggestion"><a href="/search?q={{. | urlquery}}">{{.}}</a></span>{{end}}
|
||||
</div>
|
||||
|
|
@ -43,4 +86,4 @@
|
|||
{{end}}
|
||||
</aside>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
@ -50,6 +50,15 @@ type PageData struct {
|
|||
UnresponsiveEngines [][2]string
|
||||
PageNumbers []PageNumber
|
||||
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.
|
||||
|
|
@ -73,6 +82,12 @@ type InfoboxView struct {
|
|||
ImgSrc string
|
||||
}
|
||||
|
||||
// FilterOption represents a filter radio option for the sidebar.
|
||||
type FilterOption struct {
|
||||
Label string
|
||||
Value string
|
||||
}
|
||||
|
||||
var (
|
||||
tmplFull *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.
|
||||
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{
|
||||
Query: query,
|
||||
Pageno: pageno,
|
||||
NumberOfResults: resp.NumberOfResults,
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue