add pagination
This commit is contained in:
@@ -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 }}
|
||||||
|
|||||||
10
src/assets/templates/components/pagination.html
Normal file
10
src/assets/templates/components/pagination.html
Normal 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 }}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
35
src/internal/model/pagination.go
Normal file
35
src/internal/model/pagination.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user