diff --git a/internal/app/routes.go b/internal/app/routes.go index 36682f2..2ab5a30 100644 --- a/internal/app/routes.go +++ b/internal/app/routes.go @@ -11,7 +11,10 @@ func registerRoutes(r *cais.Router, deps Deps, cfg cais.Config) { home := handlers.NewHomeHandler(deps.Site, deps.Inertia, deps.Loader) r.Get("/", home.Index) r.Get("/u/{login}", cais.StringParam("login", home.Show)) + r.Get("/u/{login}/charts", cais.StringParam("login", home.ShowCharts)) r.Get("/u/{login}/all", cais.StringParam("login", home.ShowAll)) + r.Get("/u/{login}/all/charts", cais.StringParam("login", home.ShowAllCharts)) r.Get("/u/{login}/orgs/{org}", cais.StringParams("login", "org", home.ShowOrg)) + r.Get("/u/{login}/orgs/{org}/charts", cais.StringParams("login", "org", home.ShowOrgCharts)) r.Get("/traffic/{owner}/{repo}", cais.StringParams("owner", "repo", home.RepoTraffic)) } diff --git a/internal/handlers/home.go b/internal/handlers/home.go index ac39970..f5543d9 100644 --- a/internal/handlers/home.go +++ b/internal/handlers/home.go @@ -25,54 +25,83 @@ func NewHomeHandler(site meta.Site, i *inertia.Inertia, loader *catalog.Loader) return &HomeHandler{site: site, inertia: i, loader: loader} } +const ( + viewCatalog = "catalog" + viewCharts = "charts" +) + func (h *HomeHandler) Index(w http.ResponseWriter, r *http.Request) { - h.render(w, r, catalog.Snapshot{}, "", "", false) + h.render(w, r, catalog.Snapshot{}, "", "", false, viewCatalog) } func (h *HomeHandler) Show(w http.ResponseWriter, r *http.Request, login string) { + h.showUser(w, r, login, viewCatalog) +} + +func (h *HomeHandler) ShowCharts(w http.ResponseWriter, r *http.Request, login string) { + h.showUser(w, r, login, viewCharts) +} + +func (h *HomeHandler) showUser(w http.ResponseWriter, r *http.Request, login, view string) { login = normalizeLogin(login) if login == "" { - h.render(w, r, catalog.Snapshot{}, "", "Type a GitHub username to open their catalog.", false) + h.render(w, r, catalog.Snapshot{}, "", "Type a GitHub username to open their catalog.", false, view) return } snap, err := h.loader.User(r.Context(), login) if err != nil { - h.render(w, r, catalog.Snapshot{}, login, catalogError(err), false) + h.render(w, r, catalog.Snapshot{}, login, catalogError(err), false, view) return } - h.render(w, r, snap, login, "", true) + h.render(w, r, snap, login, "", true, view) } func (h *HomeHandler) ShowAll(w http.ResponseWriter, r *http.Request, login string) { + h.showAll(w, r, login, viewCatalog) +} + +func (h *HomeHandler) ShowAllCharts(w http.ResponseWriter, r *http.Request, login string) { + h.showAll(w, r, login, viewCharts) +} + +func (h *HomeHandler) showAll(w http.ResponseWriter, r *http.Request, login, view string) { login = normalizeLogin(login) if login == "" { - h.render(w, r, catalog.Snapshot{}, "", "Type a GitHub username to open their catalog.", false) + h.render(w, r, catalog.Snapshot{}, "", "Type a GitHub username to open their catalog.", false, view) return } snap, err := h.loader.All(r.Context(), login) if err != nil { - h.render(w, r, catalog.Snapshot{}, login, catalogError(err), false) + h.render(w, r, catalog.Snapshot{}, login, catalogError(err), false, view) return } - h.render(w, r, snap, login, "", true) + h.render(w, r, snap, login, "", true, view) } func (h *HomeHandler) ShowOrg(w http.ResponseWriter, r *http.Request, login, org string) { + h.showOrg(w, r, login, org, viewCatalog) +} + +func (h *HomeHandler) ShowOrgCharts(w http.ResponseWriter, r *http.Request, login, org string) { + h.showOrg(w, r, login, org, viewCharts) +} + +func (h *HomeHandler) showOrg(w http.ResponseWriter, r *http.Request, login, org, view string) { login = normalizeLogin(login) org = normalizeLogin(org) if login == "" || org == "" { - h.render(w, r, catalog.Snapshot{}, login, "Type a GitHub username to open their catalog.", false) + h.render(w, r, catalog.Snapshot{}, login, "Type a GitHub username to open their catalog.", false, view) return } snap, err := h.loader.Org(r.Context(), login, org) if err != nil { - h.render(w, r, catalog.Snapshot{}, login, catalogError(err), false) + h.render(w, r, catalog.Snapshot{}, login, catalogError(err), false, view) return } - h.render(w, r, snap, login, "", true) + h.render(w, r, snap, login, "", true, view) } -func (h *HomeHandler) render(w http.ResponseWriter, r *http.Request, snap catalog.Snapshot, lookup, errMsg string, populated bool) { +func (h *HomeHandler) render(w http.ResponseWriter, r *http.Request, snap catalog.Snapshot, lookup, errMsg string, populated bool, view string) { props := inertia.Props{ "title": "Projects Viewer", "site": meta.ForRequest(h.site, r), @@ -83,6 +112,7 @@ func (h *HomeHandler) render(w http.ResponseWriter, r *http.Request, snap catalo "error": errMsg, "populated": populated, "lookup": lookup, + "view": view, } if snap.Orgs == nil { props["orgs"] = []catalog.Org{} diff --git a/internal/handlers/home_test.go b/internal/handlers/home_test.go index b844650..c0004bf 100644 --- a/internal/handlers/home_test.go +++ b/internal/handlers/home_test.go @@ -140,6 +140,74 @@ func TestHomeHandler_ShowUserCatalog(t *testing.T) { } } +func TestHomeHandler_ShowUserCatalogDefaultsToCatalogView(t *testing.T) { + h := newHomeHandler(t, homeGitHub{ + user: githubapi.User{Login: "octocat", Name: "The Octocat", PublicRepos: 8}, + repos: []githubapi.Repo{{Name: "hello-world", Stars: 10, OwnerLogin: "octocat"}}, + }) + rr := httptest.NewRecorder() + h.Show(rr, inertiaRequest(http.MethodGet, "/u/octocat", nil), "octocat") + if assertInertiaProp(t, rr, "view") != "catalog" { + t.Fatalf("view = %v, want catalog", assertInertiaProp(t, rr, "view")) + } +} + +func TestHomeHandler_ShowUserCharts(t *testing.T) { + h := newHomeHandler(t, homeGitHub{ + user: githubapi.User{Login: "octocat", Name: "The Octocat", PublicRepos: 8}, + repos: []githubapi.Repo{{Name: "hello-world", Stars: 10, OwnerLogin: "octocat"}}, + }) + rr := httptest.NewRecorder() + h.ShowCharts(rr, inertiaRequest(http.MethodGet, "/u/octocat/charts", nil), "octocat") + assertInertiaComponent(t, rr, "Home") + if assertInertiaProp(t, rr, "view") != "charts" { + t.Fatalf("view = %v, want charts", assertInertiaProp(t, rr, "view")) + } + if assertInertiaProp(t, rr, "populated") != true { + t.Fatal("expected populated") + } +} + +func TestHomeHandler_ShowAllCharts(t *testing.T) { + h := newHomeHandler(t, homeGitHub{ + token: true, + user: githubapi.User{Login: "puppe1990", Name: "Matheus"}, + repos: []githubapi.Repo{{Name: "cais", FullName: "puppe1990/cais", OwnerLogin: "puppe1990"}}, + orgs: []githubapi.Org{{Login: "hidden-org"}}, + orgRepos: []githubapi.Repo{{Name: "private-app", FullName: "hidden-org/private-app", OwnerLogin: "hidden-org", Private: true}}, + }) + rr := httptest.NewRecorder() + h.ShowAllCharts(rr, inertiaRequest(http.MethodGet, "/u/puppe1990/all/charts", nil), "puppe1990") + if assertInertiaProp(t, rr, "view") != "charts" { + t.Fatalf("view = %v, want charts", assertInertiaProp(t, rr, "view")) + } + source := assertInertiaProp(t, rr, "source").(map[string]any) + if source["type"] != catalog.SourceAll { + t.Fatalf("source = %#v", source) + } + repos := assertInertiaProp(t, rr, "repos").([]any) + if len(repos) != 2 { + t.Fatalf("repos = %#v", repos) + } +} + +func TestHomeHandler_ShowOrgCharts(t *testing.T) { + h := newHomeHandler(t, homeGitHub{ + user: githubapi.User{Login: "octocat", Name: "The Octocat"}, + orgs: []githubapi.Org{{Login: "github"}}, + orgRepos: []githubapi.Repo{{Name: "linguist", OwnerLogin: "github", Stars: 4}}, + }) + rr := httptest.NewRecorder() + h.ShowOrgCharts(rr, inertiaRequest(http.MethodGet, "/u/octocat/orgs/github/charts", nil), "octocat", "github") + if assertInertiaProp(t, rr, "view") != "charts" { + t.Fatalf("view = %v, want charts", assertInertiaProp(t, rr, "view")) + } + source := assertInertiaProp(t, rr, "source").(map[string]any) + if source["type"] != catalog.SourceOrg || source["login"] != "github" { + t.Fatalf("source = %#v", source) + } +} + func TestHomeHandler_ShowNotFound(t *testing.T) { h := newHomeHandler(t, homeGitHub{userErr: githubapi.ErrNotFound}) rr := httptest.NewRecorder() diff --git a/web/src/pages/Home.svelte b/web/src/pages/Home.svelte index 3776609..1181689 100644 --- a/web/src/pages/Home.svelte +++ b/web/src/pages/Home.svelte @@ -1,5 +1,5 @@ @@ -276,8 +284,8 @@ {#if populated} {#if hasTraffic}
{/if} {#if view === "catalog" || !hasTraffic} diff --git a/web/src/pages/Home.test.js b/web/src/pages/Home.test.js index 452986e..095a726 100644 --- a/web/src/pages/Home.test.js +++ b/web/src/pages/Home.test.js @@ -5,6 +5,7 @@ import Home from "./Home.svelte"; vi.mock("@inertiajs/svelte", () => ({ router: { get: vi.fn() }, + inertia: () => {}, })); describe("Home", () => { @@ -103,38 +104,58 @@ describe("Home", () => { expect(router.get).toHaveBeenCalledWith("/u/puppe1990/all", {}, expect.any(Object)); }); - test("switches from catalog cards to a charts tab", async () => { - render(Home, { - props: { - populated: true, - lookup: "octocat", - profile: { login: "octocat", name: "The Octocat", html_url: "https://github.com/octocat", avatar_url: "https://example.com/a.png", public_repos: 1, followers: 1, following: 1 }, - orgs: [], - source: { type: "user", login: "octocat" }, - repos: [ - { - name: "hello-world", - html_url: "https://github.com/octocat/hello-world", - stargazers_count: 10, - forks_count: 2, - language: "Go", - description: "demo", - updated_at: "2026-08-01T00:00:00Z", - traffic: { available: true, views: 40, view_uniques: 22, clones: 9, clone_uniques: 6 }, - }, - ], - error: "", + const trafficHomeProps = { + populated: true, + lookup: "octocat", + profile: { login: "octocat", name: "The Octocat", html_url: "https://github.com/octocat", avatar_url: "https://example.com/a.png", public_repos: 1, followers: 1, following: 1 }, + orgs: [], + source: { type: "user", login: "octocat" }, + repos: [ + { + name: "hello-world", + html_url: "https://github.com/octocat/hello-world", + stargazers_count: 10, + forks_count: 2, + language: "Go", + description: "demo", + updated_at: "2026-08-01T00:00:00Z", + traffic: { available: true, views: 40, view_uniques: 22, clones: 9, clone_uniques: 6 }, }, - }); - expect(screen.getByRole("tab", { name: "Charts" })).toBeInTheDocument(); - expect(screen.getByRole("link", { name: "hello-world" })).toBeInTheDocument(); - await fireEvent.click(screen.getByRole("tab", { name: "Charts" })); + ], + error: "", + }; + + test("links catalog and charts to their own routes", () => { + render(Home, { props: trafficHomeProps }); + const catalog = screen.getByRole("tab", { name: "Catalog" }); + const charts = screen.getByRole("tab", { name: "Charts" }); + expect(catalog).toHaveAttribute("href", "/u/octocat"); + expect(charts).toHaveAttribute("href", "/u/octocat/charts"); + expect(catalog).toHaveAttribute("aria-selected", "true"); + expect(charts).toHaveAttribute("aria-selected", "false"); + expect(screen.getByRole("tabpanel", { name: "Catalog" })).toBeInTheDocument(); + expect(screen.queryByRole("region", { name: "Top unique visitors" })).not.toBeInTheDocument(); + }); + + test("renders the charts panel on its own route", () => { + render(Home, { props: { ...trafficHomeProps, view: "charts" } }); expect(screen.getByRole("tab", { name: "Charts" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByRole("region", { name: "Top unique visitors" })).toBeInTheDocument(); expect(screen.queryByRole("tabpanel", { name: "Catalog" })).not.toBeInTheDocument(); - await fireEvent.click(screen.getByRole("tab", { name: "Catalog" })); - expect(screen.getByRole("tabpanel", { name: "Catalog" })).toBeInTheDocument(); - expect(screen.getByRole("link", { name: "hello-world" })).toBeInTheDocument(); + }); + + test("links the charts route for the combined catalog", () => { + render(Home, { + props: { ...trafficHomeProps, profile: { ...trafficHomeProps.profile, login: "puppe1990" }, source: { type: "all", login: "puppe1990" } }, + }); + expect(screen.getByRole("tab", { name: "Charts" })).toHaveAttribute("href", "/u/puppe1990/all/charts"); + }); + + test("links the charts route for an org catalog", () => { + render(Home, { + props: { ...trafficHomeProps, source: { type: "org", login: "github" } }, + }); + expect(screen.getByRole("tab", { name: "Charts" })).toHaveAttribute("href", "/u/octocat/orgs/github/charts"); }); test("paginates the catalog twenty repositories at a time", async () => { diff --git a/web/static/css/atlas.css b/web/static/css/atlas.css index e05c322..1db576b 100644 --- a/web/static/css/atlas.css +++ b/web/static/css/atlas.css @@ -1266,12 +1266,15 @@ a.stat:hover { } .view-tab { + display: inline-flex; + align-items: center; border: 0; background: transparent; color: var(--ink-dim); border-radius: 999px; padding: 0.42rem 1rem; font: 600 0.88rem/1 var(--font-ui); + text-decoration: none; cursor: pointer; }