feat(web): archived and finished tabs with per-card status controls
This commit is contained in:
@@ -131,10 +131,19 @@ button, .primary, [role="tab"] {
|
||||
|
||||
.search:focus-visible { outline: 2px solid var(--accent); }
|
||||
|
||||
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||
/* Five buttons no longer divide a phone's width legibly, so the row scrolls
|
||||
sideways instead of squeezing. The scrollbar is hidden: on mobile there is
|
||||
none anyway, and on desktop it would sit under the tab labels. */
|
||||
.tabs {
|
||||
display: flex; gap: 8px; margin-bottom: 16px;
|
||||
overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.tabs [role="tab"] {
|
||||
flex: 1;
|
||||
flex: 1 0 auto;
|
||||
padding-inline: 14px;
|
||||
white-space: nowrap;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--surface);
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
<a role="tab" href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
|
||||
hx-get="/ui/list?tab=fav" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=fav" hx-on::after-request="setActiveTab(this)">Favourites</a>
|
||||
<a role="tab" href="/?tab=archived" class="{{if eq .Tab "archived"}}active{{end}}"
|
||||
hx-get="/ui/list?tab=archived" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=archived" hx-on::after-request="setActiveTab(this)">Archived</a>
|
||||
<a role="tab" href="/?tab=finished" class="{{if eq .Tab "finished"}}active{{end}}"
|
||||
hx-get="/ui/list?tab=finished" hx-target="#list" hx-swap="innerHTML"
|
||||
hx-push-url="/?tab=finished" hx-on::after-request="setActiveTab(this)">Finished</a>
|
||||
</nav>
|
||||
|
||||
{{if .Recent}}
|
||||
|
||||
@@ -38,6 +38,48 @@
|
||||
<path d="M13.8 7.2l3 3"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{if eq .Status "finished"}}
|
||||
<button class="icon on" title="Restore to reading" aria-label="Restore to reading"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"reading"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4 12a8 8 0 1 1 2.6 5.9M4 12V7M4 12h5"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{else}}
|
||||
{{if eq .Status "archived"}}
|
||||
<button class="icon on" title="Restore to reading" aria-label="Restore to reading"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"reading"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4 12a8 8 0 1 1 2.6 5.9M4 12V7M4 12h5"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{else}}
|
||||
<button class="icon" title="Archive" aria-label="Archive"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"archived"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M3.5 6.4h17v3.2h-17zM5 9.6h14V19H5zM10 13h4"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{end}}
|
||||
<button class="icon" title="Mark finished" aria-label="Mark finished"
|
||||
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"finished"}'
|
||||
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
|
||||
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
||||
<path d="M4.5 12.5l5 5 10-11"/>
|
||||
</svg>
|
||||
</button>
|
||||
{{end}}
|
||||
<button class="icon danger" title="Remove" aria-label="Remove"
|
||||
onclick="toggleConfirmRow('{{.Key}}')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
<p class="empty">No favourites yet. Star a series to pin it here.</p>
|
||||
{{else if eq .Tab "new"}}
|
||||
<p class="empty">Nothing new. Every series is caught up to its latest chapter.</p>
|
||||
{{else if eq .Tab "archived"}}
|
||||
<p class="empty">Nothing archived. Shelve a series to park it here — it keeps getting checked for new chapters.</p>
|
||||
{{else if eq .Tab "finished"}}
|
||||
<p class="empty">Nothing finished yet.</p>
|
||||
{{else}}
|
||||
<p class="empty">
|
||||
Nothing here yet. Bookmarks appear once the userscript records a chapter.
|
||||
|
||||
+57
-2
@@ -308,8 +308,8 @@ func TestCardHxTargetIsValidSelectorForColonKey(t *testing.T) {
|
||||
body := rr.Body.String()
|
||||
|
||||
want := `hx-target="[id='card-asura:solo']"`
|
||||
if strings.Count(body, want) != 3 {
|
||||
t.Fatalf("body has %d occurrences of %s, want 3 (favorite button, delete button, chapter form)",
|
||||
if strings.Count(body, want) != 5 {
|
||||
t.Fatalf("body has %d occurrences of %s, want 5 (favorite, archive, finish, delete buttons, chapter form)",
|
||||
strings.Count(body, want), want)
|
||||
}
|
||||
if strings.Contains(body, `hx-target="#card-asura:solo"`) {
|
||||
@@ -675,3 +675,58 @@ func TestUIStatusDoesNotReorderList(t *testing.T) {
|
||||
t.Fatalf("UpdatedAt moved %d -> %d", before.UpdatedAt, after.UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardShowsStatusControls(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seedStatusRows(t, store)
|
||||
|
||||
cases := []struct {
|
||||
tab string
|
||||
want, dontWant []string
|
||||
}{
|
||||
// A series being read can be shelved or completed, not restored.
|
||||
{"all", []string{`hx-vals='{"status":"archived"}'`, `hx-vals='{"status":"finished"}'`}, nil},
|
||||
// An archived one can come back or be completed.
|
||||
{"archived", []string{`hx-vals='{"status":"reading"}'`, `hx-vals='{"status":"finished"}'`}, nil},
|
||||
// A finished one can only come back.
|
||||
{"finished", []string{`hx-vals='{"status":"reading"}'`}, []string{`hx-vals='{"status":"finished"}'`}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.tab, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/ui/list?tab="+tc.tab, nil)
|
||||
req.AddCookie(sessionCookie(t, cfg))
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
|
||||
body := rr.Body.String()
|
||||
for _, w := range tc.want {
|
||||
if !strings.Contains(body, w) {
|
||||
t.Fatalf("tab %s missing control %s", tc.tab, w)
|
||||
}
|
||||
}
|
||||
for _, d := range tc.dontWant {
|
||||
if strings.Contains(body, d) {
|
||||
t.Fatalf("tab %s offered %s", tc.tab, d)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRendersNewTabs(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seedStatusRows(t, store)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(sessionCookie(t, cfg))
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
|
||||
for _, want := range []string{`href="/?tab=archived"`, `href="/?tab=finished"`} {
|
||||
if !strings.Contains(rr.Body.String(), want) {
|
||||
t.Fatalf("app page missing %s", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user