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