add pagination

This commit is contained in:
2025-01-11 05:43:13 +01:00
parent 6deacf478f
commit f083496ebc
7 changed files with 101 additions and 10 deletions

View File

@@ -1,8 +1,8 @@
{{ define "content" }} {{ define "content" }}
<div class="content max-w-screen-lg mx-auto"> <div class="content max-w-screen-lg flex flex-col mx-auto">
{{ range . }} {{ range .ArticleVMs }}
<div tabindex="0" class="collapse bg-base-200 shadow mb-4"> <div tabindex="0" class="collapse bg-base-200 shadow mb-4">
<div class="collapse-title font-medium">{{ .Title }}</div> <div class="collapse-title font-medium">{{ .Title }}</div>
<div class="collapse-content"> <div class="collapse-content">
@@ -19,5 +19,7 @@
</div> </div>
{{ end }} {{ end }}
{{ template "pagination" .Paginations }}
</div> </div>
{{ end }} {{ end }}

View File

@@ -0,0 +1,10 @@
{{ define "pagination" }}
<div class="join pagination p-5 mx-auto">
{{ range . }}
<a class="join-item btn btn-sm {{ if .Active }}btn-active{{ end }} {{ if .Disabled }}btn-disabled{{end}}" up-follow up-target=".content"
{{ if .Disabled }}{{else}}href="/page/{{ .Content }}" tabindex="0"{{end}}>{{ .Content }}</a>
{{ end }}
</div>
{{ end }}

View File

@@ -11,9 +11,9 @@
{{/* Logo with navigation */}} {{/* Logo with navigation */}}
<div class="flex-1"> <div class="flex-1">
<a href="/" tabindex="0" class="btn btn-ghost text-xl">crowsnest</a> <a href="/" tabindex="0" class="btn btn-ghost text-xl" up-follow up-target=".body">crowsnest</a>
<ul class="menu menu-horizontal hidden sm:flex"> <ul class="menu menu-horizontal hidden sm:flex">
<li><a tabindex="0" class="active">Artikel</a></li> <li><a tabindex="0" {{ if .SelectedNavItemArticle }} class="active" {{ end }}>Artikel</a></li>
<li><a tabindex="0">Themen</a></li> <li><a tabindex="0">Themen</a></li>
</ul> </ul>
</div> </div>

View File

@@ -4,17 +4,36 @@ import (
"crowsnest/internal/model" "crowsnest/internal/model"
"html/template" "html/template"
"net/http" "net/http"
"strconv"
) )
// List the latest articles using the base template. // List the latest articles using the base template.
func (app *App) Index(w http.ResponseWriter, req *http.Request) { func (app *App) Index(w http.ResponseWriter, req *http.Request) {
const pageSize = 15
var limit, offset, pageId uint64 = pageSize, 0, 0
var err error
// get page number
if pageId, err = strconv.ParseUint(req.PathValue("id"), 10, 32); err == nil {
pageId--
offset = pageId * pageSize
}
// get articles // get articles
articles, err := app.articles.All(30) articles, err := app.articles.All(int(limit), int(offset))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// get count of total articles
totalCount, err := app.articles.CountAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
totalCount /= pageSize
// convert to viewmodel // convert to viewmodel
articleVMs := make([]*model.ArticleViewModel, 0, len(articles)) articleVMs := make([]*model.ArticleViewModel, 0, len(articles))
for _, a := range articles { for _, a := range articles {
@@ -22,8 +41,17 @@ func (app *App) Index(w http.ResponseWriter, req *http.Request) {
} }
// render template // render template
t := template.Must(template.ParseFiles("assets/templates/article.html", "assets/templates/layout.html")) t := template.Must(template.ParseFiles(
err = t.ExecuteTemplate(w, "base", articleVMs) "assets/templates/article.html",
"assets/templates/layout.html",
"assets/templates/components/pagination.html"))
data := map[string]interface{}{
"SelectedNavItemArticle": true,
"ArticleVMs": &articleVMs,
"Paginations": model.NewPaginationViewModel(uint(pageId+1), totalCount+1),
}
err = t.ExecuteTemplate(w, "base", data)
if err != nil { if err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError) http.Error(w, "Failed to render template", http.StatusInternalServerError)
return return

View File

@@ -23,6 +23,7 @@ func (app *App) routes() http.Handler {
// dynamic routes // dynamic routes
mux.Handle("GET /", LoggingMiddleware(http.HandlerFunc(app.Index))) mux.Handle("GET /", LoggingMiddleware(http.HandlerFunc(app.Index)))
mux.Handle("GET /page/{id}", LoggingMiddleware(http.HandlerFunc(app.Index)))
mux.Handle("POST /up/search", LoggingMiddleware(http.HandlerFunc(app.UpSearch))) mux.Handle("POST /up/search", LoggingMiddleware(http.HandlerFunc(app.UpSearch)))
// serve files from the "static" directory // serve files from the "static" directory

View File

@@ -11,14 +11,14 @@ type ArticleModel struct {
// Gets all the article objects from the database. This may throw an error if // Gets all the article objects from the database. This may throw an error if
// the connection to the database fails. // the connection to the database fails.
func (m *ArticleModel) All(limit int) ([]model.Article, error) { func (m *ArticleModel) All(limit int, offset int) ([]model.Article, error) {
stmt := ` stmt := `
SELECT id, title, sourceUrl, content, publishDate, fetchDate, aisummary SELECT id, title, sourceUrl, content, publishDate, fetchDate, aisummary
FROM articles FROM articles
ORDER BY publishDate DESC ORDER BY publishDate DESC
LIMIT $1 LIMIT $1 OFFSET $2
` `
rows, err := m.DB.Query(stmt, limit) rows, err := m.DB.Query(stmt, limit, offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -41,6 +41,21 @@ func (m *ArticleModel) All(limit int) ([]model.Article, error) {
return articles, nil return articles, nil
} }
// Counts all articles in the database. This may throw an error if the
// connection to the database fails.
func (m *ArticleModel) CountAll() (uint, error) {
stmt := `SELECT count(id) FROM articles `
rows := m.DB.QueryRow(stmt)
count := uint(0)
if err := rows.Scan(&count); err != nil {
return 0, err
}
return count, nil
}
// Will use the full-text search features of the underlying database to search // Will use the full-text search features of the underlying database to search
// articles for a given search query. This may fail if the connection to the // articles for a given search query. This may fail if the connection to the
// database fails. // database fails.

View File

@@ -0,0 +1,35 @@
package model
import (
"strconv"
)
type PageButton struct {
Content string
Active bool
Disabled bool
}
type PaginationViewModel []PageButton
func NewPaginationViewModel(currentPage uint, totalPages uint) *PaginationViewModel {
pagVM := make(PaginationViewModel, 0)
if totalPages > 1 {
pagVM = append(pagVM, PageButton{"1", currentPage == 1, false})
}
if currentPage > 3 {
pagVM = append(pagVM, PageButton{"...", false, true})
}
for i := max(2, currentPage-1); i <= min(totalPages-1, currentPage+1); i++ {
pagVM = append(pagVM, PageButton{strconv.Itoa(int(i)), i == currentPage, false})
}
if currentPage < totalPages-2 {
pagVM = append(pagVM, PageButton{"...", false, true})
}
if totalPages > 1 {
pagVM = append(pagVM, PageButton{strconv.Itoa(int(totalPages)), totalPages == currentPage, false})
}
return &pagVM
}