Add dashboard and explore feeds
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
3e9cf7ed06bb49547c06625234d86157af98e337- Parents
-
681f412 - Tree
14138a1
3e9cf7e
3e9cf7ed06bb49547c06625234d86157af98e337681f412
14138a1| Status | File | + | - |
|---|---|---|---|
| M |
internal/social/feed.go
|
69 | 0 |
| M |
internal/social/social_test.go
|
27 | 0 |
| A |
internal/web/handlers/explore.go
|
112 | 0 |
| M |
internal/web/handlers/handlers.go
|
5 | 1 |
| M |
internal/web/handlers/hello.go
|
46 | 1 |
| M |
internal/web/server.go
|
1 | 0 |
| M |
internal/web/static/css/shithub.css
|
329 | 0 |
| A |
internal/web/templates/_feed_row.html
|
27 | 0 |
| A |
internal/web/templates/dashboard.html
|
80 | 0 |
| A |
internal/web/templates/explore/index.html
|
90 | 0 |
internal/web/handlers/explore.goadded@@ -0,0 +1,112 @@ | ||
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | |
| 3 | +package handlers | |
| 4 | + | |
| 5 | +import ( | |
| 6 | + "log/slog" | |
| 7 | + "net/http" | |
| 8 | + "strconv" | |
| 9 | + "strings" | |
| 10 | + "time" | |
| 11 | + | |
| 12 | + "github.com/jackc/pgx/v5/pgxpool" | |
| 13 | + | |
| 14 | + "github.com/tenseleyFlow/shithub/internal/social" | |
| 15 | + "github.com/tenseleyFlow/shithub/internal/web/render" | |
| 16 | +) | |
| 17 | + | |
| 18 | +const feedDisplayLimit int32 = 20 | |
| 19 | + | |
| 20 | +type exploreHandler struct { | |
| 21 | + render *render.Renderer | |
| 22 | + logger *slog.Logger | |
| 23 | + pool *pgxpool.Pool | |
| 24 | +} | |
| 25 | + | |
| 26 | +func (h exploreHandler) ServeExplore(w http.ResponseWriter, r *http.Request) { | |
| 27 | + h.serve(w, r, "Explore", "/explore", "activity") | |
| 28 | +} | |
| 29 | + | |
| 30 | +func (h exploreHandler) ServeTrending(w http.ResponseWriter, r *http.Request) { | |
| 31 | + h.serve(w, r, "Trending", "/trending", "trending") | |
| 32 | +} | |
| 33 | + | |
| 34 | +func (h exploreHandler) serve(w http.ResponseWriter, r *http.Request, title, path, activeTab string) { | |
| 35 | + var ( | |
| 36 | + feed []social.FeedItem | |
| 37 | + hasNext bool | |
| 38 | + nextURL string | |
| 39 | + trendingRepos []social.TrendingRepo | |
| 40 | + trendingUsers []social.TrendingUser | |
| 41 | + ) | |
| 42 | + if h.pool != nil { | |
| 43 | + deps := social.Deps{Pool: h.pool, Logger: h.logger} | |
| 44 | + feed, hasNext, nextURL = feedPageFor(r, func(cursor social.FeedCursor, limit int32) ([]social.FeedItem, error) { | |
| 45 | + return social.PublicFeed(r.Context(), deps, cursor, limit) | |
| 46 | + }) | |
| 47 | + var err error | |
| 48 | + trendingRepos, err = social.CachedTrendingRepos(r.Context(), deps, social.TrendingScopeWeek, 7, 10) | |
| 49 | + if err != nil && h.logger != nil { | |
| 50 | + h.logger.WarnContext(r.Context(), "explore trending repos", "error", err) | |
| 51 | + } | |
| 52 | + trendingUsers, err = social.CachedTrendingUsers(r.Context(), deps, social.TrendingScopeWeek, 7, 8) | |
| 53 | + if err != nil && h.logger != nil { | |
| 54 | + h.logger.WarnContext(r.Context(), "explore trending users", "error", err) | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + data := map[string]any{ | |
| 59 | + "Title": title, | |
| 60 | + "ActiveTab": activeTab, | |
| 61 | + "Feed": feed, | |
| 62 | + "FeedHasNext": hasNext, | |
| 63 | + "FeedNextURL": nextURL, | |
| 64 | + "TrendingRepos": trendingRepos, | |
| 65 | + "TrendingUsers": trendingUsers, | |
| 66 | + "Path": path, | |
| 67 | + } | |
| 68 | + if err := h.render.RenderPage(w, r, "explore/index", data); err != nil { | |
| 69 | + if h.logger != nil { | |
| 70 | + h.logger.Error("render explore", "error", err) | |
| 71 | + } | |
| 72 | + http.Error(w, "internal server error", http.StatusInternalServerError) | |
| 73 | + } | |
| 74 | +} | |
| 75 | + | |
| 76 | +func feedPageFor(r *http.Request, load func(social.FeedCursor, int32) ([]social.FeedItem, error)) ([]social.FeedItem, bool, string) { | |
| 77 | + items, err := load(parseFeedCursor(r), feedDisplayLimit+1) | |
| 78 | + if err != nil { | |
| 79 | + return nil, false, "" | |
| 80 | + } | |
| 81 | + if int32(len(items)) <= feedDisplayLimit { | |
| 82 | + return items, false, "" | |
| 83 | + } | |
| 84 | + display := items[:feedDisplayLimit] | |
| 85 | + return display, true, feedNextURL(r, display[len(display)-1]) | |
| 86 | +} | |
| 87 | + | |
| 88 | +func parseFeedCursor(r *http.Request) social.FeedCursor { | |
| 89 | + raw := r.URL.Query().Get("before") | |
| 90 | + if raw == "" { | |
| 91 | + return social.FeedCursor{} | |
| 92 | + } | |
| 93 | + parts := strings.SplitN(raw, "~", 2) | |
| 94 | + if len(parts) != 2 { | |
| 95 | + return social.FeedCursor{} | |
| 96 | + } | |
| 97 | + createdAt, err := time.Parse(time.RFC3339Nano, parts[0]) | |
| 98 | + if err != nil { | |
| 99 | + return social.FeedCursor{} | |
| 100 | + } | |
| 101 | + id, err := strconv.ParseInt(parts[1], 10, 64) | |
| 102 | + if err != nil || id <= 0 { | |
| 103 | + return social.FeedCursor{} | |
| 104 | + } | |
| 105 | + return social.FeedCursor{BeforeCreatedAt: createdAt, BeforeID: id} | |
| 106 | +} | |
| 107 | + | |
| 108 | +func feedNextURL(r *http.Request, item social.FeedItem) string { | |
| 109 | + q := r.URL.Query() | |
| 110 | + q.Set("before", item.CreatedAt.UTC().Format(time.RFC3339Nano)+"~"+strconv.FormatInt(item.ID, 10)) | |
| 111 | + return r.URL.Path + "?" + q.Encode() | |
| 112 | +} | |
internal/web/handlers/handlers.gomodified@@ -15,6 +15,7 @@ import ( | ||
| 15 | 15 | "time" |
| 16 | 16 | |
| 17 | 17 | "github.com/go-chi/chi/v5" |
| 18 | + "github.com/jackc/pgx/v5/pgxpool" | |
| 18 | 19 | |
| 19 | 20 | "github.com/tenseleyFlow/shithub/internal/auth/session" |
| 20 | 21 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
@@ -30,6 +31,7 @@ type Deps struct { | ||
| 30 | 31 | StaticFS fs.FS |
| 31 | 32 | LogoSVG string |
| 32 | 33 | SessionStore session.Store |
| 34 | + Pool *pgxpool.Pool | |
| 33 | 35 | // CookieSecure is the Secure flag for session-related cookies |
| 34 | 36 | // (currently the CSRF cookie). Mirrors session.Config.Secure |
| 35 | 37 | // from the loaded config so the CSRF cookie matches the |
@@ -253,7 +255,9 @@ func RegisterChi(r *chi.Mux, deps Deps) (*chi.Mux, middleware.PanicHandler, http | ||
| 253 | 255 | r.Use(middleware.Compress) |
| 254 | 256 | r.Use(middleware.Timeout(30 * time.Second)) |
| 255 | 257 | r.Use(csrf) |
| 256 | - r.Get("/", helloHandler{render: rr, logoSVG: deps.LogoSVG, logger: deps.Logger}.ServeHTTP) | |
| 258 | + r.Get("/", helloHandler{render: rr, logoSVG: deps.LogoSVG, logger: deps.Logger, pool: deps.Pool}.ServeHTTP) | |
| 259 | + r.Get("/explore", exploreHandler{render: rr, logger: deps.Logger, pool: deps.Pool}.ServeExplore) | |
| 260 | + r.Get("/trending", exploreHandler{render: rr, logger: deps.Logger, pool: deps.Pool}.ServeTrending) | |
| 257 | 261 | // /internal/panic is a dev affordance: GET it to trigger the |
| 258 | 262 | // panic-recovery path so an operator can confirm the styled 500 |
| 259 | 263 | // page renders. S35 will gate this behind a dev flag. |
internal/web/handlers/hello.gomodified@@ -7,6 +7,9 @@ import ( | ||
| 7 | 7 | "log/slog" |
| 8 | 8 | "net/http" |
| 9 | 9 | |
| 10 | + "github.com/jackc/pgx/v5/pgxpool" | |
| 11 | + | |
| 12 | + "github.com/tenseleyFlow/shithub/internal/social" | |
| 10 | 13 | "github.com/tenseleyFlow/shithub/internal/version" |
| 11 | 14 | "github.com/tenseleyFlow/shithub/internal/web/middleware" |
| 12 | 15 | "github.com/tenseleyFlow/shithub/internal/web/render" |
@@ -16,6 +19,7 @@ type helloHandler struct { | ||
| 16 | 19 | render *render.Renderer |
| 17 | 20 | logoSVG string |
| 18 | 21 | logger *slog.Logger |
| 22 | + pool *pgxpool.Pool | |
| 19 | 23 | } |
| 20 | 24 | |
| 21 | 25 | type helloData struct { |
@@ -49,13 +53,19 @@ type helloData struct { | ||
| 49 | 53 | } |
| 50 | 54 | |
| 51 | 55 | func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 56 | + viewer := middleware.CurrentUserFromContext(r.Context()) | |
| 57 | + if viewer.ID != 0 && h.pool != nil { | |
| 58 | + h.serveDashboard(w, r, viewer) | |
| 59 | + return | |
| 60 | + } | |
| 61 | + | |
| 52 | 62 | data := helloData{ |
| 53 | 63 | Title: "Welcome", |
| 54 | 64 | Version: version.Version, |
| 55 | 65 | Commit: version.Commit, |
| 56 | 66 | BuiltAt: version.BuiltAt, |
| 57 | 67 | LogoSVG: template.HTML(h.logoSVG), // #nosec G203 — embedded server-owned asset |
| 58 | - Viewer: middleware.CurrentUserFromContext(r.Context()), | |
| 68 | + Viewer: viewer, | |
| 59 | 69 | CSRFToken: middleware.CSRFTokenForRequest(r), |
| 60 | 70 | } |
| 61 | 71 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
@@ -64,3 +74,38 @@ func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
| 64 | 74 | http.Error(w, "internal server error", http.StatusInternalServerError) |
| 65 | 75 | } |
| 66 | 76 | } |
| 77 | + | |
| 78 | +func (h helloHandler) serveDashboard(w http.ResponseWriter, r *http.Request, viewer middleware.CurrentUser) { | |
| 79 | + deps := social.Deps{Pool: h.pool, Logger: h.logger} | |
| 80 | + feed, hasNext, nextURL := feedPageFor(r, func(cursor social.FeedCursor, limit int32) ([]social.FeedItem, error) { | |
| 81 | + return social.DashboardFeed(r.Context(), deps, viewer.ID, cursor, limit) | |
| 82 | + }) | |
| 83 | + repos, err := social.DashboardRepos(r.Context(), deps, viewer.ID, 8) | |
| 84 | + if err != nil && h.logger != nil { | |
| 85 | + h.logger.WarnContext(r.Context(), "dashboard repos", "error", err) | |
| 86 | + } | |
| 87 | + trendingRepos, err := social.CachedTrendingRepos(r.Context(), deps, social.TrendingScopeWeek, 7, 5) | |
| 88 | + if err != nil && h.logger != nil { | |
| 89 | + h.logger.WarnContext(r.Context(), "dashboard trending repos", "error", err) | |
| 90 | + } | |
| 91 | + trendingUsers, err := social.CachedTrendingUsers(r.Context(), deps, social.TrendingScopeWeek, 7, 5) | |
| 92 | + if err != nil && h.logger != nil { | |
| 93 | + h.logger.WarnContext(r.Context(), "dashboard trending users", "error", err) | |
| 94 | + } | |
| 95 | + | |
| 96 | + data := map[string]any{ | |
| 97 | + "Title": "Home", | |
| 98 | + "TopRepos": repos, | |
| 99 | + "Feed": feed, | |
| 100 | + "FeedHasNext": hasNext, | |
| 101 | + "FeedNextURL": nextURL, | |
| 102 | + "TrendingRepos": trendingRepos, | |
| 103 | + "TrendingUsers": trendingUsers, | |
| 104 | + } | |
| 105 | + if err := h.render.RenderPage(w, r, "dashboard", data); err != nil { | |
| 106 | + if h.logger != nil { | |
| 107 | + h.logger.Error("render dashboard", "error", err) | |
| 108 | + } | |
| 109 | + http.Error(w, "internal server error", http.StatusInternalServerError) | |
| 110 | + } | |
| 111 | +} | |
internal/web/server.gomodified@@ -145,6 +145,7 @@ func Run(ctx context.Context, opts Options) error { | ||
| 145 | 145 | StaticFS: StaticFS(), |
| 146 | 146 | LogoSVG: string(logoBytes), |
| 147 | 147 | SessionStore: sessionStore, |
| 148 | + Pool: pool, | |
| 148 | 149 | CookieSecure: cfg.Session.Secure, |
| 149 | 150 | } |
| 150 | 151 | if pool != nil { |
internal/web/static/css/shithub.cssmodified@@ -8479,3 +8479,332 @@ button.shithub-repo-action { | ||
| 8479 | 8479 | } |
| 8480 | 8480 | .shithub-imp-write { background: #ffd33d; color: #1a1f24; padding: 0 0.4em; border-radius: 4px; font-weight: 600; } |
| 8481 | 8481 | .shithub-imp-read { background: rgba(255, 255, 255, 0.2); padding: 0 0.4em; border-radius: 4px; } |
| 8482 | + | |
| 8483 | +/* S42 — social dashboard and Explore feed. */ | |
| 8484 | +.shithub-dashboard-page, | |
| 8485 | +.shithub-explore-page { | |
| 8486 | + width: 100%; | |
| 8487 | + padding: 1.5rem 1rem 2.5rem; | |
| 8488 | +} | |
| 8489 | +.shithub-dashboard-shell { | |
| 8490 | + display: grid; | |
| 8491 | + grid-template-columns: minmax(220px, 296px) minmax(0, 1fr) minmax(220px, 296px); | |
| 8492 | + gap: 2rem; | |
| 8493 | + max-width: 1280px; | |
| 8494 | + margin: 0 auto; | |
| 8495 | +} | |
| 8496 | +.shithub-explore-shell { | |
| 8497 | + display: grid; | |
| 8498 | + grid-template-columns: minmax(0, 1fr) minmax(220px, 320px); | |
| 8499 | + gap: 2rem; | |
| 8500 | + max-width: 1100px; | |
| 8501 | + margin: 0 auto; | |
| 8502 | +} | |
| 8503 | +.shithub-dashboard-left, | |
| 8504 | +.shithub-dashboard-right, | |
| 8505 | +.shithub-explore-right { | |
| 8506 | + min-width: 0; | |
| 8507 | +} | |
| 8508 | +.shithub-dashboard-left { | |
| 8509 | + position: sticky; | |
| 8510 | + top: 1rem; | |
| 8511 | + align-self: start; | |
| 8512 | +} | |
| 8513 | +.shithub-dashboard-sidehead, | |
| 8514 | +.shithub-feed-toolbar, | |
| 8515 | +.shithub-explore-head { | |
| 8516 | + display: flex; | |
| 8517 | + align-items: center; | |
| 8518 | + justify-content: space-between; | |
| 8519 | + gap: 0.75rem; | |
| 8520 | +} | |
| 8521 | +.shithub-dashboard-sidehead h2, | |
| 8522 | +.shithub-feed-toolbar h2, | |
| 8523 | +.shithub-side-panel h2, | |
| 8524 | +.shithub-trending-section h2 { | |
| 8525 | + margin: 0; | |
| 8526 | + font-size: 1rem; | |
| 8527 | +} | |
| 8528 | +.shithub-dashboard-main h1, | |
| 8529 | +.shithub-explore-head h1 { | |
| 8530 | + margin: 0 0 1.25rem; | |
| 8531 | + font-size: 1.6rem; | |
| 8532 | + line-height: 1.25; | |
| 8533 | +} | |
| 8534 | +.shithub-dashboard-filter { | |
| 8535 | + width: 100%; | |
| 8536 | + margin: 0.75rem 0 0.65rem; | |
| 8537 | + padding: 0.45rem 0.7rem; | |
| 8538 | + border: 1px solid var(--border-default); | |
| 8539 | + border-radius: 6px; | |
| 8540 | + background: var(--canvas-default); | |
| 8541 | + color: var(--fg-default); | |
| 8542 | +} | |
| 8543 | +.shithub-dashboard-repo-list, | |
| 8544 | +.shithub-feed-list, | |
| 8545 | +.shithub-trending-mini-list, | |
| 8546 | +.shithub-trending-user-list, | |
| 8547 | +.shithub-trending-repo-list { | |
| 8548 | + padding: 0; | |
| 8549 | + margin: 0; | |
| 8550 | + list-style: none; | |
| 8551 | +} | |
| 8552 | +.shithub-dashboard-repo-list { | |
| 8553 | + display: grid; | |
| 8554 | + gap: 0.45rem; | |
| 8555 | +} | |
| 8556 | +.shithub-dashboard-repo-list a, | |
| 8557 | +.shithub-trending-user-list a { | |
| 8558 | + display: inline-flex; | |
| 8559 | + align-items: center; | |
| 8560 | + gap: 0.45rem; | |
| 8561 | + min-width: 0; | |
| 8562 | + color: var(--fg-default); | |
| 8563 | + font-weight: 600; | |
| 8564 | +} | |
| 8565 | +.shithub-dashboard-repo-list img, | |
| 8566 | +.shithub-trending-user-list img, | |
| 8567 | +.shithub-feed-avatar img { | |
| 8568 | + border-radius: 50%; | |
| 8569 | +} | |
| 8570 | +.shithub-dashboard-empty, | |
| 8571 | +.shithub-feed-empty-inline { | |
| 8572 | + margin: 0.75rem 0 0; | |
| 8573 | + color: var(--fg-muted); | |
| 8574 | + font-size: 0.875rem; | |
| 8575 | +} | |
| 8576 | +.shithub-feed-toolbar { | |
| 8577 | + margin-bottom: 0.75rem; | |
| 8578 | +} | |
| 8579 | +.shithub-feed-list { | |
| 8580 | + border: 1px solid var(--border-default); | |
| 8581 | + border-radius: 6px; | |
| 8582 | + background: var(--canvas-default); | |
| 8583 | +} | |
| 8584 | +.shithub-feed-row { | |
| 8585 | + display: grid; | |
| 8586 | + grid-template-columns: 40px minmax(0, 1fr); | |
| 8587 | + gap: 1rem; | |
| 8588 | + padding: 1rem; | |
| 8589 | + border-top: 1px solid var(--border-default); | |
| 8590 | +} | |
| 8591 | +.shithub-feed-row:first-child { | |
| 8592 | + border-top: 0; | |
| 8593 | +} | |
| 8594 | +.shithub-feed-avatar { | |
| 8595 | + display: inline-flex; | |
| 8596 | + width: 40px; | |
| 8597 | + height: 40px; | |
| 8598 | +} | |
| 8599 | +.shithub-feed-head { | |
| 8600 | + margin: 0; | |
| 8601 | + color: var(--fg-muted); | |
| 8602 | + font-size: 0.95rem; | |
| 8603 | +} | |
| 8604 | +.shithub-feed-head a { | |
| 8605 | + color: var(--fg-default); | |
| 8606 | +} | |
| 8607 | +.shithub-feed-head time { | |
| 8608 | + display: inline-block; | |
| 8609 | + color: var(--fg-muted); | |
| 8610 | + font-size: 0.875rem; | |
| 8611 | +} | |
| 8612 | +.shithub-feed-target { | |
| 8613 | + font-weight: 600; | |
| 8614 | +} | |
| 8615 | +.shithub-feed-repo { | |
| 8616 | + margin-top: 0.8rem; | |
| 8617 | + padding: 0.85rem; | |
| 8618 | + border: 1px solid var(--border-default); | |
| 8619 | + border-radius: 6px; | |
| 8620 | + background: var(--canvas-subtle); | |
| 8621 | +} | |
| 8622 | +.shithub-feed-repo-title { | |
| 8623 | + display: inline-flex; | |
| 8624 | + align-items: center; | |
| 8625 | + gap: 0.4rem; | |
| 8626 | + font-weight: 600; | |
| 8627 | +} | |
| 8628 | +.shithub-feed-repo p { | |
| 8629 | + margin: 0.4rem 0 0; | |
| 8630 | + color: var(--fg-muted); | |
| 8631 | + font-size: 0.875rem; | |
| 8632 | +} | |
| 8633 | +.shithub-feed-meta { | |
| 8634 | + display: flex; | |
| 8635 | + flex-wrap: wrap; | |
| 8636 | + gap: 0.9rem; | |
| 8637 | + padding: 0; | |
| 8638 | + margin: 0.7rem 0 0; | |
| 8639 | + color: var(--fg-muted); | |
| 8640 | + list-style: none; | |
| 8641 | + font-size: 0.8rem; | |
| 8642 | +} | |
| 8643 | +.shithub-feed-meta li { | |
| 8644 | + display: inline-flex; | |
| 8645 | + align-items: center; | |
| 8646 | + gap: 0.3rem; | |
| 8647 | +} | |
| 8648 | +.shithub-lang-dot { | |
| 8649 | + width: 10px; | |
| 8650 | + height: 10px; | |
| 8651 | + border-radius: 50%; | |
| 8652 | + background: #3572a5; | |
| 8653 | +} | |
| 8654 | +.shithub-feed-empty { | |
| 8655 | + padding: 2rem; | |
| 8656 | + border: 1px solid var(--border-default); | |
| 8657 | + border-radius: 6px; | |
| 8658 | + background: var(--canvas-default); | |
| 8659 | + color: var(--fg-muted); | |
| 8660 | +} | |
| 8661 | +.shithub-feed-empty h2 { | |
| 8662 | + display: flex; | |
| 8663 | + align-items: center; | |
| 8664 | + gap: 0.5rem; | |
| 8665 | + margin: 0 0 0.5rem; | |
| 8666 | + color: var(--fg-default); | |
| 8667 | + font-size: 1rem; | |
| 8668 | +} | |
| 8669 | +.shithub-feed-empty p { | |
| 8670 | + margin: 0 0 1rem; | |
| 8671 | +} | |
| 8672 | +.shithub-feed-pagination { | |
| 8673 | + display: flex; | |
| 8674 | + justify-content: center; | |
| 8675 | + padding-top: 1rem; | |
| 8676 | +} | |
| 8677 | +.shithub-side-panel { | |
| 8678 | + padding: 0 0 1.25rem; | |
| 8679 | + margin-bottom: 1.25rem; | |
| 8680 | + border-bottom: 1px solid var(--border-default); | |
| 8681 | +} | |
| 8682 | +.shithub-side-panel h2 { | |
| 8683 | + display: flex; | |
| 8684 | + align-items: center; | |
| 8685 | + gap: 0.4rem; | |
| 8686 | + margin-bottom: 0.75rem; | |
| 8687 | +} | |
| 8688 | +.shithub-trending-mini-list, | |
| 8689 | +.shithub-trending-user-list { | |
| 8690 | + display: grid; | |
| 8691 | + gap: 0.85rem; | |
| 8692 | +} | |
| 8693 | +.shithub-trending-mini-list a { | |
| 8694 | + color: var(--fg-default); | |
| 8695 | + font-weight: 600; | |
| 8696 | +} | |
| 8697 | +.shithub-trending-mini-list p { | |
| 8698 | + margin: 0.2rem 0; | |
| 8699 | + color: var(--fg-muted); | |
| 8700 | + font-size: 0.85rem; | |
| 8701 | + line-height: 1.35; | |
| 8702 | +} | |
| 8703 | +.shithub-trending-mini-list span, | |
| 8704 | +.shithub-trending-user-list span { | |
| 8705 | + display: inline-flex; | |
| 8706 | + align-items: center; | |
| 8707 | + gap: 0.25rem; | |
| 8708 | + color: var(--fg-muted); | |
| 8709 | + font-size: 0.8rem; | |
| 8710 | +} | |
| 8711 | +.shithub-explore-tabs { | |
| 8712 | + display: inline-flex; | |
| 8713 | + align-items: center; | |
| 8714 | + gap: 0.35rem; | |
| 8715 | +} | |
| 8716 | +.shithub-explore-tabs a { | |
| 8717 | + display: inline-flex; | |
| 8718 | + align-items: center; | |
| 8719 | + gap: 0.35rem; | |
| 8720 | + padding: 0.45rem 0.7rem; | |
| 8721 | + border: 1px solid transparent; | |
| 8722 | + border-radius: 6px; | |
| 8723 | + color: var(--fg-muted); | |
| 8724 | + font-weight: 600; | |
| 8725 | +} | |
| 8726 | +.shithub-explore-tabs a:hover, | |
| 8727 | +.shithub-explore-tabs a.is-selected { | |
| 8728 | + border-color: var(--border-default); | |
| 8729 | + background: var(--canvas-subtle); | |
| 8730 | + color: var(--fg-default); | |
| 8731 | + text-decoration: none; | |
| 8732 | +} | |
| 8733 | +.shithub-trending-section { | |
| 8734 | + border: 1px solid var(--border-default); | |
| 8735 | + border-radius: 6px; | |
| 8736 | + background: var(--canvas-default); | |
| 8737 | +} | |
| 8738 | +.shithub-trending-section h2 { | |
| 8739 | + padding: 1rem; | |
| 8740 | + border-bottom: 1px solid var(--border-default); | |
| 8741 | +} | |
| 8742 | +.shithub-trending-repo-row { | |
| 8743 | + display: flex; | |
| 8744 | + justify-content: space-between; | |
| 8745 | + gap: 1rem; | |
| 8746 | + padding: 1rem; | |
| 8747 | + border-top: 1px solid var(--border-default); | |
| 8748 | +} | |
| 8749 | +.shithub-trending-repo-row:first-child { | |
| 8750 | + border-top: 0; | |
| 8751 | +} | |
| 8752 | +.shithub-trending-repo-row h3 { | |
| 8753 | + margin: 0; | |
| 8754 | + font-size: 1.05rem; | |
| 8755 | +} | |
| 8756 | +.shithub-trending-repo-row h3 a { | |
| 8757 | + display: inline-flex; | |
| 8758 | + align-items: center; | |
| 8759 | + gap: 0.4rem; | |
| 8760 | +} | |
| 8761 | +.shithub-trending-repo-row p { | |
| 8762 | + margin: 0.45rem 0 0; | |
| 8763 | + color: var(--fg-muted); | |
| 8764 | +} | |
| 8765 | +.shithub-trending-score { | |
| 8766 | + flex: 0 0 auto; | |
| 8767 | + align-self: flex-start; | |
| 8768 | + min-width: 2.5rem; | |
| 8769 | + padding: 0.15rem 0.45rem; | |
| 8770 | + border: 1px solid var(--border-default); | |
| 8771 | + border-radius: 999px; | |
| 8772 | + color: var(--fg-muted); | |
| 8773 | + text-align: center; | |
| 8774 | + font-size: 0.8rem; | |
| 8775 | + font-weight: 600; | |
| 8776 | +} | |
| 8777 | +@media (max-width: 960px) { | |
| 8778 | + .shithub-dashboard-shell, | |
| 8779 | + .shithub-explore-shell { | |
| 8780 | + grid-template-columns: 1fr; | |
| 8781 | + } | |
| 8782 | + .shithub-dashboard-left { | |
| 8783 | + position: static; | |
| 8784 | + } | |
| 8785 | + .shithub-dashboard-right, | |
| 8786 | + .shithub-explore-right { | |
| 8787 | + display: none; | |
| 8788 | + } | |
| 8789 | +} | |
| 8790 | +@media (max-width: 640px) { | |
| 8791 | + .shithub-feed-row { | |
| 8792 | + grid-template-columns: 32px minmax(0, 1fr); | |
| 8793 | + gap: 0.75rem; | |
| 8794 | + padding: 0.85rem; | |
| 8795 | + } | |
| 8796 | + .shithub-feed-avatar, | |
| 8797 | + .shithub-feed-avatar img { | |
| 8798 | + width: 32px; | |
| 8799 | + height: 32px; | |
| 8800 | + } | |
| 8801 | + .shithub-explore-head, | |
| 8802 | + .shithub-dashboard-sidehead, | |
| 8803 | + .shithub-feed-toolbar { | |
| 8804 | + align-items: flex-start; | |
| 8805 | + flex-direction: column; | |
| 8806 | + } | |
| 8807 | + .shithub-trending-repo-row { | |
| 8808 | + flex-direction: column; | |
| 8809 | + } | |
| 8810 | +} | |
internal/web/templates/_feed_row.htmladded@@ -0,0 +1,27 @@ | ||
| 1 | +{{ define "feed-row" -}} | |
| 2 | +<li class="shithub-feed-row"> | |
| 3 | + <a class="shithub-feed-avatar" href="/{{ .ActorUsername }}" aria-label="{{ .ActorUsername }}"> | |
| 4 | + <img src="/avatars/{{ .ActorUsername }}" alt="" width="40" height="40"> | |
| 5 | + </a> | |
| 6 | + <div class="shithub-feed-body"> | |
| 7 | + <p class="shithub-feed-head"> | |
| 8 | + <a href="/{{ .ActorUsername }}"><strong>{{ if .ActorDisplayName }}{{ .ActorDisplayName }}{{ else }}{{ .ActorUsername }}{{ end }}</strong></a> | |
| 9 | + <span>{{ .Verb }}</span> | |
| 10 | + {{ if .ItemURL }}<a href="{{ .ItemURL }}" class="shithub-feed-target">{{ .ItemTitle }}</a>{{ else if .ItemTitle }}<span class="shithub-feed-target">{{ .ItemTitle }}</span>{{ end }} | |
| 11 | + {{ if and .RepoFullName (ne .ItemTitle .RepoFullName) }}<span>in</span> <a href="{{ .RepoURL }}" class="shithub-feed-target">{{ .RepoFullName }}</a>{{ end }} | |
| 12 | + <time datetime="{{ .CreatedAt.Format "2006-01-02T15:04:05Z" }}">{{ relativeTime .CreatedAt }}</time> | |
| 13 | + </p> | |
| 14 | + {{ if .Repo }} | |
| 15 | + <div class="shithub-feed-repo"> | |
| 16 | + <a href="{{ .RepoURL }}" class="shithub-feed-repo-title">{{ octicon "repo" }} {{ .RepoFullName }}</a> | |
| 17 | + {{ if .Repo.Description }}<p>{{ .Repo.Description }}</p>{{ end }} | |
| 18 | + <ul class="shithub-feed-meta"> | |
| 19 | + {{ if .Repo.PrimaryLanguage }}<li><span class="shithub-lang-dot" aria-hidden="true"></span>{{ .Repo.PrimaryLanguage }}</li>{{ end }} | |
| 20 | + <li>{{ octicon "star" }} {{ .Repo.StarCount }}</li> | |
| 21 | + <li>{{ octicon "repo-forked" }} {{ .Repo.ForkCount }}</li> | |
| 22 | + </ul> | |
| 23 | + </div> | |
| 24 | + {{ end }} | |
| 25 | + </div> | |
| 26 | +</li> | |
| 27 | +{{- end }} | |
internal/web/templates/dashboard.htmladded@@ -0,0 +1,80 @@ | ||
| 1 | +{{ define "page" -}} | |
| 2 | +<section class="shithub-dashboard-page"> | |
| 3 | + <div class="shithub-dashboard-shell"> | |
| 4 | + <aside class="shithub-dashboard-left" aria-label="Your repositories"> | |
| 5 | + <div class="shithub-dashboard-sidehead"> | |
| 6 | + <h2>Top repositories</h2> | |
| 7 | + <a href="/new" class="shithub-button shithub-button-primary shithub-button-small">{{ octicon "repo" }} New</a> | |
| 8 | + </div> | |
| 9 | + <label class="sr-only" for="dashboard-repo-filter">Find a repository</label> | |
| 10 | + <input id="dashboard-repo-filter" class="shithub-dashboard-filter" type="search" placeholder="Find a repository..." autocomplete="off"> | |
| 11 | + {{ if .TopRepos }} | |
| 12 | + <ol class="shithub-dashboard-repo-list"> | |
| 13 | + {{ range .TopRepos }} | |
| 14 | + <li> | |
| 15 | + <a href="/{{ $.Viewer.Username }}/{{ .Name }}"><img src="/avatars/{{ $.Viewer.Username }}" alt="" width="18" height="18"> {{ $.Viewer.Username }}/{{ .Name }}</a> | |
| 16 | + </li> | |
| 17 | + {{ end }} | |
| 18 | + </ol> | |
| 19 | + {{ else }} | |
| 20 | + <p class="shithub-dashboard-empty">No repositories yet.</p> | |
| 21 | + {{ end }} | |
| 22 | + </aside> | |
| 23 | + | |
| 24 | + <main class="shithub-dashboard-main"> | |
| 25 | + <h1>Home</h1> | |
| 26 | + <div class="shithub-feed-toolbar"> | |
| 27 | + <h2>Feed</h2> | |
| 28 | + <a class="shithub-button shithub-button-small" href="/explore">{{ octicon "pulse" }} Explore</a> | |
| 29 | + </div> | |
| 30 | + {{ if .Feed }} | |
| 31 | + <ol class="shithub-feed-list"> | |
| 32 | + {{ range .Feed }}{{ template "feed-row" . }}{{ end }} | |
| 33 | + </ol> | |
| 34 | + {{ else }} | |
| 35 | + <div class="shithub-feed-empty"> | |
| 36 | + <h2>{{ octicon "people" }} Follow people and organizations to build your feed</h2> | |
| 37 | + <p>Stars, forks, pushes, issues, pull requests, and follows from your network will appear here.</p> | |
| 38 | + <a href="/explore" class="shithub-button">Explore activity</a> | |
| 39 | + </div> | |
| 40 | + {{ end }} | |
| 41 | + {{ if .FeedHasNext }} | |
| 42 | + <nav class="shithub-feed-pagination" aria-label="Feed pagination"> | |
| 43 | + <a class="shithub-button" href="{{ .FeedNextURL }}">More</a> | |
| 44 | + </nav> | |
| 45 | + {{ end }} | |
| 46 | + </main> | |
| 47 | + | |
| 48 | + <aside class="shithub-dashboard-right" aria-label="Discover"> | |
| 49 | + <section class="shithub-side-panel"> | |
| 50 | + <h2>{{ octicon "pulse" }} Trending repositories</h2> | |
| 51 | + {{ if .TrendingRepos }} | |
| 52 | + <ol class="shithub-trending-mini-list"> | |
| 53 | + {{ range .TrendingRepos }} | |
| 54 | + <li> | |
| 55 | + <a href="/{{ .Owner }}/{{ .Name }}">{{ .Owner }}/{{ .Name }}</a> | |
| 56 | + {{ if .Description }}<p>{{ .Description }}</p>{{ end }} | |
| 57 | + <span>{{ octicon "star" }} {{ .StarCount }}</span> | |
| 58 | + </li> | |
| 59 | + {{ end }} | |
| 60 | + </ol> | |
| 61 | + {{ else }}<p class="shithub-dashboard-empty">No public activity yet.</p>{{ end }} | |
| 62 | + </section> | |
| 63 | + | |
| 64 | + <section class="shithub-side-panel"> | |
| 65 | + <h2>{{ octicon "people" }} Trending developers</h2> | |
| 66 | + {{ if .TrendingUsers }} | |
| 67 | + <ol class="shithub-trending-user-list"> | |
| 68 | + {{ range .TrendingUsers }} | |
| 69 | + <li> | |
| 70 | + <a href="/{{ .Username }}"><img src="/avatars/{{ .Username }}" alt="" width="24" height="24"> {{ if .DisplayName }}{{ .DisplayName }}{{ else }}{{ .Username }}{{ end }}</a> | |
| 71 | + <span>@{{ .Username }}</span> | |
| 72 | + </li> | |
| 73 | + {{ end }} | |
| 74 | + </ol> | |
| 75 | + {{ else }}<p class="shithub-dashboard-empty">No developers trending yet.</p>{{ end }} | |
| 76 | + </section> | |
| 77 | + </aside> | |
| 78 | + </div> | |
| 79 | +</section> | |
| 80 | +{{- end }} | |
internal/web/templates/explore/index.htmladded@@ -0,0 +1,90 @@ | ||
| 1 | +{{ define "page" -}} | |
| 2 | +<section class="shithub-explore-page"> | |
| 3 | + <div class="shithub-explore-shell"> | |
| 4 | + <main class="shithub-explore-main"> | |
| 5 | + <div class="shithub-explore-head"> | |
| 6 | + <h1>{{ if eq .ActiveTab "trending" }}Trending{{ else }}Explore{{ end }}</h1> | |
| 7 | + <nav class="shithub-explore-tabs" aria-label="Explore"> | |
| 8 | + <a href="/explore"{{ if eq .ActiveTab "activity" }} class="is-selected" aria-current="page"{{ end }}>{{ octicon "pulse" }} Activity</a> | |
| 9 | + <a href="/trending"{{ if eq .ActiveTab "trending" }} class="is-selected" aria-current="page"{{ end }}>{{ octicon "star" }} Trending</a> | |
| 10 | + </nav> | |
| 11 | + </div> | |
| 12 | + | |
| 13 | + {{ if eq .ActiveTab "trending" }} | |
| 14 | + <section class="shithub-trending-section" aria-labelledby="trending-repositories-title"> | |
| 15 | + <h2 id="trending-repositories-title">Trending repositories</h2> | |
| 16 | + {{ if .TrendingRepos }} | |
| 17 | + <ol class="shithub-trending-repo-list"> | |
| 18 | + {{ range .TrendingRepos }} | |
| 19 | + <li class="shithub-trending-repo-row"> | |
| 20 | + <div> | |
| 21 | + <h3><a href="/{{ .Owner }}/{{ .Name }}">{{ octicon "repo" }} {{ .Owner }}/{{ .Name }}</a></h3> | |
| 22 | + {{ if .Description }}<p>{{ .Description }}</p>{{ end }} | |
| 23 | + <ul class="shithub-feed-meta"> | |
| 24 | + {{ if .PrimaryLanguage }}<li><span class="shithub-lang-dot" aria-hidden="true"></span>{{ .PrimaryLanguage }}</li>{{ end }} | |
| 25 | + <li>{{ octicon "star" }} {{ .StarCount }}</li> | |
| 26 | + <li>{{ octicon "repo-forked" }} {{ .ForkCount }}</li> | |
| 27 | + </ul> | |
| 28 | + </div> | |
| 29 | + <span class="shithub-trending-score">{{ .Score }}</span> | |
| 30 | + </li> | |
| 31 | + {{ end }} | |
| 32 | + </ol> | |
| 33 | + {{ else }}<p class="shithub-feed-empty-inline">No public repositories are trending yet.</p>{{ end }} | |
| 34 | + </section> | |
| 35 | + {{ else }} | |
| 36 | + <div class="shithub-feed-toolbar"> | |
| 37 | + <h2>Public activity</h2> | |
| 38 | + <a class="shithub-button shithub-button-small" href="/trending">{{ octicon "star" }} Trending</a> | |
| 39 | + </div> | |
| 40 | + {{ if .Feed }} | |
| 41 | + <ol class="shithub-feed-list"> | |
| 42 | + {{ range .Feed }}{{ template "feed-row" . }}{{ end }} | |
| 43 | + </ol> | |
| 44 | + {{ else }} | |
| 45 | + <div class="shithub-feed-empty"> | |
| 46 | + <h2>{{ octicon "pulse" }} No public activity yet</h2> | |
| 47 | + <p>Public stars, forks, pushes, issues, pull requests, and follows will appear here.</p> | |
| 48 | + </div> | |
| 49 | + {{ end }} | |
| 50 | + {{ if .FeedHasNext }} | |
| 51 | + <nav class="shithub-feed-pagination" aria-label="Public activity pagination"> | |
| 52 | + <a class="shithub-button" href="{{ .FeedNextURL }}">More</a> | |
| 53 | + </nav> | |
| 54 | + {{ end }} | |
| 55 | + {{ end }} | |
| 56 | + </main> | |
| 57 | + | |
| 58 | + <aside class="shithub-explore-right" aria-label="Trending"> | |
| 59 | + <section class="shithub-side-panel"> | |
| 60 | + <h2>{{ octicon "star" }} Repositories</h2> | |
| 61 | + {{ if .TrendingRepos }} | |
| 62 | + <ol class="shithub-trending-mini-list"> | |
| 63 | + {{ range .TrendingRepos }} | |
| 64 | + <li> | |
| 65 | + <a href="/{{ .Owner }}/{{ .Name }}">{{ .Owner }}/{{ .Name }}</a> | |
| 66 | + {{ if .Description }}<p>{{ .Description }}</p>{{ end }} | |
| 67 | + <span>{{ octicon "star" }} {{ .StarCount }}</span> | |
| 68 | + </li> | |
| 69 | + {{ end }} | |
| 70 | + </ol> | |
| 71 | + {{ else }}<p class="shithub-dashboard-empty">No public activity yet.</p>{{ end }} | |
| 72 | + </section> | |
| 73 | + | |
| 74 | + <section class="shithub-side-panel"> | |
| 75 | + <h2>{{ octicon "people" }} Developers</h2> | |
| 76 | + {{ if .TrendingUsers }} | |
| 77 | + <ol class="shithub-trending-user-list"> | |
| 78 | + {{ range .TrendingUsers }} | |
| 79 | + <li> | |
| 80 | + <a href="/{{ .Username }}"><img src="/avatars/{{ .Username }}" alt="" width="24" height="24"> {{ if .DisplayName }}{{ .DisplayName }}{{ else }}{{ .Username }}{{ end }}</a> | |
| 81 | + <span>@{{ .Username }}</span> | |
| 82 | + </li> | |
| 83 | + {{ end }} | |
| 84 | + </ol> | |
| 85 | + {{ else }}<p class="shithub-dashboard-empty">No developers trending yet.</p>{{ end }} | |
| 86 | + </section> | |
| 87 | + </aside> | |
| 88 | + </div> | |
| 89 | +</section> | |
| 90 | +{{- end }} | |