tenseleyflow/shithub / 373ed2f

Browse files

Align profile activity and stars UI

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
373ed2fb03f574ef0289179b5045371658c614f1
Parents
142e85f
Tree
21103a4

5 changed files

StatusFile+-
M docs/internal/profile.md 31 1
M internal/web/static/css/shithub.css 218 2
M internal/web/templates/_layout.html 14 0
M internal/web/templates/profile/stars_tab.html 133 36
M internal/web/templates/profile/view.html 40 9
docs/internal/profile.mdmodified
@@ -7,8 +7,10 @@ S09 shipped the public `/{username}` page and the `/avatars/{username}` route. L
7
 | Route | Source | Notes |
7
 | Route | Source | Notes |
8
 |---|---|---|
8
 |---|---|---|
9
 | `GET /{username}` | profile.serveProfile | Public profile. citext lookup; canonical-case 301; reserved short-circuit. |
9
 | `GET /{username}` | profile.serveProfile | Public profile. citext lookup; canonical-case 301; reserved short-circuit. |
10
+| `GET /{username}?tab=repositories` | profile.serveRepositoriesTab | Visibility-filtered user-owned repositories. |
11
+| `GET /{username}?tab=stars` | profile.serveStarsTab | Visibility-filtered starred repositories with search, type, language, and sort controls. |
10
 | `POST /{username}/contribution-settings` | profile.contributionSettingsUpdate | Auth required. Profile owner toggles private contribution counts. |
12
 | `POST /{username}/contribution-settings` | profile.contributionSettingsUpdate | Auth required. Profile owner toggles private contribution counts. |
11
-| `POST /{username}/pins` | profile.pinsUpdate | Auth required. Profile owner saves up to six public owned repositories. |
13
+| `POST /{username}/pins` | profile.pinsUpdate | Auth required. Profile owner saves up to six public affiliated repositories. |
12
 | `GET /avatars/{username}` | profile.serveAvatar | Streams uploaded avatar OR falls back to deterministic SVG identicon. |
14
 | `GET /avatars/{username}` | profile.serveAvatar | Streams uploaded avatar OR falls back to deterministic SVG identicon. |
13
 
15
 
14
 `/{username}` is the **catch-all** — chi matches static routes in registration order, so the wildcard is registered last via the `ProfileMounter` hook. The reserved-name list (`internal/auth/reserved.go`) is the second line of defense if a future top-level route is added but not registered before the wildcard.
16
 `/{username}` is the **catch-all** — chi matches static routes in registration order, so the wildcard is registered last via the `ProfileMounter` hook. The reserved-name list (`internal/auth/reserved.go`) is the second line of defense if a future top-level route is added but not registered before the wildcard.
@@ -106,6 +108,34 @@ The overview contribution calendar is computed from local Git history:
106
 - Arbitrary public repositories outside the user's affiliation remain verified-email-only to avoid spoofed username/display-name commits.
108
 - Arbitrary public repositories outside the user's affiliation remain verified-email-only to avoid spoofed username/display-name commits.
107
 - Private repositories are excluded by default. When the profile owner enables "Private contributions", private user-owned and member-org repositories contribute to the aggregate graph counts, but repository names and commit metadata are not exposed.
109
 - Private repositories are excluded by default. When the profile owner enables "Private contributions", private user-owned and member-org repositories contribute to the aggregate graph counts, but repository names and commit metadata are not exposed.
108
 
110
 
111
+The "Contribution activity" timeline below the graph reuses the same
112
+visibility gates:
113
+
114
+- Commit rows are grouped by month and repository, with private repository
115
+  names collapsed to "Private repositories".
116
+- Public user-owned repositories created during the selected contribution
117
+  window are shown as "Created repositories" entries.
118
+- Issues and pull requests authored by the profile user are loaded through
119
+  the issues sqlc package and then post-filtered with `policy.IsVisibleTo`
120
+  before rendering. PR rows distinguish open, closed, and merged counts.
121
+- The first month is shown initially; the "Show more activity" button expands
122
+  older months in place without another request.
123
+
124
+## Stars tab
125
+
126
+`/{username}?tab=stars` uses the same profile shell as the GitHub stars page:
127
+
128
+- The left profile sidebar matches the overview profile metadata and
129
+  organization badges.
130
+- The placeholder Lists panel is rendered so the page layout matches GitHub
131
+  while list management remains future work.
132
+- Starred repositories are loaded in one bounded scan (`starsTabScanLimit`) and
133
+  then visibility-filtered with `policy.IsVisibleTo`.
134
+- Both user-owned and organization-owned repository stars render from the
135
+  stored owner slug, so org-owned stars do not require an extra owner lookup.
136
+- Filters are applied server-side for search text, repository type, language,
137
+  and sort order (`recently-starred`, `recently-active`, `stars`).
138
+
109
 ## Self-view enrichment
139
 ## Self-view enrichment
110
 
140
 
111
 When the viewer's session matches the profile's user (`viewer.ID == user.ID`):
141
 When the viewer's session matches the profile's user (`viewer.ID == user.ID`):
internal/web/static/css/shithub.cssmodified
@@ -1627,12 +1627,15 @@ button.shithub-contrib-setting-item:hover {
1627
 .shithub-profile-activity h2 {
1627
 .shithub-profile-activity h2 {
1628
   margin-bottom: 1.5rem;
1628
   margin-bottom: 1.5rem;
1629
 }
1629
 }
1630
+.shithub-profile-activity-timeline {
1631
+  display: grid;
1632
+  gap: 1.5rem;
1633
+}
1630
 .shithub-profile-activity-row {
1634
 .shithub-profile-activity-row {
1631
   display: grid;
1635
   display: grid;
1632
   grid-template-columns: 96px minmax(0, 1fr);
1636
   grid-template-columns: 96px minmax(0, 1fr);
1633
   gap: 1.5rem;
1637
   gap: 1.5rem;
1634
   align-items: start;
1638
   align-items: start;
1635
-  margin-bottom: 1.5rem;
1636
 }
1639
 }
1637
 .shithub-profile-activity-month {
1640
 .shithub-profile-activity-month {
1638
   color: var(--fg-muted);
1641
   color: var(--fg-muted);
@@ -1640,10 +1643,15 @@ button.shithub-contrib-setting-item:hover {
1640
   font-size: 0.875rem;
1643
   font-size: 0.875rem;
1641
   padding-top: 0.15rem;
1644
   padding-top: 0.15rem;
1642
 }
1645
 }
1646
+.shithub-profile-activity-list {
1647
+  display: grid;
1648
+  gap: 1.25rem;
1649
+  min-width: 0;
1650
+}
1643
 .shithub-profile-activity-item {
1651
 .shithub-profile-activity-item {
1644
   position: relative;
1652
   position: relative;
1645
   display: flex;
1653
   display: flex;
1646
-  align-items: center;
1654
+  align-items: flex-start;
1647
   gap: 0.75rem;
1655
   gap: 0.75rem;
1648
   min-height: 2.25rem;
1656
   min-height: 2.25rem;
1649
   color: var(--fg-default);
1657
   color: var(--fg-default);
@@ -1671,6 +1679,201 @@ button.shithub-contrib-setting-item:hover {
1671
   color: var(--fg-muted);
1679
   color: var(--fg-muted);
1672
   flex: 0 0 auto;
1680
   flex: 0 0 auto;
1673
 }
1681
 }
1682
+.shithub-profile-activity-body {
1683
+  min-width: 0;
1684
+  padding-top: 0.35rem;
1685
+}
1686
+.shithub-profile-activity-body strong {
1687
+  display: block;
1688
+  font-weight: 600;
1689
+}
1690
+.shithub-profile-activity-repos {
1691
+  list-style: none;
1692
+  margin: 0.5rem 0 0;
1693
+  padding: 0;
1694
+  display: grid;
1695
+  gap: 0.2rem;
1696
+  color: var(--fg-muted);
1697
+  font-size: 0.875rem;
1698
+}
1699
+.shithub-profile-activity-repos li {
1700
+  display: flex;
1701
+  flex-wrap: wrap;
1702
+  align-items: center;
1703
+  gap: 0.45rem;
1704
+  min-width: 0;
1705
+}
1706
+.shithub-profile-activity-repos a {
1707
+  color: var(--fg-muted);
1708
+  text-decoration: none;
1709
+}
1710
+.shithub-profile-activity-repos a:hover {
1711
+  color: var(--accent-fg);
1712
+  text-decoration: underline;
1713
+}
1714
+.shithub-profile-activity-lang {
1715
+  display: inline-flex;
1716
+  align-items: center;
1717
+  gap: 0.25rem;
1718
+}
1719
+.shithub-profile-activity-state {
1720
+  display: inline-flex;
1721
+  align-items: center;
1722
+  gap: 0.25rem;
1723
+}
1724
+.shithub-profile-activity-state b {
1725
+  display: inline-flex;
1726
+  align-items: center;
1727
+  justify-content: center;
1728
+  min-width: 1.25rem;
1729
+  height: 1.25rem;
1730
+  padding: 0 0.35rem;
1731
+  border-radius: 999px;
1732
+  color: #fff;
1733
+  background: var(--fg-muted);
1734
+  font-size: 0.75rem;
1735
+  font-weight: 600;
1736
+}
1737
+.shithub-profile-activity-state.is-open b {
1738
+  background: #2da44e;
1739
+}
1740
+.shithub-profile-activity-state.is-merged b {
1741
+  background: #8250df;
1742
+}
1743
+.shithub-profile-activity-state.is-closed b {
1744
+  background: #cf222e;
1745
+}
1746
+.shithub-profile-stars-page .shithub-user-profile-container {
1747
+  align-items: start;
1748
+}
1749
+.shithub-stars-main {
1750
+  display: grid;
1751
+  gap: 2rem;
1752
+}
1753
+.shithub-stars-section-head {
1754
+  display: flex;
1755
+  align-items: center;
1756
+  justify-content: space-between;
1757
+  gap: 1rem;
1758
+  margin-bottom: 1rem;
1759
+}
1760
+.shithub-stars-section-head h2,
1761
+.shithub-stars-results h2 {
1762
+  margin: 0;
1763
+  font-size: 1rem;
1764
+  font-weight: 400;
1765
+}
1766
+.shithub-stars-section-head h2 span {
1767
+  color: var(--fg-muted);
1768
+}
1769
+.shithub-stars-list-actions {
1770
+  display: flex;
1771
+  gap: 0.5rem;
1772
+}
1773
+.shithub-stars-empty-list {
1774
+  min-height: 148px;
1775
+  border: 1px solid var(--border-default);
1776
+  border-radius: 6px;
1777
+  display: grid;
1778
+  place-items: center;
1779
+  align-content: center;
1780
+  gap: 0.55rem;
1781
+  padding: 1.5rem;
1782
+  text-align: center;
1783
+  color: var(--fg-default);
1784
+}
1785
+.shithub-stars-empty-list > span {
1786
+  color: var(--fg-muted);
1787
+}
1788
+.shithub-stars-empty-list p {
1789
+  margin: 0;
1790
+  color: var(--fg-muted);
1791
+  font-size: 0.875rem;
1792
+}
1793
+.shithub-stars-results {
1794
+  min-width: 0;
1795
+}
1796
+.shithub-stars-toolbar {
1797
+  display: flex;
1798
+  align-items: center;
1799
+  gap: 0.5rem;
1800
+  margin: 1rem 0;
1801
+}
1802
+.shithub-stars-search {
1803
+  position: relative;
1804
+  flex: 1 1 240px;
1805
+  min-width: 180px;
1806
+}
1807
+.shithub-stars-search svg {
1808
+  position: absolute;
1809
+  left: 0.65rem;
1810
+  top: 50%;
1811
+  transform: translateY(-50%);
1812
+  color: var(--fg-muted);
1813
+  pointer-events: none;
1814
+}
1815
+.shithub-stars-search input {
1816
+  width: 100%;
1817
+  padding-left: 2rem;
1818
+}
1819
+.shithub-stars-toolbar select {
1820
+  min-height: 32px;
1821
+  padding: 0 1.75rem 0 0.75rem;
1822
+  border: 1px solid var(--border-default);
1823
+  border-radius: 6px;
1824
+  background: var(--canvas-subtle);
1825
+  color: var(--fg-default);
1826
+  font: inherit;
1827
+}
1828
+.shithub-stars-list {
1829
+  list-style: none;
1830
+  margin: 0;
1831
+  padding: 0;
1832
+  border-top: 1px solid var(--border-default);
1833
+}
1834
+.shithub-stars-row {
1835
+  display: grid;
1836
+  grid-template-columns: minmax(0, 1fr) auto;
1837
+  gap: 1rem;
1838
+  align-items: start;
1839
+  padding: 1.25rem 0;
1840
+  border-bottom: 1px solid var(--border-default);
1841
+}
1842
+.shithub-stars-row h3 {
1843
+  margin: 0;
1844
+  font-size: 1.125rem;
1845
+  font-weight: 600;
1846
+}
1847
+.shithub-stars-row h3 a {
1848
+  color: var(--accent-fg);
1849
+  text-decoration: none;
1850
+}
1851
+.shithub-stars-row h3 a:hover {
1852
+  text-decoration: underline;
1853
+}
1854
+.shithub-stars-row p {
1855
+  margin: 0.45rem 0 0;
1856
+  color: var(--fg-muted);
1857
+}
1858
+.shithub-stars-meta {
1859
+  list-style: none;
1860
+  margin: 0.65rem 0 0;
1861
+  padding: 0;
1862
+  display: flex;
1863
+  flex-wrap: wrap;
1864
+  align-items: center;
1865
+  gap: 0.75rem;
1866
+  color: var(--fg-muted);
1867
+  font-size: 0.875rem;
1868
+}
1869
+.shithub-stars-meta li {
1870
+  display: inline-flex;
1871
+  align-items: center;
1872
+  gap: 0.25rem;
1873
+}
1874
+.shithub-stars-starred svg:first-child {
1875
+  color: #d29922;
1876
+}
1674
 @media (max-width: 980px) {
1877
 @media (max-width: 980px) {
1675
   .shithub-profile-tabs-shell .shithub-profile-tabs {
1878
   .shithub-profile-tabs-shell .shithub-profile-tabs {
1676
     padding: 0 1rem;
1879
     padding: 0 1rem;
@@ -1754,6 +1957,19 @@ button.shithub-contrib-setting-item:hover {
1754
   .shithub-profile-activity-item::before {
1957
   .shithub-profile-activity-item::before {
1755
     display: none;
1958
     display: none;
1756
   }
1959
   }
1960
+  .shithub-stars-toolbar,
1961
+  .shithub-stars-row,
1962
+  .shithub-stars-section-head {
1963
+    align-items: stretch;
1964
+    flex-direction: column;
1965
+    display: flex;
1966
+  }
1967
+  .shithub-stars-list-actions {
1968
+    width: 100%;
1969
+  }
1970
+  .shithub-stars-list-actions .shithub-button {
1971
+    flex: 1;
1972
+  }
1757
 }
1973
 }
1758
 
1974
 
1759
 /* ----- settings shell (S10) ----- */
1975
 /* ----- settings shell (S10) ----- */
internal/web/templates/_layout.htmlmodified
@@ -331,6 +331,20 @@
331
     refreshPins();
331
     refreshPins();
332
   })();
332
   })();
333
 
333
 
334
+  (function () {
335
+    document.querySelectorAll("[data-profile-activity]").forEach(function (root) {
336
+      var button = root.querySelector("[data-profile-activity-show-more]");
337
+      if (!button) return;
338
+      button.addEventListener("click", function () {
339
+        root.querySelectorAll("[data-profile-activity-more]").forEach(function (row) {
340
+          row.hidden = false;
341
+          row.removeAttribute("data-profile-activity-more");
342
+        });
343
+        button.hidden = true;
344
+      });
345
+    });
346
+  })();
347
+
334
   (function () {
348
   (function () {
335
     var copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>';
349
     var copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>';
336
     var checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>';
350
     var checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>';
internal/web/templates/profile/stars_tab.htmlmodified
@@ -1,39 +1,136 @@
1
 {{ define "page" -}}
1
 {{ define "page" -}}
2
-<section class="shithub-profile">
2
+<section class="shithub-user-profile shithub-profile-stars-page">
3
-  <header class="shithub-profile-header">
3
+  <div class="shithub-profile-tabs-shell">
4
-    <img class="shithub-profile-avatar" src="{{ .AvatarURL }}" alt="" width="120" height="120">
4
+    {{ template "profile-tabs" . }}
5
-    <div class="shithub-profile-id">
5
+  </div>
6
-      <h1 class="shithub-profile-name">
6
+
7
-        {{ if .User.DisplayName }}{{ .User.DisplayName }}{{ else }}{{ .User.Username }}{{ end }}
7
+  <div class="shithub-user-profile-container">
8
-      </h1>
8
+    <aside class="shithub-user-profile-sidebar" aria-label="{{ .User.Username }} profile">
9
-      <p class="shithub-profile-handle">@{{ .User.Username }}</p>
9
+      <a class="shithub-profile-avatar-link" href="{{ .AvatarURL }}">
10
-    </div>
10
+        <img class="shithub-profile-avatar" src="{{ .AvatarURL }}" alt="@{{ .User.Username }}" width="296" height="296">
11
-  </header>
11
+      </a>
12
-
12
+      <div class="shithub-profile-names">
13
-  {{ template "profile-tabs" . }}
13
+        <h1>
14
-
14
+          <span class="shithub-profile-name">{{ .DisplayName }}</span>
15
-  {{ if .Stars }}
15
+          <span class="shithub-profile-handle">{{ .User.Username }}</span>
16
-  <ul class="shithub-social-list">
16
+        </h1>
17
-    {{ range .Stars }}
17
+        {{ if .User.Pronouns }}<span class="shithub-profile-pronouns">{{ .User.Pronouns }}</span>{{ end }}
18
-    <li>
18
+      </div>
19
-      <a href="/{{ .OwnerName }}/{{ .RepoName }}"><strong>{{ .OwnerName }}/{{ .RepoName }}</strong></a>
19
+
20
-      {{ if eq .Visibility "private" }}<span class="shithub-pill shithub-pill-private">private</span>{{ end }}
20
+      {{ if .User.Bio }}<div class="shithub-profile-bio">{{ .User.Bio }}</div>{{ end }}
21
-      {{ if .PrimaryLanguage }}<small>{{ .PrimaryLanguage }}</small>{{ end }}
21
+      {{ if .IsSelf }}<a href="/settings/profile" class="shithub-button shithub-button-block">Edit profile</a>{{ end }}
22
-      <small>★ {{ .StarCount }}</small>
22
+
23
-      <small><time datetime="{{ .StarredAt.Format "2006-01-02T15:04:05Z" }}">starred {{ relativeTime .StarredAt }}</time></small>
23
+      <p class="shithub-profile-follow-counts">
24
-      {{ if .Description }}<p class="shithub-meta">{{ .Description }}</p>{{ end }}
24
+        {{ octicon "people" }}
25
-    </li>
25
+        <a href="/{{ .User.Username }}?tab=followers"><strong>{{ .FollowersCount }}</strong> followers</a>
26
-    {{ end }}
26
+        <span class="shithub-profile-dot" aria-hidden="true">·</span>
27
-  </ul>
27
+        <a href="/{{ .User.Username }}?tab=following"><strong>{{ .FollowingCount }}</strong> following</a>
28
-  {{ else }}
28
+      </p>
29
-  <p class="shithub-empty">No starred repositories visible.</p>
29
+
30
-  {{ end }}
30
+      <ul class="shithub-profile-vcard">
31
-
31
+        {{ if .User.Company }}<li>{{ octicon "organization" }} <span>{{ .User.Company }}</span></li>{{ end }}
32
-  {{ if or .HasPrev .HasNext }}
32
+        {{ if .User.Location }}<li>{{ octicon "location" }} <span>{{ .User.Location }}</span></li>{{ end }}
33
-  <nav class="shithub-pagination">
33
+        {{ if .WebsiteSafe }}<li>{{ octicon "link" }} <a href="{{ .WebsiteSafe }}" rel="nofollow noopener">{{ .User.Website }}</a></li>{{ end }}
34
-    {{ if .HasPrev }}<a href="?tab=stars&page={{ sub .Page 1 }}" class="shithub-button">Previous</a>{{ end }}
34
+        <li>{{ octicon "calendar" }} <span>Joined {{ .JoinedFormatted }}</span></li>
35
-    {{ if .HasNext }}<a href="?tab=stars&page={{ add .Page 1 }}" class="shithub-button">Next</a>{{ end }}
35
+      </ul>
36
-  </nav>
36
+
37
-  {{ end }}
37
+      {{ if .Orgs }}
38
+      <section class="shithub-profile-sidebar-section" aria-labelledby="stars-profile-orgs-heading">
39
+        <h2 id="stars-profile-orgs-heading">Organizations</h2>
40
+        <div class="shithub-profile-orgs">
41
+          {{ range .Orgs }}
42
+          <a href="/{{ .Slug }}" aria-label="{{ .DisplayName }}">
43
+            <img src="{{ .AvatarURL }}" alt="" width="32" height="32">
44
+          </a>
45
+          {{ end }}
46
+        </div>
47
+      </section>
48
+      {{ end }}
49
+    </aside>
50
+
51
+    <main class="shithub-user-profile-main shithub-stars-main">
52
+      <section class="shithub-stars-lists" aria-labelledby="stars-lists-heading">
53
+        <div class="shithub-stars-section-head">
54
+          <h2 id="stars-lists-heading">Lists <span>(0)</span></h2>
55
+          <div class="shithub-stars-list-actions">
56
+            <button type="button" class="shithub-button" disabled>Sort {{ octicon "triangle-down" }}</button>
57
+            <button type="button" class="shithub-button shithub-button-primary" disabled>Create list</button>
58
+          </div>
59
+        </div>
60
+        <div class="shithub-stars-empty-list">
61
+          <span>{{ octicon "star" }}</span>
62
+          <strong>Create your first list</strong>
63
+          <p>Lists make it easier to organize and curate repositories that you have starred. <a href="/{{ .User.Username }}?tab=stars">Create your first list.</a></p>
64
+        </div>
65
+      </section>
66
+
67
+      <section class="shithub-stars-results" aria-labelledby="stars-heading">
68
+        <h2 id="stars-heading">Stars</h2>
69
+        <form class="shithub-stars-toolbar" method="get" action="/{{ .User.Username }}" aria-label="{{ .StarsSearchLabel }}">
70
+          <input type="hidden" name="tab" value="stars">
71
+          <label class="shithub-stars-search">
72
+            <span class="sr-only">Search stars</span>
73
+            {{ octicon "search" }}
74
+            <input type="search" name="q" value="{{ .StarFilters.Query }}" placeholder="Search stars" autocomplete="off">
75
+          </label>
76
+          <button type="submit" class="shithub-button">Search</button>
77
+          <label>
78
+            <span class="sr-only">Repository type</span>
79
+            <select name="type" onchange="this.form.submit()">
80
+              <option value="all"{{ if eq .StarFilters.Type "all" }} selected{{ end }}>Type: All</option>
81
+              <option value="public"{{ if eq .StarFilters.Type "public" }} selected{{ end }}>Public</option>
82
+              <option value="private"{{ if eq .StarFilters.Type "private" }} selected{{ end }}>Private</option>
83
+            </select>
84
+          </label>
85
+          <label>
86
+            <span class="sr-only">Language</span>
87
+            <select name="language" onchange="this.form.submit()">
88
+              <option value=""{{ if not .StarFilters.Language }} selected{{ end }}>Language</option>
89
+              {{ range .LanguageOptions }}<option value="{{ . }}"{{ if eq $.StarFilters.Language . }} selected{{ end }}>{{ . }}</option>{{ end }}
90
+            </select>
91
+          </label>
92
+          <label>
93
+            <span class="sr-only">Sort</span>
94
+            <select name="sort" onchange="this.form.submit()">
95
+              <option value="recently-starred"{{ if eq .StarFilters.Sort "recently-starred" }} selected{{ end }}>Sort by: Recently starred</option>
96
+              <option value="recently-active"{{ if eq .StarFilters.Sort "recently-active" }} selected{{ end }}>Recently active</option>
97
+              <option value="stars"{{ if eq .StarFilters.Sort "stars" }} selected{{ end }}>Most stars</option>
98
+            </select>
99
+          </label>
100
+        </form>
101
+
102
+        {{ if .Stars }}
103
+        <ol class="shithub-stars-list">
104
+          {{ range .Stars }}
105
+          <li class="shithub-stars-row">
106
+            <div class="shithub-stars-row-main">
107
+              <h3>
108
+                <a href="{{ .URL }}">{{ .FullName }}</a>
109
+                {{ if .IsPrivate }}<span class="shithub-pill shithub-pill-private">Private</span>{{ end }}
110
+              </h3>
111
+              {{ if .Description }}<p>{{ .Description }}</p>{{ end }}
112
+              <ul class="shithub-stars-meta">
113
+                {{ if .PrimaryLanguage }}<li><span class="shithub-language-dot" style="background-color: {{ .LanguageColor }};"></span>{{ .PrimaryLanguage }}</li>{{ end }}
114
+                <li>{{ octicon "star" }} {{ .StarCount }}</li>
115
+                <li>Updated <time datetime="{{ .UpdatedAt.Format "2006-01-02T15:04:05Z" }}">{{ relativeTime .UpdatedAt }}</time></li>
116
+              </ul>
117
+            </div>
118
+            <button type="button" class="shithub-button shithub-stars-starred" disabled>{{ octicon "star" }} Starred {{ octicon "triangle-down" }}</button>
119
+          </li>
120
+          {{ end }}
121
+        </ol>
122
+        {{ else }}
123
+        <p class="shithub-empty">No starred repositories visible.</p>
124
+        {{ end }}
125
+
126
+        {{ if or .HasPrev .HasNext }}
127
+        <nav class="shithub-pagination">
128
+          {{ if .HasPrev }}<a href="{{ .PrevHref }}" class="shithub-button">Previous</a>{{ end }}
129
+          {{ if .HasNext }}<a href="{{ .NextHref }}" class="shithub-button">Next</a>{{ end }}
130
+        </nav>
131
+        {{ end }}
132
+      </section>
133
+    </main>
134
+  </div>
38
 </section>
135
 </section>
39
 {{- end }}
136
 {{- end }}
internal/web/templates/profile/view.htmlmodified
@@ -175,20 +175,51 @@
175
         </div>
175
         </div>
176
       </section>
176
       </section>
177
 
177
 
178
-      <section class="shithub-profile-activity" aria-labelledby="activity-h">
178
+      <section class="shithub-profile-activity" aria-labelledby="activity-h" data-profile-activity>
179
         <h2 id="activity-h">Contribution activity</h2>
179
         <h2 id="activity-h">Contribution activity</h2>
180
+        {{ if .Contributions.Activity }}
181
+        <div class="shithub-profile-activity-timeline">
182
+          {{ range .Contributions.Activity }}
183
+          <div class="shithub-profile-activity-row"{{ if .InitiallyHidden }} hidden data-profile-activity-more{{ end }}>
184
+            <div class="shithub-profile-activity-month">{{ .Label }}</div>
185
+            <div class="shithub-profile-activity-list">
186
+              {{ range .Items }}
187
+              <article class="shithub-profile-activity-item shithub-profile-activity-item-{{ .Kind }}">
188
+                <span class="shithub-profile-activity-icon">{{ octicon .Icon }}</span>
189
+                <div class="shithub-profile-activity-body">
190
+                  <strong>{{ .Summary }}</strong>
191
+                  {{ if .Repos }}
192
+                  <ol class="shithub-profile-activity-repos">
193
+                    {{ range .Repos }}
194
+                    <li>
195
+                      {{ if .URL }}<a href="{{ .URL }}">{{ .FullName }}</a>{{ else }}<span>{{ .FullName }}</span>{{ end }}
196
+                      {{ if .Language }}<span class="shithub-profile-activity-lang"><span class="shithub-language-dot" style="background-color: {{ .LanguageColor }};"></span>{{ .Language }}</span>{{ end }}
197
+                      {{ if .CountLabel }}<span>{{ .CountLabel }}</span>{{ end }}
198
+                      {{ range .StateCounts }}<span class="shithub-profile-activity-state is-{{ .Class }}"><b>{{ .Count }}</b> {{ .Label }}</span>{{ end }}
199
+                      {{ if .DateLabel }}<time>{{ .DateLabel }}</time>{{ end }}
200
+                    </li>
201
+                    {{ end }}
202
+                  </ol>
203
+                  {{ end }}
204
+                </div>
205
+              </article>
206
+              {{ end }}
207
+            </div>
208
+          </div>
209
+          {{ end }}
210
+        </div>
211
+        {{ if .Contributions.HasMoreActivity }}<button type="button" class="shithub-button shithub-button-block" data-profile-activity-show-more>Show more activity</button>{{ end }}
212
+        {{ else }}
180
         <div class="shithub-profile-activity-row">
213
         <div class="shithub-profile-activity-row">
181
           <div class="shithub-profile-activity-month">{{ .Contributions.MonthLabel }}</div>
214
           <div class="shithub-profile-activity-month">{{ .Contributions.MonthLabel }}</div>
182
-          <div class="shithub-profile-activity-item">
215
+          <div class="shithub-profile-activity-list">
183
-            <span class="shithub-profile-activity-icon">{{ octicon "repo" }}</span>
216
+            <article class="shithub-profile-activity-item">
184
-            {{ if .Contributions.MonthCommitCount }}
217
+              <span class="shithub-profile-activity-icon">{{ octicon "repo" }}</span>
185
-            <strong>Created {{ .Contributions.MonthCommitCount }} commit{{ pluralize .Contributions.MonthCommitCount "" "s" }} in {{ .Contributions.MonthRepoCount }} repositor{{ pluralize .Contributions.MonthRepoCount "y" "ies" }}</strong>
218
+              <div class="shithub-profile-activity-body"><strong>No activity in {{ .Contributions.MonthLabel }}</strong></div>
186
-            {{ else }}
219
+            </article>
187
-            <strong>No activity in {{ .Contributions.MonthLabel }}</strong>
188
-            {{ end }}
189
           </div>
220
           </div>
190
         </div>
221
         </div>
191
-        <button type="button" class="shithub-button shithub-button-block" disabled>Show more activity</button>
222
+        {{ end }}
192
       </section>
223
       </section>
193
     </main>
224
     </main>
194
   </div>
225
   </div>