Archived and finished buckets, userscript nav chips #4
@@ -131,10 +131,19 @@ button, .primary, [role="tab"] {
|
|||||||
|
|
||||||
.search:focus-visible { outline: 2px solid var(--accent); }
|
.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"] {
|
.tabs [role="tab"] {
|
||||||
flex: 1;
|
flex: 1 0 auto;
|
||||||
|
padding-inline: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
<a role="tab" href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
|
<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-get="/ui/list?tab=fav" hx-target="#list" hx-swap="innerHTML"
|
||||||
hx-push-url="/?tab=fav" hx-on::after-request="setActiveTab(this)">Favourites</a>
|
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>
|
</nav>
|
||||||
|
|
||||||
{{if .Recent}}
|
{{if .Recent}}
|
||||||
|
|||||||
@@ -38,6 +38,48 @@
|
|||||||
<path d="M13.8 7.2l3 3"/>
|
<path d="M13.8 7.2l3 3"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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"
|
<button class="icon danger" title="Remove" aria-label="Remove"
|
||||||
onclick="toggleConfirmRow('{{.Key}}')">
|
onclick="toggleConfirmRow('{{.Key}}')">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
<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>
|
<p class="empty">No favourites yet. Star a series to pin it here.</p>
|
||||||
{{else if eq .Tab "new"}}
|
{{else if eq .Tab "new"}}
|
||||||
<p class="empty">Nothing new. Every series is caught up to its latest chapter.</p>
|
<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}}
|
{{else}}
|
||||||
<p class="empty">
|
<p class="empty">
|
||||||
Nothing here yet. Bookmarks appear once the userscript records a chapter.
|
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()
|
body := rr.Body.String()
|
||||||
|
|
||||||
want := `hx-target="[id='card-asura:solo']"`
|
want := `hx-target="[id='card-asura:solo']"`
|
||||||
if strings.Count(body, want) != 3 {
|
if strings.Count(body, want) != 5 {
|
||||||
t.Fatalf("body has %d occurrences of %s, want 3 (favorite button, delete button, chapter form)",
|
t.Fatalf("body has %d occurrences of %s, want 5 (favorite, archive, finish, delete buttons, chapter form)",
|
||||||
strings.Count(body, want), want)
|
strings.Count(body, want), want)
|
||||||
}
|
}
|
||||||
if strings.Contains(body, `hx-target="#card-asura:solo"`) {
|
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)
|
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