Gitfed
bastien-mrq/gitfed/ Commits/ 067a1f3

Redesign Explorer page: search, topic filter, repo cards

Replaces the plain table with a name filter, clickable topic chips, and repo cards matching Dashboard/repo-settings' visual language, plus a proper empty state.

bastien-mrq 2026-07-28 21:25 commit 067a1f300cc4829d78f6e63bf79fa12e0e44f385 parent 48c0bbac64959f84fb0860190775911f5caf1a26
5 files changed +114 −30
M CHANGELOG.md +4 −0
M cmd/gitfed-web/handlers_home.go +65 −16
M cmd/gitfed-web/render.go +23 −0
M internal/i18n/strings_en.go +11 −7
M internal/i18n/strings_fr.go +11 −7
CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md index aabc807..d7dcd22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.4 + +- Redesigned the Explorer page: a name filter, a topic-chip row (click to filter, click again to clear), and repo cards matching the rest of the app's visual language instead of a plain table. Proper empty state when a filter matches nothing. + ## 0.9.3 - Fixed inconsistent card widths on repo settings and account settings: forms (visibility/topics, grant collaborator, add SSH key, change password) were capped at 480px while list cards next to them weren't, making the layout look jagged. Only the login form keeps the narrow width now.
cmd/gitfed-web/handlers_home.go
diff --git a/cmd/gitfed-web/handlers_home.go b/cmd/gitfed-web/handlers_home.go index fd8c2f0..efc086a 100644 --- a/cmd/gitfed-web/handlers_home.go +++ b/cmd/gitfed-web/handlers_home.go @@ -4,6 +4,8 @@ import ( "bytes" "html/template" "net/http" + "sort" + "strings" "gitfed/internal/i18n" "gitfed/internal/store" @@ -11,24 +13,56 @@ import ( var exploreTpl = newTpl("explore", ` {{.Flash}} -<h1>{{t .Lang "explore.title"}}</h1> -{{if .Topic}}<p class="muted">{{t .Lang "explore.filtered_by"}} <span class="badge plain">{{.Topic}}</span> — <a href="/explore">{{t .Lang "explore.clear"}}</a></p>{{end}} -<table> -<tr><th>{{t .Lang "explore.col_repo"}}</th><th>{{t .Lang "explore.col_owner"}}</th><th>{{t .Lang "explore.col_topics"}}</th></tr> -{{range .Repos}} -<tr> - <td><a href="/r/{{.Name}}">{{.Name}}</a></td> - <td class="muted">{{.Owner}}</td> - <td>{{range .Topics}}<a class="badge plain" href="/explore?topic={{.}}">{{.}}</a>{{end}}</td> -</tr> +<div class="gf-page-head"><h1>{{t .Lang "explore.title"}}</h1><span class="count">{{len .Repos}} {{t .Lang "explore.repo_count"}}</span></div> +<p class="gf-page-sub">{{t .Lang "explore.subtitle"}}</p> + +<form class="gf-search" method="get" action="/explore"> + {{if .Topic}}<input type="hidden" name="topic" value="{{.Topic}}">{{end}} + {{icon "search"}} + <input type="search" name="q" value="{{.Query}}" placeholder="{{t .Lang "explore.search_placeholder"}}"> +</form> + +{{if .AllTopics}} +<div class="gf-topics-row"> + <span class="label">{{t .Lang "explore.topics_label"}}</span> + {{range .AllTopics}} + {{if eq . $.Topic}} + <a class="chip active" href="/explore{{if $.Query}}?q={{$.Query}}{{end}}">{{.}} {{icon "close"}}</a> + {{else}} + <a class="chip" href="/explore?topic={{.}}{{if $.Query}}&q={{$.Query}}{{end}}">{{.}}</a> + {{end}} + {{end}} +</div> +{{end}} + +<div class="gf-card"> +{{if .Repos}} + <div class="gf-repo-list"> + {{range .Repos}} + <div class="gf-repo-row"> + <div class="gf-repo-icon">{{icon "folder"}}</div> + <div class="gf-repo-main"> + <div class="name"><a href="/r/{{.Name}}">{{.Name}}</a> <span class="owner">— {{.Owner}}</span></div> + {{if .Topics}}<div class="topics">{{range .Topics}}<a href="/explore?topic={{.}}">{{.}}</a>{{end}}</div>{{end}} + </div> + <span class="badge trusted">{{t $.Lang "common.public"}}</span> + </div> + {{end}} + </div> {{else}} -<tr><td colspan="3" class="muted">{{t .Lang "explore.empty"}}</td></tr> + <div class="gf-empty"> + {{icon "search"}} + <p><strong>{{t .Lang "explore.empty_title"}}</strong></p> + <p class="muted">{{t .Lang "explore.empty_hint"}}</p> + <a class="clear" href="/explore">{{t .Lang "explore.clear"}} →</a> + </div> {{end}} -</table> +</div> `) func (s *server) handleExplore(w http.ResponseWriter, r *http.Request) { topic := r.URL.Query().Get("topic") + query := strings.TrimSpace(r.URL.Query().Get("q")) lang := s.lang(r) repos, err := s.ops.ListRepos() @@ -36,23 +70,38 @@ func (s *server) handleExplore(w http.ResponseWriter, r *http.Request) { s.serverError(w, r, err) return } + + topicSet := map[string]bool{} var visible []store.Repo + needle := strings.ToLower(query) for _, repo := range repos { if !repo.Public { continue } + for _, t := range repo.Topics { + topicSet[t] = true + } if topic != "" && !containsTopic(repo.Topics, topic) { continue } + if needle != "" && !strings.Contains(strings.ToLower(repo.Name), needle) { + continue + } visible = append(visible, repo) } + allTopics := make([]string, 0, len(topicSet)) + for t := range topicSet { + allTopics = append(allTopics, t) + } + sort.Strings(allTopics) var buf bytes.Buffer _ = exploreTpl.Execute(&buf, struct { - Repos []store.Repo - Topic, Lang string - Flash template.HTML - }{visible, topic, string(lang), flash(r)}) + Repos []store.Repo + AllTopics []string + Topic, Query, Lang string + Flash template.HTML + }{visible, allTopics, topic, query, string(lang), flash(r)}) s.render(w, r, i18n.T(lang, "explore.title"), "explore", template.HTML(buf.String())) }
cmd/gitfed-web/render.go
diff --git a/cmd/gitfed-web/render.go b/cmd/gitfed-web/render.go index 375c11a..8ba001c 100644 --- a/cmd/gitfed-web/render.go +++ b/cmd/gitfed-web/render.go @@ -139,6 +139,7 @@ const iconSprite = `<svg width="0" height="0" style="position:absolute" aria-hid <symbol id="ic-sliders" viewBox="0 0 24 24"><path d="M5 6h9M17.5 6H19M5 12h5.5M13 12H19M5 18h9M17.5 18H19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="12" cy="6" r="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="9.5" cy="12" r="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="15.5" cy="18" r="1.8" fill="none" stroke="currentColor" stroke-width="1.5"/></symbol> <symbol id="ic-arrow" viewBox="0 0 24 24"><path d="M4 12h14.5M13 6.5l6 5.5-6 5.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></symbol> <symbol id="ic-close" viewBox="0 0 24 24"><path d="M5.5 5.5l13 13M18.5 5.5l-13 13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></symbol> +<symbol id="ic-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="7" fill="none" stroke="currentColor" stroke-width="1.6"/><line x1="15.8" y1="15.8" x2="20.5" y2="20.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></symbol> <symbol id="ic-bell" viewBox="0 0 24 24"><path d="M6 10.5c0-3.3 2.7-6 6-6s6 2.7 6 6v3.3l1.6 2.7H4.4L6 13.8V10.5Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M9.8 18.5a2.3 2.3 0 0 0 4.4 0" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol> <symbol id="ic-history" viewBox="0 0 24 24"><circle cx="12" cy="12.5" r="8" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M12 8v4.7l3.3 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 3.3 5 6M16 3.3 19 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol> <symbol id="ic-user" viewBox="0 0 24 24"><circle cx="12" cy="8.3" r="3.3" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M4.8 19c1-3.2 3.9-5 7.2-5s6.2 1.8 7.2 5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol> @@ -353,6 +354,7 @@ const shellHeadSrc = `<!doctype html> /* ---------- dashboard / settings / admin ---------- */ .gf-page-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; } .gf-page-head h1 { font-size: 1.35rem; margin: 0; } + .gf-page-head .count { font-family: var(--mono); font-size: 0.82rem; color: var(--text-faint); } .gf-stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 1.25rem; } .gf-stat { background: var(--surface); padding: 1rem 1.1rem; } @@ -368,6 +370,10 @@ const shellHeadSrc = `<!doctype html> .gf-repo-main .name { font-family: var(--mono); font-size: 0.92rem; font-weight: 600; } .gf-repo-main .name a:hover { color: var(--accent); } .gf-repo-main .meta { font-size: 0.78rem; color: var(--text-faint); margin-top: 0.15rem; } + .gf-repo-main .owner { color: var(--text-faint); font-weight: 400; } + .gf-repo-main .topics { margin-top: 0.35rem; display: flex; gap: 0.35rem; flex-wrap: wrap; } + .gf-repo-main .topics a { font-family: var(--mono); font-size: 0.72rem; padding: 0.1rem 0.5rem; border-radius: 999px; background: var(--surface-3); color: var(--text-faint); text-decoration: none; } + .gf-repo-main .topics a:hover { color: var(--text-dim); } .gf-commit-list { display: flex; flex-direction: column; } .gf-commit-row { display: flex; align-items: center; gap: 0.9rem; padding: 0.8rem 1.1rem; border-bottom: 1px solid var(--border); } @@ -382,6 +388,23 @@ const shellHeadSrc = `<!doctype html> .gf-new-repo summary::-webkit-details-marker { display: none; } .gf-new-repo[open] summary { margin-bottom: 0.5rem; } + .gf-page-sub { color: var(--text-dim); font-size: 0.9rem; margin: -0.6rem 0 1.1rem; } + .gf-search { display: flex; align-items: center; gap: 0.6rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 0.55rem 0.8rem; margin-bottom: 1.1rem; color: var(--text-faint); } + .gf-search input { border: none; background: none; color: var(--text); font-size: 0.9rem; flex: 1; font-family: inherit; } + .gf-search input:focus { outline: none; } + .gf-search input::placeholder { color: var(--text-faint); } + + .gf-topics-row { display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; margin-bottom: 1.4rem; } + .gf-topics-row .label { font-size: 0.76rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.04em; margin-right: 0.3rem; } + .chip { display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--mono); font-size: 0.76rem; padding: 0.28rem 0.65rem; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); color: var(--text-dim); text-decoration: none; } + .chip:hover { border-color: var(--border-strong); color: var(--text); } + .chip.active { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 700; } + + .gf-empty { padding: 2.2rem 1.5rem; text-align: center; } + .gf-empty .icon { width: 30px; height: 30px; color: var(--text-faint); margin-bottom: 0.7rem; } + .gf-empty p { margin: 0.2rem 0; } + .gf-empty .clear { color: var(--accent); font-size: 0.86rem; margin-top: 0.6rem; display: inline-block; } + .gf-tabs { display: flex; gap: 0.3rem; border-bottom: 1px solid var(--border); margin-bottom: 1.25rem; } .gf-tabs button { margin: 0; padding: 0.6rem 0.9rem; font-size: 0.86rem; color: var(--text-dim); border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; background: none; border-radius: 0; cursor: pointer; font-family: inherit; } .gf-tabs button:hover { background: none; color: var(--text); }
internal/i18n/strings_en.go
diff --git a/internal/i18n/strings_en.go b/internal/i18n/strings_en.go index 0cb282c..3ca0b5b 100644 --- a/internal/i18n/strings_en.go +++ b/internal/i18n/strings_en.go @@ -16,13 +16,17 @@ var en = map[string]string{ "footer.changelog": "gitfed %s", // ---------- explore ---------- - "explore.title": "Explore public repositories", - "explore.filtered_by": "Filtered by topic", - "explore.clear": "clear", - "explore.col_repo": "Repo", - "explore.col_owner": "Owner", - "explore.col_topics": "Topics", - "explore.empty": "No public repositories yet.", + "explore.title": "Explore public repositories", + "explore.subtitle": "Public repos on this instance, readable by anyone — clonable over HTTPS with no account.", + "explore.repo_count": "public repos", + "explore.search_placeholder": "Filter by name…", + "explore.topics_label": "Topics", + "explore.clear": "clear", + "explore.col_repo": "Repo", + "explore.col_owner": "Owner", + "explore.col_topics": "Topics", + "explore.empty_title": "No public repositories match", + "explore.empty_hint": "Try a different topic or search, or clear the filter.", // ---------- landing ---------- "landing.title": "gitfed",
internal/i18n/strings_fr.go
diff --git a/internal/i18n/strings_fr.go b/internal/i18n/strings_fr.go index a4a58cc..a3fbfd7 100644 --- a/internal/i18n/strings_fr.go +++ b/internal/i18n/strings_fr.go @@ -16,13 +16,17 @@ var fr = map[string]string{ "footer.changelog": "gitfed %s", // ---------- explore ---------- - "explore.title": "Explorer les dépôts publics", - "explore.filtered_by": "Filtré par sujet", - "explore.clear": "effacer", - "explore.col_repo": "Dépôt", - "explore.col_owner": "Propriétaire", - "explore.col_topics": "Sujets", - "explore.empty": "Aucun dépôt public pour le moment.", + "explore.title": "Explorer les dépôts publics", + "explore.subtitle": "Dépôts publics de cette instance, lisibles par tout le monde — clonables en HTTPS sans compte.", + "explore.repo_count": "dépôts publics", + "explore.search_placeholder": "Filtrer par nom…", + "explore.topics_label": "Sujets", + "explore.clear": "effacer", + "explore.col_repo": "Dépôt", + "explore.col_owner": "Propriétaire", + "explore.col_topics": "Sujets", + "explore.empty_title": "Aucun dépôt public ne correspond", + "explore.empty_hint": "Essayez un autre sujet ou une autre recherche, ou effacez le filtre.", // ---------- landing ---------- "landing.title": "gitfed",