move to gomponents for rendering html
This commit is contained in:
@@ -1,24 +0,0 @@
|
|||||||
{{ define "content" }}
|
|
||||||
|
|
||||||
<div class="content max-w-screen-lg flex flex-col mx-auto">
|
|
||||||
|
|
||||||
{{ range .ArticleVMs }}
|
|
||||||
<div tabindex="0" class="collapse bg-base-200 shadow mb-4">
|
|
||||||
<div class="collapse-title font-medium">{{ .Title }}</div>
|
|
||||||
<div class="collapse-content">
|
|
||||||
<p class="pb-2">
|
|
||||||
<span class="badge badge-outline">{{ .ShortSource }}</span>
|
|
||||||
<span class="badge badge-outline">{{ .PublishDate }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="card-text">{{ .Summary }}</p>
|
|
||||||
<div class="flex flex-row-reverse">
|
|
||||||
<a href="/article/{{ .Id }}" class="btn btn-active btn-sm btn-primary">Details</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ template "pagination" .Paginations }}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{{ define "content" }}
|
|
||||||
|
|
||||||
<div class="content max-w-screen-lg flex flex-col mx-auto">
|
|
||||||
|
|
||||||
<div tabindex="0" class="card bg-base-200 shadow mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex flex-row pb-4">
|
|
||||||
<div class="divider divider-horizontal divider-primary"></div>
|
|
||||||
<div class="card-title font-medium">{{ .ArticlePageVM.Title }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-5 pb-4 grid gap-y-4 grid-cols-1">
|
|
||||||
<p><span class="badge badge-neutral me-4 w-20">Datum</span>{{ .ArticlePageVM.PublishDate }}</p>
|
|
||||||
<p><span class="badge badge-neutral me-4 w-20">Quelle</span>{{ .ArticlePageVM.ShortSource }}</p>
|
|
||||||
<p><span class="badge badge-neutral me-4 w-20">TLDR</span>{{ .ArticlePageVM.Summary }}</p>
|
|
||||||
<p><span class="badge badge-neutral me-4 w-20">Inhalt</span>{{ .ArticlePageVM.Content }}</p>
|
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<a href="{{ .ArticlePageVM.SourceUrl }}">
|
|
||||||
<button class="btn btn-primary btn-sm">Seite besuchen</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{{ 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 }}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
{{ define "base" }}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
|
|
||||||
{{/* Unpoly */}}
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/unpoly.min.css" />
|
|
||||||
<script src="/static/unpoly.min.js"></script>
|
|
||||||
|
|
||||||
{{/* DasyUi */}}
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/daisyui.min.css" />
|
|
||||||
<script src="/static/tailwindcss.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="fixed top-0 z-50 w-full p-4">
|
|
||||||
<div class="navbar bg-base-300 rounded-box drop-shadow-md">
|
|
||||||
|
|
||||||
{{/* Logo with navigation */}}
|
|
||||||
<div class="flex-1">
|
|
||||||
<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">
|
|
||||||
<li><a href="/" tabindex="0" {{ if .SelectedNavItemArticle }} class="active" {{ end }}>Artikel</a></li>
|
|
||||||
<li><a tabindex="0">Themen</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/* Search field for normal sized screen */}}
|
|
||||||
<div class="hidden sm:flex flex-none pe-4">
|
|
||||||
<form role="search" method="post" action="/up/search" up-submit up-autosubmit up-target=".content">
|
|
||||||
<label class="input input-bordered input-sm flex items-center gap-2">
|
|
||||||
<input name="search" type="search" class="grow" placeholder="Suche" />
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"
|
|
||||||
class="h-4 w-4 opacity-70">
|
|
||||||
<path fill-rule="evenodd" d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/* Dropdown for small screens */}}
|
|
||||||
<div class="dropdown dropdown-end sm:hidden">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
|
||||||
class="h-6 w-6 opacity-70">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M12 17.25h8.25" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
|
|
||||||
<li><a href="/" tabindex="0" class="active">Artikel</a></li>
|
|
||||||
<li><a tabindex="0">Themen</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container mx-auto px-4 mt-28">
|
|
||||||
{{ template "content" . }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script>
|
|
||||||
up.link.config.followSelectors.push('a[href]')
|
|
||||||
up.link.config.instantSelectors.push('a[href]')
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
||||||
@@ -79,4 +79,6 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gorgonia.org/vecf32 v0.9.0 // indirect
|
gorgonia.org/vecf32 v0.9.0 // indirect
|
||||||
gorgonia.org/vecf64 v0.9.0 // indirect
|
gorgonia.org/vecf64 v0.9.0 // indirect
|
||||||
|
maragu.dev/gomponents v1.1.0 // indirect
|
||||||
|
maragu.dev/gomponents-heroicons/v3 v3.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -447,5 +447,9 @@ gorgonia.org/vecf64 v0.9.0 h1:bgZDP5x0OzBF64PjMGC3EvTdOoMEcmfAh1VCUnZFm1A=
|
|||||||
gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA=
|
gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U=
|
||||||
|
maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||||
|
maragu.dev/gomponents-heroicons/v3 v3.0.0 h1:QBw4CSST12mrdcYzl1XrEnbMxfhvQgnVunhFgQ4RPyI=
|
||||||
|
maragu.dev/gomponents-heroicons/v3 v3.0.0/go.mod h1:Rqc5BhSQUHBnGuWEPihg+IsQnOkiBY+Ibu1DDGEarsY=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"crowsnest/internal/html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@@ -24,17 +24,8 @@ func (app *App) Article(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// render template
|
// render page
|
||||||
t := template.Must(template.ParseFiles(
|
err = html.ArticleLayout(articlePageVM).Render(w)
|
||||||
"assets/templates/articlePage.html",
|
|
||||||
"assets/templates/layout.html",
|
|
||||||
))
|
|
||||||
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"SelectedNavItemArticle": false,
|
|
||||||
"ArticlePageVM": articlePageVM,
|
|
||||||
}
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crowsnest/internal/model"
|
"crowsnest/internal/html"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@@ -10,7 +9,7 @@ import (
|
|||||||
// 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
|
const pageSize = 15
|
||||||
var limit, offset, pageId uint64 = pageSize, 0, 0
|
var offset, pageId uint64 = 0, 0
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// get page number
|
// get page number
|
||||||
@@ -20,7 +19,7 @@ func (app *App) Index(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get articles
|
// get articles
|
||||||
articleVMs, err := app.articleVMs.All(int(limit), int(offset))
|
articleVMs, err := app.articleVMs.All(pageSize, int(offset))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -34,18 +33,8 @@ func (app *App) Index(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
totalCount /= pageSize
|
totalCount /= pageSize
|
||||||
|
|
||||||
// render template
|
// render page
|
||||||
t := template.Must(template.ParseFiles(
|
err = html.IndexLayout(articleVMs, uint(pageId+1), totalCount+1).Render(w)
|
||||||
"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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"crowsnest/internal/html"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,18 +23,8 @@ func (app *App) UpSearch(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// render template
|
// render page
|
||||||
t := template.Must(template.ParseFiles(
|
err = html.IndexLayout(articleVMs, 0, 0).Render(w)
|
||||||
"assets/templates/article.html",
|
|
||||||
"assets/templates/layout.html",
|
|
||||||
"assets/templates/components/pagination.html"))
|
|
||||||
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"SelectedNavItemArticle": true,
|
|
||||||
"ArticleVMs": &articleVMs,
|
|
||||||
"Paginations": nil,
|
|
||||||
}
|
|
||||||
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
|
||||||
|
|||||||
39
src/internal/html/article.go
Normal file
39
src/internal/html/article.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crowsnest/internal/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
h "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Articles(articles []*model.ArticleViewModel) g.Node {
|
||||||
|
return h.Div(h.Class("content max-w-screen-lg flex flex-col mx-auto"), g.Map(articles, Article))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Article(article *model.ArticleViewModel) g.Node {
|
||||||
|
return h.Div(
|
||||||
|
h.Class("collapse bg-base-200 shadow mb-4"),
|
||||||
|
h.TabIndex("0"),
|
||||||
|
h.Div(h.Class("collapse-title font-medium"), g.Text(article.Title)),
|
||||||
|
h.Div(
|
||||||
|
h.Class("collapse-content"),
|
||||||
|
h.P(
|
||||||
|
h.Class("pb-2"),
|
||||||
|
h.Span(h.Class("badge badge-outline"), g.Text(article.ShortSource)),
|
||||||
|
h.Span(h.Class("badge badge-outline"), g.Text(article.PublishDate)),
|
||||||
|
),
|
||||||
|
h.P(h.Class("card-text"), g.Text(article.Summary)),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex flex-row-reverse"),
|
||||||
|
h.A(
|
||||||
|
h.Href("/article/"+strconv.Itoa(article.Id)),
|
||||||
|
h.Class("btn btn-active btn-sm btn-primary"),
|
||||||
|
g.Attr("up-follow"),
|
||||||
|
g.Text("Details"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
14
src/internal/html/articleLayout.go
Normal file
14
src/internal/html/articleLayout.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crowsnest/internal/model"
|
||||||
|
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ArticleLayout(articlePageVMs *model.ArticlePageViewModel) g.Node {
|
||||||
|
return Layout(
|
||||||
|
"Crowsnest - "+articlePageVMs.Title,
|
||||||
|
ArticlePage(articlePageVMs),
|
||||||
|
)
|
||||||
|
}
|
||||||
55
src/internal/html/articlePage.go
Normal file
55
src/internal/html/articlePage.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crowsnest/internal/model"
|
||||||
|
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
h "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ArticlePageColumn(key string, value string) g.Node {
|
||||||
|
return h.P(
|
||||||
|
h.Span(
|
||||||
|
h.Class("badge badge-neutral me-4 w-20"),
|
||||||
|
g.Text(key),
|
||||||
|
),
|
||||||
|
g.Text(value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ArticlePage(articlePageVM *model.ArticlePageViewModel) g.Node {
|
||||||
|
return h.Div(
|
||||||
|
h.TabIndex("0"),
|
||||||
|
h.Class("card bg-base-200 shadow mb-4"),
|
||||||
|
h.Div(
|
||||||
|
h.Class("card-body"),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex flex-row pb-4"),
|
||||||
|
h.Div(
|
||||||
|
h.Class("divider divider-horizontal divider-primary"),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("card-title font-medium"),
|
||||||
|
g.Text(articlePageVM.Title),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("px-5 pb-4 grid gap-y-4 grid-cols-1"),
|
||||||
|
ArticlePageColumn("Datum", articlePageVM.PublishDate),
|
||||||
|
ArticlePageColumn("Quelle", articlePageVM.ShortSource),
|
||||||
|
ArticlePageColumn("TLDR", articlePageVM.Summary),
|
||||||
|
ArticlePageColumn("Inhalt", articlePageVM.Content),
|
||||||
|
h.Div(
|
||||||
|
h.Class("card-actions justify-end"),
|
||||||
|
h.A(
|
||||||
|
h.Href(articlePageVM.SourceUrl),
|
||||||
|
h.Button(
|
||||||
|
h.Class("btn btn-primary btn-sm"),
|
||||||
|
g.Text("Seite besuchen"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
17
src/internal/html/indexLayout.go
Normal file
17
src/internal/html/indexLayout.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crowsnest/internal/model"
|
||||||
|
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IndexLayout(articleVMs []*model.ArticleViewModel, paginationCurrent uint, paginationTotal uint) g.Node {
|
||||||
|
return Layout(
|
||||||
|
"Crowsnest - Artikel",
|
||||||
|
g.Group{
|
||||||
|
Articles(articleVMs),
|
||||||
|
Pagination(paginationCurrent, paginationTotal),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/internal/html/layout.go
Normal file
43
src/internal/html/layout.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
c "maragu.dev/gomponents/components"
|
||||||
|
h "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Layout(title string, body g.Node) g.Node {
|
||||||
|
completeBody := []g.Node{
|
||||||
|
Navbar(),
|
||||||
|
h.Div(
|
||||||
|
h.Class("content container flex flex-col mx-auto px-4 mt-28"),
|
||||||
|
body,
|
||||||
|
),
|
||||||
|
h.Script(h.Src("https://cdn.tailwindcss.com")),
|
||||||
|
h.Script(
|
||||||
|
g.Text(
|
||||||
|
"up.link.config.followSelectors.push('a[href]')\n" +
|
||||||
|
"up.link.config.instantSelectors.push('a[href]')",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.HTML5(c.HTML5Props{
|
||||||
|
Title: title,
|
||||||
|
Language: "de",
|
||||||
|
Head: layoutHead(),
|
||||||
|
Body: completeBody,
|
||||||
|
HTMLAttrs: []g.Node{g.Attr("data-theme", "dark")},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func layoutHead() []g.Node {
|
||||||
|
return []g.Node{
|
||||||
|
h.Meta(h.Name("viewport"), h.Content("width=device-width, initial-scale=1")),
|
||||||
|
h.Link(h.Rel("stylesheet"), h.Type("text/css"), h.Href("/static/unpoly.min.css")),
|
||||||
|
h.Script(h.Src("/static/unpoly.min.js")),
|
||||||
|
h.Link(h.Rel("stylesheet"), h.Type("text/css"), h.Href("/static/daisyui.min.css")),
|
||||||
|
h.Script(h.Src("/static/tailwindcss.min.js")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
76
src/internal/html/navbar.go
Normal file
76
src/internal/html/navbar.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
"maragu.dev/gomponents-heroicons/v3/outline"
|
||||||
|
h "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NavbarLi(href string, text string, active bool) g.Node {
|
||||||
|
return h.Li(h.A(h.Href(href), h.TabIndex("0"), g.If(active, h.Class("active")), g.Text(text)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchForm() g.Node {
|
||||||
|
return h.Form(
|
||||||
|
h.Role("search"),
|
||||||
|
h.Method("post"),
|
||||||
|
h.Action("/up/search"),
|
||||||
|
g.Attr("up-submit"),
|
||||||
|
g.Attr("up-autosubmit"),
|
||||||
|
g.Attr("up-target", ".content"),
|
||||||
|
h.Label(
|
||||||
|
h.Class("input input-bordered input-sm flex items-center gap-2"),
|
||||||
|
h.Input(
|
||||||
|
h.Name("search"),
|
||||||
|
h.Type("search"),
|
||||||
|
h.Class("grow"),
|
||||||
|
h.Placeholder("Suche"),
|
||||||
|
),
|
||||||
|
outline.MagnifyingGlass(
|
||||||
|
h.Class("h-4 w-4 opacity-70"),
|
||||||
|
g.Attr("stroke", "currentColor"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Navbar() g.Node {
|
||||||
|
return h.Nav(
|
||||||
|
h.Class("fixed top-0 z-50 w-full p-4"),
|
||||||
|
h.Div(
|
||||||
|
h.Class("navbar bg-base-300 rounded-box drop-shadow-md"),
|
||||||
|
// links for large screens
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex-1"),
|
||||||
|
h.A(h.Href("/"), h.TabIndex("0"), h.Class("btn btn-ghost text-xl"), g.Text("crowsnest")),
|
||||||
|
h.Ul(
|
||||||
|
h.Class("menu menu-horizontal hidden sm:flex"),
|
||||||
|
NavbarLi("/", "Artikel", true),
|
||||||
|
NavbarLi("/", "Themen", false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// search
|
||||||
|
h.Div(h.Class("hidden sm:flex flex-none pe-4"), SearchForm()),
|
||||||
|
// small navbar dropdown
|
||||||
|
h.Div(
|
||||||
|
h.Class("dropdown dropdown-end sm:hidden"),
|
||||||
|
// dropdown button
|
||||||
|
h.Div(
|
||||||
|
h.TabIndex("0"),
|
||||||
|
h.Role("button"),
|
||||||
|
h.Class("btn btn-ghost"),
|
||||||
|
outline.Bars3BottomRight(
|
||||||
|
h.Class("h-6 w-6 opacity-70"),
|
||||||
|
g.Attr("stroke", "currentColor"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// dropdown content
|
||||||
|
h.Ul(
|
||||||
|
h.Class("menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"),
|
||||||
|
NavbarLi("/", "Artikel", true),
|
||||||
|
NavbarLi("/", "Themen", false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
45
src/internal/html/pagination.go
Normal file
45
src/internal/html/pagination.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
h "maragu.dev/gomponents/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PaginationButton(content string, active bool, disabled bool) g.Node {
|
||||||
|
classStr := "join-item btn btn-sm"
|
||||||
|
if disabled { classStr += " btn-disabled" }
|
||||||
|
if active { classStr += " btn-active" }
|
||||||
|
|
||||||
|
return h.A(
|
||||||
|
h.Class(classStr),
|
||||||
|
g.If(!disabled, h.Href("/page/"+content)),
|
||||||
|
g.If(!disabled, h.TabIndex("0")),
|
||||||
|
g.Attr("up-follow"),
|
||||||
|
g.Attr("up-target", ".content"),
|
||||||
|
g.Text(content),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pagination(currentPage uint, totalPages uint) g.Node {
|
||||||
|
buttons := make([]g.Node, 0)
|
||||||
|
|
||||||
|
if totalPages > 1 {
|
||||||
|
buttons = append(buttons, PaginationButton("1", currentPage == 1, false))
|
||||||
|
}
|
||||||
|
if currentPage > 3 {
|
||||||
|
buttons = append(buttons, PaginationButton("...", false, true))
|
||||||
|
}
|
||||||
|
for i := max(2, currentPage-1); i <= min(totalPages-1, currentPage+1); i++ {
|
||||||
|
buttons = append(buttons, PaginationButton(strconv.Itoa(int(i)), i == currentPage, false))
|
||||||
|
}
|
||||||
|
if currentPage < totalPages-2 {
|
||||||
|
buttons = append(buttons, PaginationButton("...", false, true))
|
||||||
|
}
|
||||||
|
if totalPages > 1 {
|
||||||
|
buttons = append(buttons, PaginationButton(strconv.Itoa(int(totalPages)), totalPages == currentPage, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Div(h.Class("join pagination p-5 mx-auto"), h.Span(buttons...))
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ func (m *ArticleRepository) All(limit int, offset int) ([]*model.Article, error)
|
|||||||
// Counts all articles in the database. This may throw an error if the
|
// Counts all articles in the database. This may throw an error if the
|
||||||
// connection to the database fails.
|
// connection to the database fails.
|
||||||
func (m *ArticleRepository) CountAll() (uint, error) {
|
func (m *ArticleRepository) CountAll() (uint, error) {
|
||||||
stmt := `SELECT count(id) FROM articles `
|
stmt := `SELECT count(id) FROM articles`
|
||||||
|
|
||||||
rows := m.DB.QueryRow(stmt)
|
rows := m.DB.QueryRow(stmt)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user