feat: replace load-more with page-number pagination and fix progress dot

- Replace 'Load more chapters' append pattern with discrete page-number
  pagination bar (« 1 2 3 … N ») on /books/{slug}
- Clicking a page number swaps #chapter-list via HTMX (innerHTML replace)
  and updates the OOB #chapter-pagination bar; URL is pushed via hx-push-url
- applyProgress() is called after each swap to re-highlight saved chapter
- Fix empty bullet bug: progress-dot span no longer contains the '●'
  character; replaced with a small amber circle via Tailwind (w-2 h-2
  rounded-full bg-amber-400) so hidden spans are truly space-free
- Add template FuncMap (pages/prev/next helpers) to both handleBook and
  handleBookChaptersPage; remove HasMore/NextPage from template data structs
This commit is contained in:
Admin
2026-03-01 17:22:42 +05:00
parent 1469e49190
commit e0dc4678b3

View File

@@ -593,7 +593,7 @@ const bookTmpl = `
<span class="text-zinc-300 group-hover:text-amber-400 truncate block">{{.Title}}</span>
{{if .Date}}<span class="text-xs text-zinc-500 block mt-0.5">{{.Date}}</span>{{end}}
</div>
<span class="progress-dot hidden text-amber-400 text-xs flex-shrink-0"></span>
<span class="progress-dot hidden text-amber-400 text-xs flex-shrink-0 w-2 h-2 rounded-full bg-amber-400"></span>
</a>
</li>
{{else}}
@@ -601,16 +601,36 @@ const bookTmpl = `
{{end}}
</ul>
{{if .HasMore}}
<div id="load-more-wrap" class="mt-4 text-center">
<button
hx-get="/books/{{.Slug}}/chapters-page?page={{.NextPage}}"
{{if gt .TotalPages 1}}
<div id="chapter-pagination" class="mt-6 flex justify-center gap-1 flex-wrap">
{{if gt .CurrentPage 1}}
<a hx-get="/books/{{.Slug}}/chapters-page?page={{prev .CurrentPage}}"
hx-target="#chapter-list"
hx-swap="beforeend"
hx-swap="innerHTML"
hx-push-url="/books/{{.Slug}}?page={{prev .CurrentPage}}"
hx-on::after-request="applyProgress('{{.Slug}}')"
class="px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors">
Load more chapters
</button>
class="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors cursor-pointer">«</a>
{{end}}
{{range pages .TotalPages}}
{{if eq . $.CurrentPage}}
<span class="px-3 py-1.5 rounded-lg bg-amber-700 text-white text-sm font-semibold">{{.}}</span>
{{else}}
<a hx-get="/books/{{$.Slug}}/chapters-page?page={{.}}"
hx-target="#chapter-list"
hx-swap="innerHTML"
hx-push-url="/books/{{$.Slug}}?page={{.}}"
hx-on::after-request="applyProgress('{{$.Slug}}')"
class="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors cursor-pointer">{{.}}</a>
{{end}}
{{end}}
{{if lt .CurrentPage .TotalPages}}
<a hx-get="/books/{{.Slug}}/chapters-page?page={{next .CurrentPage}}"
hx-target="#chapter-list"
hx-swap="innerHTML"
hx-push-url="/books/{{.Slug}}?page={{next .CurrentPage}}"
hx-on::after-request="applyProgress('{{.Slug}}')"
class="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors cursor-pointer">»</a>
{{end}}
</div>
{{end}}
</div>
@@ -677,29 +697,45 @@ func (s *Server) handleBook(w http.ResponseWriter, r *http.Request) {
}
total := len(chapters)
page := chapters
hasMore := false
if total > chapterPageSize {
page = chapters[:chapterPageSize]
hasMore = true
totalPages := (total + chapterPageSize - 1) / chapterPageSize
if totalPages < 1 {
totalPages = 1
}
t := template.Must(template.New("book").Parse(bookTmpl))
currentPage := 1
page := chapters
if total > chapterPageSize {
page = chapters[:chapterPageSize]
}
funcMap := template.FuncMap{
"pages": func(n int) []int {
out := make([]int, n)
for i := range out {
out[i] = i + 1
}
return out
},
"prev": func(n int) int { return n - 1 },
"next": func(n int) int { return n + 1 },
}
t := template.Must(template.New("book").Funcs(funcMap).Parse(bookTmpl))
var buf bytes.Buffer
_ = t.Execute(&buf, struct {
Slug string
Meta interface{}
Chapters interface{}
TotalDownloaded int
HasMore bool
NextPage int
TotalPages int
CurrentPage int
}{
Slug: slug,
Meta: meta,
Chapters: page,
TotalDownloaded: total,
HasMore: hasMore,
NextPage: 2,
TotalPages: totalPages,
CurrentPage: currentPage,
})
s.respond(w, r, meta.Title, buf.String())
@@ -721,33 +757,47 @@ const chapterPageTmpl = `{{range .Chapters}}
<span class="text-zinc-300 group-hover:text-amber-400 truncate block">{{.Title}}</span>
{{if .Date}}<span class="text-xs text-zinc-500 block mt-0.5">{{.Date}}</span>{{end}}
</div>
<span class="progress-dot hidden text-amber-400 text-xs flex-shrink-0"></span>
<span class="progress-dot hidden text-amber-400 text-xs flex-shrink-0 w-2 h-2 rounded-full bg-amber-400"></span>
</a>
</li>
{{end}}
{{if .HasMore}}
<li id="load-more-wrap" hx-swap-oob="true">
<div class="mt-4 text-center">
<button
hx-get="/books/{{.Slug}}/chapters-page?page={{.NextPage}}"
<div id="chapter-pagination" hx-swap-oob="true" class="mt-6 flex justify-center gap-1 flex-wrap">
{{if gt .CurrentPage 1}}
<a hx-get="/books/{{.Slug}}/chapters-page?page={{prev .CurrentPage}}"
hx-target="#chapter-list"
hx-swap="beforeend"
hx-swap="innerHTML"
hx-push-url="/books/{{.Slug}}?page={{prev .CurrentPage}}"
hx-on::after-request="applyProgress('{{.Slug}}')"
class="px-4 py-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors">
Load more chapters
</button>
</div>
</li>
class="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors cursor-pointer">«</a>
{{end}}
{{range pages .TotalPages}}
{{if eq . $.CurrentPage}}
<span class="px-3 py-1.5 rounded-lg bg-amber-700 text-white text-sm font-semibold">{{.}}</span>
{{else}}
<li id="load-more-wrap" hx-swap-oob="true"></li>
{{end}}`
<a hx-get="/books/{{$.Slug}}/chapters-page?page={{.}}"
hx-target="#chapter-list"
hx-swap="innerHTML"
hx-push-url="/books/{{$.Slug}}?page={{.}}"
hx-on::after-request="applyProgress('{{$.Slug}}')"
class="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors cursor-pointer">{{.}}</a>
{{end}}
{{end}}
{{if lt .CurrentPage .TotalPages}}
<a hx-get="/books/{{.Slug}}/chapters-page?page={{next .CurrentPage}}"
hx-target="#chapter-list"
hx-swap="innerHTML"
hx-push-url="/books/{{.Slug}}?page={{next .CurrentPage}}"
hx-on::after-request="applyProgress('{{.Slug}}')"
class="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-sm transition-colors cursor-pointer">»</a>
{{end}}
</div>`
func (s *Server) handleBookChaptersPage(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
page := 1
currentPage := 1
if p := r.URL.Query().Get("page"); p != "" {
if n, err := strconv.Atoi(p); err == nil && n > 0 {
page = n
currentPage = n
}
}
@@ -757,29 +807,46 @@ func (s *Server) handleBookChaptersPage(w http.ResponseWriter, r *http.Request)
return
}
start := (page - 1) * chapterPageSize
if start >= len(chapters) {
total := len(chapters)
totalPages := (total + chapterPageSize - 1) / chapterPageSize
if totalPages < 1 {
totalPages = 1
}
start := (currentPage - 1) * chapterPageSize
if start >= total {
w.WriteHeader(http.StatusNoContent)
return
}
end := start + chapterPageSize
hasMore := end < len(chapters)
if end > len(chapters) {
end = len(chapters)
if end > total {
end = total
}
t := template.Must(template.New("chapterPage").Parse(chapterPageTmpl))
funcMap := template.FuncMap{
"pages": func(n int) []int {
out := make([]int, n)
for i := range out {
out[i] = i + 1
}
return out
},
"prev": func(n int) int { return n - 1 },
"next": func(n int) int { return n + 1 },
}
t := template.Must(template.New("chapterPage").Funcs(funcMap).Parse(chapterPageTmpl))
var buf bytes.Buffer
_ = t.Execute(&buf, struct {
Slug string
Chapters interface{}
HasMore bool
NextPage int
TotalPages int
CurrentPage int
}{
Slug: slug,
Chapters: chapters[start:end],
HasMore: hasMore,
NextPage: page + 1,
TotalPages: totalPages,
CurrentPage: currentPage,
})
w.Header().Set("Content-Type", "text/html; charset=utf-8")