diff --git a/src/assets/templates/article.html b/src/assets/templates/article.html index c119940..99f81d6 100644 --- a/src/assets/templates/article.html +++ b/src/assets/templates/article.html @@ -1,8 +1,8 @@ {{ define "content" }} -
+
-{{ range . }} +{{ range .ArticleVMs }}
{{ .Title }}
@@ -19,5 +19,7 @@
{{ end }} +{{ template "pagination" .Paginations }} +
{{ end }} diff --git a/src/assets/templates/components/pagination.html b/src/assets/templates/components/pagination.html new file mode 100644 index 0000000..54dea5a --- /dev/null +++ b/src/assets/templates/components/pagination.html @@ -0,0 +1,10 @@ +{{ define "pagination" }} + + + +{{ end }} \ No newline at end of file diff --git a/src/assets/templates/layout.html b/src/assets/templates/layout.html index 537037b..44f70b6 100644 --- a/src/assets/templates/layout.html +++ b/src/assets/templates/layout.html @@ -11,9 +11,9 @@ {{/* Logo with navigation */}} diff --git a/src/cmd/frontend/Index.go b/src/cmd/frontend/Index.go index 633f6b5..2a79ae6 100644 --- a/src/cmd/frontend/Index.go +++ b/src/cmd/frontend/Index.go @@ -4,17 +4,36 @@ import ( "crowsnest/internal/model" "html/template" "net/http" + "strconv" ) // List the latest articles using the base template. 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 - articles, err := app.articles.All(30) + articles, err := app.articles.All(int(limit), int(offset)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) 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 articleVMs := make([]*model.ArticleViewModel, 0, len(articles)) for _, a := range articles { @@ -22,8 +41,17 @@ func (app *App) Index(w http.ResponseWriter, req *http.Request) { } // render template - t := template.Must(template.ParseFiles("assets/templates/article.html", "assets/templates/layout.html")) - err = t.ExecuteTemplate(w, "base", articleVMs) + t := template.Must(template.ParseFiles( + "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 { http.Error(w, "Failed to render template", http.StatusInternalServerError) return diff --git a/src/cmd/frontend/routes.go b/src/cmd/frontend/routes.go index 59ec3a7..ef0c054 100644 --- a/src/cmd/frontend/routes.go +++ b/src/cmd/frontend/routes.go @@ -23,6 +23,7 @@ func (app *App) routes() http.Handler { // dynamic routes 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))) // serve files from the "static" directory diff --git a/src/internal/model/database/articles.go b/src/internal/model/database/articles.go index 34d78d1..8ec1ee6 100644 --- a/src/internal/model/database/articles.go +++ b/src/internal/model/database/articles.go @@ -11,14 +11,14 @@ type ArticleModel struct { // Gets all the article objects from the database. This may throw an error if // 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 := ` SELECT id, title, sourceUrl, content, publishDate, fetchDate, aisummary FROM articles 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 { return nil, err } @@ -41,6 +41,21 @@ func (m *ArticleModel) All(limit int) ([]model.Article, error) { 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 // articles for a given search query. This may fail if the connection to the // database fails. diff --git a/src/internal/model/pagination.go b/src/internal/model/pagination.go new file mode 100644 index 0000000..ead099c --- /dev/null +++ b/src/internal/model/pagination.go @@ -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 +}