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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user