aboutsummaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/config/config.go10
-rw-r--r--pkg/handler/add/add.go (renamed from pkg/handler/add.go)4
-rw-r--r--pkg/handler/add/handler.go21
-rw-r--r--pkg/handler/admin/admin.go (renamed from pkg/handler/admin.go)36
-rw-r--r--pkg/handler/captcha/handler.go17
-rw-r--r--pkg/handler/feed/feed.go73
-rw-r--r--pkg/handler/feed/handler.go21
-rw-r--r--pkg/handler/handler.go14
-rw-r--r--pkg/handler/quote/handler.go26
-rw-r--r--pkg/handler/quote/index.go (renamed from pkg/handler/index.go)10
-rw-r--r--pkg/handler/quote/quote.go (renamed from pkg/handler/quote.go)4
-rw-r--r--pkg/handler/quote/random.go18
-rw-r--r--pkg/handler/quote/top.go28
-rw-r--r--pkg/handler/random.go31
-rw-r--r--pkg/handler/rate/handler.go19
-rw-r--r--pkg/handler/rate/rate.go69
-rw-r--r--pkg/middleware/context.go21
-rw-r--r--pkg/middleware/session/store.go230
-rw-r--r--pkg/model/quote.go1
-rw-r--r--pkg/tpl/add.templ24
-rw-r--r--pkg/tpl/add_templ.go52
-rw-r--r--pkg/tpl/layout.templ8
-rw-r--r--pkg/tpl/layout_templ.go10
-rw-r--r--pkg/tpl/list.templ (renamed from pkg/tpl/index.templ)10
-rw-r--r--pkg/tpl/list_templ.go (renamed from pkg/tpl/index_templ.go)15
-rw-r--r--pkg/tpl/quote.templ17
-rw-r--r--pkg/tpl/quote_templ.go48
-rw-r--r--pkg/tpl/random.templ27
-rw-r--r--pkg/tpl/random_templ.go37
-rw-r--r--pkg/tpl/rate.templ31
-rw-r--r--pkg/tpl/rate_templ.go85
31 files changed, 907 insertions, 110 deletions
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 342012e..8e7402a 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -8,10 +8,12 @@ import (
)
type Config struct {
- Debug bool `yaml:"debug"`
- DB *db.Config `yaml:"db"`
- Listen string `yaml:"listen"`
- Admins map[string]string `yaml:"admins"`
+ Debug bool `yaml:"debug"`
+ DB *db.Config `yaml:"db"`
+ Listen string `yaml:"listen"`
+ Host string `yaml:"host"`
+ Admins map[string]string `yaml:"admins"`
+ Keypairs []string `yaml:"keys"`
}
func New(file string) (*Config, error) {
diff --git a/pkg/handler/add.go b/pkg/handler/add/add.go
index a6a5ced..dbcce47 100644
--- a/pkg/handler/add.go
+++ b/pkg/handler/add/add.go
@@ -1,4 +1,4 @@
-package handler
+package add
import (
"net/http"
@@ -40,7 +40,7 @@ func (h *Handler) AddQuotePost(c echo.Context) error {
Approved: false,
Archive: false,
}
- if _, err := h.DB.NewInsert().Model(q).Exec(c.Request().Context()); err != nil {
+ if _, err := h.db.NewInsert().Model(q).Exec(c.Request().Context()); err != nil {
return err
}
diff --git a/pkg/handler/add/handler.go b/pkg/handler/add/handler.go
new file mode 100644
index 0000000..8f744ab
--- /dev/null
+++ b/pkg/handler/add/handler.go
@@ -0,0 +1,21 @@
+package add
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/uptrace/bun"
+)
+
+type Handler struct {
+ db *bun.DB
+}
+
+// NewHandler returns new Handler.
+func NewHandler(db *bun.DB) *Handler {
+ return &Handler{db: db}
+}
+
+func (h *Handler) Register(g *echo.Group) {
+ g.GET("", h.AddQuote)
+ g.POST("", h.AddQuotePost)
+ g.GET("/success", h.AddQuoteSuccess)
+}
diff --git a/pkg/handler/admin.go b/pkg/handler/admin/admin.go
index 75fb650..494da05 100644
--- a/pkg/handler/admin.go
+++ b/pkg/handler/admin/admin.go
@@ -1,16 +1,42 @@
-package handler
+package admin
import (
"net/http"
"github.com/labstack/echo/v4"
+ "github.com/labstack/echo/v4/middleware"
+ "github.com/uptrace/bun"
+ "sh.org.ru/pkg/config"
"sh.org.ru/pkg/model"
"sh.org.ru/pkg/tpl"
)
+type Handler struct {
+ db *bun.DB
+ cfg *config.Config
+}
+
+// NewHandler returns new Handler.
+func NewHandler(db *bun.DB, cfg *config.Config) *Handler {
+ return &Handler{
+ db: db,
+ cfg: cfg,
+ }
+}
+
+func (h *Handler) Register(g *echo.Group) {
+ g.Use(middleware.BasicAuth(func(u, p string, ctx echo.Context) (bool, error) {
+ return h.cfg.Admins[u] == p, nil
+ }))
+
+ g.GET("/", h.Admin)
+ g.POST("/action", h.AdminAction)
+ g.GET("/export", h.AdminExport)
+}
+
func (h *Handler) Admin(c echo.Context) error {
quotes := make([]model.Quote, 0, 20)
- count, err := h.DB.NewSelect().
+ count, err := h.db.NewSelect().
Model((*model.Quote)(nil)).
Order("id ASC").
Where("approved = ?", false).
@@ -30,7 +56,7 @@ func (h *Handler) AdminAction(c echo.Context) error {
switch form.Action {
case "approve":
- _, err := h.DB.NewUpdate().
+ _, err := h.db.NewUpdate().
Model(&model.Quote{
ID: int64(form.ID),
Approved: true,
@@ -42,7 +68,7 @@ func (h *Handler) AdminAction(c echo.Context) error {
return err
}
case "decline":
- _, err := h.DB.NewDelete().
+ _, err := h.db.NewDelete().
Model(&model.Quote{
ID: int64(form.ID),
}).
@@ -58,7 +84,7 @@ func (h *Handler) AdminAction(c echo.Context) error {
func (h *Handler) AdminExport(c echo.Context) error {
quotes := []model.Quote{}
- err := h.DB.NewSelect().
+ err := h.db.NewSelect().
Model((*model.Quote)(nil)).
Order("id ASC").
Scan(c.Request().Context(), &quotes)
diff --git a/pkg/handler/captcha/handler.go b/pkg/handler/captcha/handler.go
new file mode 100644
index 0000000..2e3b483
--- /dev/null
+++ b/pkg/handler/captcha/handler.go
@@ -0,0 +1,17 @@
+package captcha
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/ssoda/captcha"
+)
+
+type Handler struct{}
+
+// NewHandler returns new Handler.
+func NewHandler() *Handler {
+ return &Handler{}
+}
+
+func (h *Handler) Register(g *echo.Group) {
+ g.GET("/*", echo.WrapHandler(captcha.Server(400, 65)))
+}
diff --git a/pkg/handler/feed/feed.go b/pkg/handler/feed/feed.go
new file mode 100644
index 0000000..05921e9
--- /dev/null
+++ b/pkg/handler/feed/feed.go
@@ -0,0 +1,73 @@
+package feed
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/feeds"
+ "github.com/labstack/echo/v4"
+ "sh.org.ru/pkg/model"
+)
+
+func (h *Handler) Feed(c echo.Context) error {
+ feedType := c.Param("type")
+
+ quotes := make([]model.Quote, 0, 20)
+ err := h.db.NewSelect().
+ Model((*model.Quote)(nil)).
+ Order("id DESC").
+ Where("approved = ?", true).
+ Limit(20).
+ Scan(c.Request().Context(), &quotes)
+ if err != nil {
+ return err
+ }
+
+ feed := &feeds.Feed{
+ Title: "sh.org.ru - Новый цитатник Рунета",
+ Link: &feeds.Link{Href: h.cfg.Host},
+ Description: "",
+ Author: &feeds.Author{Name: "NeonXP", Email: "i@neonxp.ru"},
+ Created: time.Now(),
+ }
+
+ for _, q := range quotes {
+ uid := fmt.Sprintf("%s/quote/%d", h.cfg.Host, q.ID)
+ feed.Items = append(feed.Items, &feeds.Item{
+ Id: uid,
+ Title: fmt.Sprintf("Цитата #%d", q.ID),
+ Link: &feeds.Link{Href: uid},
+ Created: q.CreatedAt,
+ Description: q.Text(),
+ })
+ }
+ switch feedType {
+ case "rss":
+ result, err := feed.ToRss()
+ if err != nil {
+ return err
+ }
+
+ c.Response().Header().Set("Content-Type", "application/rss+xml")
+ return c.String(http.StatusOK, result)
+ case "atom":
+ result, err := feed.ToAtom()
+ if err != nil {
+ return err
+ }
+
+ c.Response().Header().Set("Content-Type", "application/atom+xml")
+ return c.String(http.StatusOK, result)
+ case "json":
+ result, err := feed.ToJSON()
+ if err != nil {
+ return err
+ }
+
+ c.Response().Header().Set("Content-Type", "application/json")
+ return c.String(http.StatusOK, result)
+ default:
+ return echo.ErrNotFound
+ }
+}
diff --git a/pkg/handler/feed/handler.go b/pkg/handler/feed/handler.go
new file mode 100644
index 0000000..868e477
--- /dev/null
+++ b/pkg/handler/feed/handler.go
@@ -0,0 +1,21 @@
+package feed
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/uptrace/bun"
+ "sh.org.ru/pkg/config"
+)
+
+type Handler struct {
+ db *bun.DB
+ cfg *config.Config
+}
+
+// NewHandler returns new Handler.
+func NewHandler(db *bun.DB, cfg *config.Config) *Handler {
+ return &Handler{db: db, cfg: cfg}
+}
+
+func (h *Handler) Register(g *echo.Group) {
+ g.GET("/:type", h.Feed)
+}
diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go
index 5ba3966..15eb42d 100644
--- a/pkg/handler/handler.go
+++ b/pkg/handler/handler.go
@@ -1,7 +1,15 @@
package handler
-import "github.com/uptrace/bun"
+import "github.com/labstack/echo/v4"
-type Handler struct {
- DB *bun.DB
+type Handler interface {
+ Register(g *echo.Group)
+}
+
+type Router map[string]Handler
+
+func (r Router) Register(e *echo.Echo) {
+ for groupName, handlers := range r {
+ handlers.Register(e.Group(groupName))
+ }
}
diff --git a/pkg/handler/quote/handler.go b/pkg/handler/quote/handler.go
new file mode 100644
index 0000000..04807c0
--- /dev/null
+++ b/pkg/handler/quote/handler.go
@@ -0,0 +1,26 @@
+package quote
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/uptrace/bun"
+)
+
+type Handler struct {
+ db *bun.DB
+}
+
+// NewHandler returns new Handler.
+func NewHandler(db *bun.DB) *Handler {
+ return &Handler{db: db}
+}
+
+func (h *Handler) Register(g *echo.Group) {
+ g.GET("", h.Index)
+ g.GET("quote/:id", h.Quote)
+ g.GET("random", h.Random)
+ g.GET("top", h.Top)
+}
+
+type Pagination struct {
+ Page int `query:"page" default:"0"`
+}
diff --git a/pkg/handler/index.go b/pkg/handler/quote/index.go
index 611a544..9dd21f8 100644
--- a/pkg/handler/index.go
+++ b/pkg/handler/quote/index.go
@@ -1,4 +1,4 @@
-package handler
+package quote
import (
"github.com/labstack/echo/v4"
@@ -13,7 +13,7 @@ func (h *Handler) Index(c echo.Context) error {
}
quotes := make([]model.Quote, 0, 20)
- count, err := h.DB.NewSelect().
+ count, err := h.db.NewSelect().
Model((*model.Quote)(nil)).
Order("id DESC").
Where("approved = ?", true).
@@ -24,9 +24,5 @@ func (h *Handler) Index(c echo.Context) error {
return err
}
- return tpl.Index(quotes, p.Page, count).Render(c.Request().Context(), c.Response())
-}
-
-type Pagination struct {
- Page int `query:"page" default:"0"`
+ return tpl.List(quotes, p.Page, count).Render(c.Request().Context(), c.Response())
}
diff --git a/pkg/handler/quote.go b/pkg/handler/quote/quote.go
index 2a5f7e6..b25eb90 100644
--- a/pkg/handler/quote.go
+++ b/pkg/handler/quote/quote.go
@@ -1,4 +1,4 @@
-package handler
+package quote
import (
"strconv"
@@ -16,7 +16,7 @@ func (h *Handler) Quote(c echo.Context) error {
}
quote := new(model.Quote)
- err = h.DB.NewSelect().
+ err = h.db.NewSelect().
Model(quote).
Where("id = ?", id).Scan(c.Request().Context(), quote)
if err != nil {
diff --git a/pkg/handler/quote/random.go b/pkg/handler/quote/random.go
new file mode 100644
index 0000000..1e04ff0
--- /dev/null
+++ b/pkg/handler/quote/random.go
@@ -0,0 +1,18 @@
+package quote
+
+import (
+ "github.com/labstack/echo/v4"
+ "sh.org.ru/pkg/model"
+ "sh.org.ru/pkg/tpl"
+)
+
+func (h *Handler) Random(c echo.Context) error {
+ quotes := make([]model.Quote, 0, 20)
+ err := h.db.NewRaw(`select q.* from quotes q where q.approved = true order by random() limit 20`).
+ Scan(c.Request().Context(), &quotes)
+ if err != nil {
+ return err
+ }
+
+ return tpl.Random(quotes).Render(c.Request().Context(), c.Response())
+}
diff --git a/pkg/handler/quote/top.go b/pkg/handler/quote/top.go
new file mode 100644
index 0000000..c2803ea
--- /dev/null
+++ b/pkg/handler/quote/top.go
@@ -0,0 +1,28 @@
+package quote
+
+import (
+ "github.com/labstack/echo/v4"
+ "sh.org.ru/pkg/model"
+ "sh.org.ru/pkg/tpl"
+)
+
+func (h *Handler) Top(c echo.Context) error {
+ p := &Pagination{}
+ if err := c.Bind(p); err != nil {
+ return err
+ }
+
+ quotes := make([]model.Quote, 0, 20)
+ count, err := h.db.NewSelect().
+ Model((*model.Quote)(nil)).
+ Order("rating DESC").
+ Where("approved = ?", true).
+ Limit(20).
+ Offset(p.Page*20).
+ ScanAndCount(c.Request().Context(), &quotes)
+ if err != nil {
+ return err
+ }
+
+ return tpl.List(quotes, p.Page, count).Render(c.Request().Context(), c.Response())
+}
diff --git a/pkg/handler/random.go b/pkg/handler/random.go
deleted file mode 100644
index 29c5f6f..0000000
--- a/pkg/handler/random.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package handler
-
-import (
- "github.com/a-h/templ"
- "github.com/labstack/echo/v4"
- "sh.org.ru/pkg/model"
- "sh.org.ru/pkg/tpl"
-)
-
-func (h *Handler) Random(c echo.Context) error {
- quotes := make([]model.Quote, 0, 20)
- err := h.DB.NewRaw(`select q.* from quotes q where q.approved = true order by random() limit 20`).
- Scan(c.Request().Context(), &quotes)
- if err != nil {
- return err
- }
-
- comp := tpl.Random(quotes)
-
- if c.Request().Header.Get("Hx-Request") == "true" {
- return comp.Render(c.Request().Context(), c.Response())
- }
-
- ctx := templ.WithChildren(c.Request().Context(), comp)
-
- return tpl.Layout(tpl.HeaderParams{
- Title: "Цитатник Рунета",
- Description: "Новый цитатник Рунета",
- URL: "https://sh.org.ru/",
- }).Render(ctx, c.Response())
-}
diff --git a/pkg/handler/rate/handler.go b/pkg/handler/rate/handler.go
new file mode 100644
index 0000000..1a2ecf3
--- /dev/null
+++ b/pkg/handler/rate/handler.go
@@ -0,0 +1,19 @@
+package rate
+
+import (
+ "github.com/labstack/echo/v4"
+ "github.com/uptrace/bun"
+)
+
+type Handler struct {
+ db *bun.DB
+}
+
+// NewHandler returns new Handler.
+func NewHandler(db *bun.DB) *Handler {
+ return &Handler{db: db}
+}
+
+func (h *Handler) Register(g *echo.Group) {
+ g.POST("/:id", h.Rate)
+}
diff --git a/pkg/handler/rate/rate.go b/pkg/handler/rate/rate.go
new file mode 100644
index 0000000..df6713f
--- /dev/null
+++ b/pkg/handler/rate/rate.go
@@ -0,0 +1,69 @@
+package rate
+
+import (
+ "strconv"
+
+ "github.com/labstack/echo-contrib/session"
+ "github.com/labstack/echo/v4"
+ "sh.org.ru/pkg/model"
+ "sh.org.ru/pkg/tpl"
+)
+
+func (h *Handler) Rate(c echo.Context) error {
+ voted, err := session.Get("votes", c)
+ if err != nil {
+ return err
+ }
+ sid := c.Param("id")
+ id, err := strconv.Atoi(sid)
+ if err != nil {
+ return err
+ }
+ f := new(rateForm)
+ if err := c.Bind(f); err != nil {
+ return err
+ }
+
+ quote := new(model.Quote)
+
+ if _, ok := voted.Values[id]; !ok {
+ voted.Values[id] = true
+ if err := voted.Save(c.Request(), c.Response()); err != nil {
+ return err
+ }
+ set := ""
+ switch f.Vote {
+ case "up":
+ set = "rating = rating + 1"
+ case "down":
+ set = "rating = rating - 1"
+ }
+ _, err = h.db.NewUpdate().
+ Model(quote).
+ Where("id = ?", id).
+ Set(set).
+ Returning("*").
+ Exec(c.Request().Context(), quote)
+ } else {
+ err = h.db.NewSelect().
+ Model(quote).
+ Where("id = ?", id).
+ Scan(c.Request().Context(), quote)
+ }
+ if err != nil {
+ return err
+ }
+
+ return tpl.Rate(quote, 0).Render(c.Request().Context(), c.Response())
+}
+
+type rateForm struct {
+ Vote string `form:"vote"`
+}
+
+type voteType string
+
+const (
+ voteUp voteType = "up"
+ voteDown voteType = "down"
+)
diff --git a/pkg/middleware/context.go b/pkg/middleware/context.go
new file mode 100644
index 0000000..f9c4425
--- /dev/null
+++ b/pkg/middleware/context.go
@@ -0,0 +1,21 @@
+package middleware
+
+import (
+ "context"
+
+ "github.com/labstack/echo/v4"
+)
+
+type ContextKey string
+
+func Context(key ContextKey, value any) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ ctx := context.WithValue(c.Request().Context(), key, value)
+ r := c.Request().WithContext(ctx)
+ c.SetRequest(r)
+
+ return next(c)
+ }
+ }
+}
diff --git a/pkg/middleware/session/store.go b/pkg/middleware/session/store.go
new file mode 100644
index 0000000..04071c9
--- /dev/null
+++ b/pkg/middleware/session/store.go
@@ -0,0 +1,230 @@
+package session
+
+import (
+ "context"
+ "encoding/base32"
+ "log/slog"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/gorilla/securecookie"
+ "github.com/gorilla/sessions"
+ "github.com/uptrace/bun"
+)
+
+const sessionIDLen = 32
+const defaultTableName = "sessions"
+const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days
+const defaultPath = "/"
+
+// Options for bunstore
+type Options struct {
+ TableName string
+ SkipCreateTable bool
+}
+
+// Store represent a bunstore
+type Store struct {
+ db *bun.DB
+ opts Options
+ Codecs []securecookie.Codec
+ SessionOpts *sessions.Options
+}
+
+type bunSession struct {
+ bun.BaseModel `bun:"table:sessions,alias:s"`
+
+ ID string `bun:",pk,unique"`
+ Data string
+ CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
+ UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
+ ExpiresAt time.Time
+}
+
+type KeyPairs []string
+
+func (k KeyPairs) ToKeys() [][]byte {
+ b := make([][]byte, 0, len(k))
+ for _, kk := range k {
+ b = append(b, []byte(kk))
+ }
+ return b
+}
+
+// New creates a new bunstore session
+func New(db *bun.DB, keyPairs KeyPairs) (*Store, error) {
+ return NewOptions(db, Options{}, keyPairs)
+}
+
+// NewOptions creates a new bunstore session with options
+func NewOptions(db *bun.DB, opts Options, keyPairs KeyPairs) (*Store, error) {
+ st := &Store{
+ db: db,
+ opts: opts,
+ Codecs: securecookie.CodecsFromPairs(keyPairs.ToKeys()...),
+ SessionOpts: &sessions.Options{
+ Path: defaultPath,
+ MaxAge: defaultMaxAge,
+ },
+ }
+ if st.opts.TableName == "" {
+ st.opts.TableName = defaultTableName
+ }
+
+ if !st.opts.SkipCreateTable {
+ model := &bunSession{}
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ if _, err := db.NewCreateTable().IfNotExists().Model(model).Exec(ctx); err != nil {
+ return nil, err
+ }
+ if _, err := db.NewCreateIndex().Model(model).Column("expires_at").Exec(ctx); err != nil {
+ return nil, err
+ }
+ }
+
+ return st, nil
+}
+
+// Get returns a session for the given name after adding it to the registry.
+func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
+ return sessions.GetRegistry(r).Get(st, name)
+}
+
+// New creates a session with name without adding it to the registry.
+func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) {
+ session := sessions.NewSession(st, name)
+ opts := *st.SessionOpts
+ session.Options = &opts
+ session.IsNew = true
+
+ st.MaxAge(st.SessionOpts.MaxAge)
+
+ // try fetch from db if there is a cookie
+ s := st.getSessionFromCookie(r, session.Name())
+ if s != nil {
+ if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil {
+ return session, nil
+ }
+ session.ID = s.ID
+ session.IsNew = false
+ }
+
+ return session, nil
+}
+
+// Save session and set cookie header
+func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+ s := st.getSessionFromCookie(r, session.Name())
+
+ // delete if max age is < 0
+ if session.Options.MaxAge < 0 {
+ if s != nil {
+ if _, err := st.db.NewDelete().Model(&bunSession{ID: session.ID}).Exec(r.Context()); err != nil {
+ return err
+ }
+ }
+ http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
+ return nil
+ }
+
+ data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...)
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ expire := now.Add(time.Second * time.Duration(session.Options.MaxAge))
+
+ if s == nil {
+ // generate random session ID key suitable for storage in the db
+ session.ID = strings.TrimRight(
+ base32.StdEncoding.EncodeToString(
+ securecookie.GenerateRandomKey(sessionIDLen)), "=")
+ s = &bunSession{
+ ID: session.ID,
+ Data: data,
+ ExpiresAt: expire,
+ }
+ if _, err := st.db.NewInsert().Model(s).Exec(r.Context()); err != nil {
+ return err
+ }
+ } else {
+ s.Data = data
+ s.ExpiresAt = expire
+ if _, err := st.db.NewUpdate().Model(s).WherePK("id").Column("data", "expires_at").Exec(r.Context()); err != nil {
+ return err
+ }
+ }
+
+ // set session id cookie
+ id, err := securecookie.EncodeMulti(session.Name(), s.ID, st.Codecs...)
+ if err != nil {
+ return err
+ }
+ http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options))
+
+ return nil
+}
+
+// getSessionFromCookie looks for an existing bunSession from a session ID stored inside a cookie
+func (st *Store) getSessionFromCookie(r *http.Request, name string) *bunSession {
+ if cookie, err := r.Cookie(name); err == nil {
+ sessionID := ""
+ if err := securecookie.DecodeMulti(name, cookie.Value, &sessionID, st.Codecs...); err != nil {
+ return nil
+ }
+ s := &bunSession{}
+ err := st.db.NewSelect().Model(s).Where("id = ? AND expires_at > ?", sessionID, time.Now()).Scan(r.Context())
+ if err != nil {
+ return nil
+ }
+ return s
+ }
+ return nil
+}
+
+// MaxAge sets the maximum age for the store and the underlying cookie
+// implementation. Individual sessions can be deleted by setting
+// Options.MaxAge = -1 for that session.
+func (st *Store) MaxAge(age int) {
+ st.SessionOpts.MaxAge = age
+ for _, codec := range st.Codecs {
+ if sc, ok := codec.(*securecookie.SecureCookie); ok {
+ sc.MaxAge(age)
+ }
+ }
+}
+
+// MaxLength restricts the maximum length of new sessions to l.
+// If l is 0 there is no limit to the size of a session, use with caution.
+// The default is 4096 (default for securecookie)
+func (st *Store) MaxLength(l int) {
+ for _, c := range st.Codecs {
+ if codec, ok := c.(*securecookie.SecureCookie); ok {
+ codec.MaxLength(l)
+ }
+ }
+}
+
+// Cleanup deletes expired sessions
+func (st *Store) Cleanup() {
+ _, err := st.db.NewDelete().Model(&bunSession{}).Where("expires_at <= ?", time.Now()).Exec(context.Background())
+ if err != nil {
+ slog.Default().With("error", err).Error("cleanup")
+ }
+}
+
+// PeriodicCleanup runs Cleanup every interval. Close quit channel to stop.
+func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) {
+ t := time.NewTicker(interval)
+ defer t.Stop()
+ for {
+ select {
+ case <-t.C:
+ st.Cleanup()
+ case <-quit:
+ return
+ }
+ }
+}
diff --git a/pkg/model/quote.go b/pkg/model/quote.go
index 0ad89cf..1e397b8 100644
--- a/pkg/model/quote.go
+++ b/pkg/model/quote.go
@@ -14,6 +14,7 @@ type Quote struct {
Quote string `bun:",notnull"`
Approved bool
Archive bool
+ Rating int
CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
DeletedAt time.Time `bun:",soft_delete,nullzero"`
}
diff --git a/pkg/tpl/add.templ b/pkg/tpl/add.templ
index 14b57eb..82d76e5 100644
--- a/pkg/tpl/add.templ
+++ b/pkg/tpl/add.templ
@@ -2,7 +2,10 @@ package tpl
import "fmt"
+var captchaHandler = templ.NewOnceHandle()
+
templ AddQuotePage(form *AddQuoteForm, err string) {
+ {{ captchaURL := fmt.Sprintf("/captcha/download/%s.png", form.CaptchaID) }}
@Layout(HeaderParams{}) {
<h2>Добавление цитаты</h2>
if err != "" {
@@ -14,10 +17,27 @@ templ AddQuotePage(form *AddQuoteForm, err string) {
<form method="post">
<textarea rows="5" name="quote" placeholder="Текст цитаты">{ form.Quote }</textarea>
<input type="hidden" name="captcha_id" value={ form.CaptchaID }/>
- <img class="captcha" src={ fmt.Sprintf("/captcha/download/%s.png", form.CaptchaID) }/>
- <input type="text" name="captcha_value" placeholder="Код с картинки"/>
+ <label for="captcha_value">
+ <img class="captcha" id="captcha" src={ captchaURL }/>
+ <a
+ role="button"
+ data-url={ captchaURL }
+ onclick="reloadCaptcha(this)"
+ >
+ <i class="fa fa-refresh"></i>&nbsp;Обновить капчу
+ </a>
+ </label>
+ <input type="text" name="captcha_value" id="captcha_value" placeholder="Код с картинки"/>
<input type="submit" value="Отправить на модерацию"/>
</form>
+ @captchaHandler.Once() {
+ <script type="text/javascript">
+ function reloadCaptcha(event) {
+ const url = event.getAttribute('data-url');
+ document.getElementById('captcha').setAttribute('src', url+'?reload='+Math.random());
+ }
+ </script>
+ }
}
}
diff --git a/pkg/tpl/add_templ.go b/pkg/tpl/add_templ.go
index d47582b..b0731ac 100644
--- a/pkg/tpl/add_templ.go
+++ b/pkg/tpl/add_templ.go
@@ -10,6 +10,8 @@ import templruntime "github.com/a-h/templ/runtime"
import "fmt"
+var captchaHandler = templ.NewOnceHandle()
+
func AddQuotePage(form *AddQuoteForm, err string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -31,6 +33,7 @@ func AddQuotePage(form *AddQuoteForm, err string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
+ captchaURL := fmt.Sprintf("/captcha/download/%s.png", form.CaptchaID)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
@@ -55,7 +58,7 @@ func AddQuotePage(form *AddQuoteForm, err string) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 11, Col: 9}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 14, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -73,7 +76,7 @@ func AddQuotePage(form *AddQuoteForm, err string) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(form.Quote)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 15, Col: 85}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 18, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -86,26 +89,61 @@ func AddQuotePage(form *AddQuoteForm, err string) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(form.CaptchaID)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 16, Col: 64}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 19, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <img class=\"captcha\" src=\"")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <label for=\"captcha_value\"><img class=\"captcha\" id=\"captcha\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/captcha/download/%s.png", form.CaptchaID))
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(captchaURL)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 17, Col: 85}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 21, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"text\" name=\"captcha_value\" placeholder=\"Код с картинки\"> <input type=\"submit\" value=\"Отправить на модерацию\"></form>")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <a role=\"button\" data-url=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 string
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(captchaURL)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/add.templ`, Line: 24, Col: 26}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" onclick=\"reloadCaptcha(this)\"><i class=\"fa fa-refresh\"></i>&nbsp;Обновить капчу</a></label> <input type=\"text\" name=\"captcha_value\" id=\"captcha_value\" placeholder=\"Код с картинки\"> <input type=\"submit\" value=\"Отправить на модерацию\"></form>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script type=\"text/javascript\">\n\t\t\t\tfunction reloadCaptcha(event) {\n\t\t\t\t\tconst url = event.getAttribute('data-url');\n\t\t\t\t\tdocument.getElementById('captcha').setAttribute('src', url+'?reload='+Math.random());\n\t\t\t\t}\n\t\t\t</script>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = captchaHandler.Once().Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pkg/tpl/layout.templ b/pkg/tpl/layout.templ
index c777457..d96aa50 100644
--- a/pkg/tpl/layout.templ
+++ b/pkg/tpl/layout.templ
@@ -10,9 +10,13 @@ templ Layout(params HeaderParams) {
<link rel="stylesheet" href="/css/pico.css"/>
<link rel="stylesheet" href="/css/style.css"/>
<link rel="stylesheet" href="/css/fork-awesome.min.css"/>
+ <link rel="alternate" type="application/rss+xml" title="RSS feed" href="/feed/rss">
+ <link rel="alternate" type="application/atom+xml" title="ATOM feed" href="/feed/atom">
+ <link rel="alternate" type="application/json" title="json feed" href="/feed/json">
<meta property="og:title" content={ params.Title }/>
<meta property="og:url" content={ params.URL }/>
<meta property="og:description" content={ params.Description }/>
+ <meta name="yandex-verification" content="ee0e23da00ce9fe4" />
<title>ШОргРу</title>
</head>
<body>
@@ -21,9 +25,11 @@ templ Layout(params HeaderParams) {
<ul>
<li><a href="/"><strong>ШОргРу</strong></a></li>
</ul>
- <ul>
+ <ul hx-boost="true" hx-indicator=".loader">
+ <span aria-busy="true" class="loader htmx-indicator">Загрузка...</span>
<li><a href="/">Главная</a></li>
<li><a href="/random">Случайные</a></li>
+ <li><a href="/top">Топ</a></li>
<li><a href="/add">Добавить цитату</a></li>
</ul>
</nav>
diff --git a/pkg/tpl/layout_templ.go b/pkg/tpl/layout_templ.go
index a86bb30..10b0bfc 100644
--- a/pkg/tpl/layout_templ.go
+++ b/pkg/tpl/layout_templ.go
@@ -29,14 +29,14 @@ func Layout(params HeaderParams) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light dark\"><link rel=\"stylesheet\" href=\"/css/pico.css\"><link rel=\"stylesheet\" href=\"/css/style.css\"><link rel=\"stylesheet\" href=\"/css/fork-awesome.min.css\"><meta property=\"og:title\" content=\"")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light dark\"><link rel=\"stylesheet\" href=\"/css/pico.css\"><link rel=\"stylesheet\" href=\"/css/style.css\"><link rel=\"stylesheet\" href=\"/css/fork-awesome.min.css\"><link rel=\"alternate\" type=\"application/rss+xml\" title=\"RSS feed\" href=\"/feed/rss\"><link rel=\"alternate\" type=\"application/atom+xml\" title=\"ATOM feed\" href=\"/feed/atom\"><link rel=\"alternate\" type=\"application/json\" title=\"json feed\" href=\"/feed/json\"><meta property=\"og:title\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(params.Title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 13, Col: 51}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 16, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -49,7 +49,7 @@ func Layout(params HeaderParams) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(params.URL)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 14, Col: 47}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 17, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -62,13 +62,13 @@ func Layout(params HeaderParams) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(params.Description)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 15, Col: 63}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/layout.templ`, Line: 18, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><title>ШОргРу</title></head><body><main class=\"container\"><nav><ul><li><a href=\"/\"><strong>ШОргРу</strong></a></li></ul><ul><li><a href=\"/\">Главная</a></li><li><a href=\"/random\">Случайные</a></li><li><a href=\"/add\">Добавить цитату</a></li></ul></nav>")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><meta name=\"yandex-verification\" content=\"ee0e23da00ce9fe4\"><title>ШОргРу</title></head><body><main class=\"container\"><nav><ul><li><a href=\"/\"><strong>ШОргРу</strong></a></li></ul><ul hx-boost=\"true\" hx-indicator=\".loader\"><span aria-busy=\"true\" class=\"loader htmx-indicator\">Загрузка...</span><li><a href=\"/\">Главная</a></li><li><a href=\"/random\">Случайные</a></li><li><a href=\"/top\">Топ</a></li><li><a href=\"/add\">Добавить цитату</a></li></ul></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pkg/tpl/index.templ b/pkg/tpl/list.templ
index 3655531..014a2db 100644
--- a/pkg/tpl/index.templ
+++ b/pkg/tpl/list.templ
@@ -2,21 +2,25 @@ package tpl
import (
"fmt"
+ "sh.org.ru/pkg/config"
"sh.org.ru/pkg/model"
"strconv"
+ "sh.org.ru/pkg/middleware"
)
-templ Index(quotes []model.Quote, page, count int) {
+templ List(quotes []model.Quote, page, count int) {
+ {{ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host }}
@Layout(HeaderParams{
Title: "Цитатник Рунета",
Description: "Новый цитатник Рунета",
- URL: "https://sh.org.ru/",
+ URL: host,
}) {
for _, q := range quotes {
@Quote(&q)
}
+ <span aria-busy="true" class="loader htmx-indicator">Загрузка...</span>
<nav>
- <ul hx-boost="true">
+ <ul hx-boost="true" hx-indicator=".loader">
if page > 0 {
<li><a href={ templ.URL(fmt.Sprintf("/?page=%d", page-1)) }>&larr;</a></li>
}
diff --git a/pkg/tpl/index_templ.go b/pkg/tpl/list_templ.go
index eabd30d..2ed9497 100644
--- a/pkg/tpl/index_templ.go
+++ b/pkg/tpl/list_templ.go
@@ -10,11 +10,13 @@ import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
+ "sh.org.ru/pkg/config"
+ "sh.org.ru/pkg/middleware"
"sh.org.ru/pkg/model"
"strconv"
)
-func Index(quotes []model.Quote, page, count int) templ.Component {
+func List(quotes []model.Quote, page, count int) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -35,6 +37,7 @@ func Index(quotes []model.Quote, page, count int) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
+ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
@@ -53,7 +56,7 @@ func Index(quotes []model.Quote, page, count int) templ.Component {
return templ_7745c5c3_Err
}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <nav><ul hx-boost=\"true\">")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <span aria-busy=\"true\" class=\"loader htmx-indicator\">Загрузка...</span><nav><ul hx-boost=\"true\" hx-indicator=\".loader\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -86,7 +89,7 @@ func Index(quotes []model.Quote, page, count int) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/index.templ`, Line: 27, Col: 14}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/list.templ`, Line: 31, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -113,7 +116,7 @@ func Index(quotes []model.Quote, page, count int) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(p)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/index.templ`, Line: 29, Col: 64}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/list.templ`, Line: 33, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -147,7 +150,7 @@ func Index(quotes []model.Quote, page, count int) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(count))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/index.templ`, Line: 38, Col: 34}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/list.templ`, Line: 42, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -162,7 +165,7 @@ func Index(quotes []model.Quote, page, count int) templ.Component {
templ_7745c5c3_Err = Layout(HeaderParams{
Title: "Цитатник Рунета",
Description: "Новый цитатник Рунета",
- URL: "https://sh.org.ru/",
+ URL: host,
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
diff --git a/pkg/tpl/quote.templ b/pkg/tpl/quote.templ
index 34429d1..7b38faa 100644
--- a/pkg/tpl/quote.templ
+++ b/pkg/tpl/quote.templ
@@ -2,11 +2,14 @@ package tpl
import (
"fmt"
+ "sh.org.ru/pkg/config"
+ "sh.org.ru/pkg/middleware"
"sh.org.ru/pkg/model"
"strconv"
)
templ Quote(quote *model.Quote) {
+ {{ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host }}
<article>
<header>
<a href={ templ.URL(fmt.Sprintf("/quote/%d", quote.ID)) }>#{ strconv.Itoa(int(quote.ID)) }</a>
@@ -14,24 +17,24 @@ templ Quote(quote *model.Quote) {
</header>
@templ.Raw(quote.Text())
<footer>
- <span>
- <a target="_blank" href={ templ.URL(fmt.Sprintf("https://t.me/share/url?url=https://sh.org.ru/quote/%d", quote.ID)) }><i class="fa fa-telegram" aria-hidden="true"></i></a>&nbsp;&middot;&nbsp
- <a target="_blank" href={ templ.URL(fmt.Sprintf("https://vk.com/share.php?url=https://sh.org.ru/quote/%d", quote.ID)) }><i class="fa fa-vk" aria-hidden="true"></i></a>&nbsp;&middot;&nbsp
- <a target="_blank" href={ templ.URL(fmt.Sprintf("https://connect.ok.ru/offer?url=https://sh.org.ru/quote/%d", quote.ID)) }><i class="fa fa-odnoklassniki-square" aria-hidden="true"></i></a>
- </span>
+ @Rate(quote, 0)
<span>
if quote.Archive {
<abbr title="Цитата из старого цитатника">Архив</abbr>
}
+ <a target="_blank" href={ templ.URL(fmt.Sprintf("https://t.me/share/url?url=%s/quote/%d", host, quote.ID)) }><i class="fa fa-telegram" aria-hidden="true"></i></a>&nbsp;&middot;&nbsp
+ <a target="_blank" href={ templ.URL(fmt.Sprintf("https://vk.com/share.php?url=%s/quote/%d", host, quote.ID)) }><i class="fa fa-vk" aria-hidden="true"></i></a>&nbsp;&middot;&nbsp
+ <a target="_blank" href={ templ.URL(fmt.Sprintf("https://connect.ok.ru/offer?url=%s/quote/%d", host, quote.ID)) }><i class="fa fa-odnoklassniki-square" aria-hidden="true"></i></a>
</span>
</footer>
</article>
}
templ QuotePage(quote *model.Quote) {
+ {{ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host }}
@Layout(HeaderParams{
- Title: "Цитата #" + strconv.Itoa(int(quote.ID)),
- URL: fmt.Sprintf("https://sh.org.ru/quote/%d", quote.ID),
+ Title: "Цитата #" + strconv.Itoa(int(quote.ID)),
+ URL: fmt.Sprintf("%s/quote/%d", host, quote.ID),
Description: templ.EscapeString(quote.Quote),
}) {
@Quote(quote)
diff --git a/pkg/tpl/quote_templ.go b/pkg/tpl/quote_templ.go
index fa139cd..cc799cc 100644
--- a/pkg/tpl/quote_templ.go
+++ b/pkg/tpl/quote_templ.go
@@ -10,6 +10,8 @@ import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
+ "sh.org.ru/pkg/config"
+ "sh.org.ru/pkg/middleware"
"sh.org.ru/pkg/model"
"strconv"
)
@@ -35,6 +37,7 @@ func Quote(quote *model.Quote) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
+ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<article><header><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -51,7 +54,7 @@ func Quote(quote *model.Quote) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(quote.ID)))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/quote.templ`, Line: 12, Col: 91}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/quote.templ`, Line: 15, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -64,7 +67,7 @@ func Quote(quote *model.Quote) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(quote.CreatedAt.Format("02.01.06"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/quote.templ`, Line: 13, Col: 92}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/quote.templ`, Line: 16, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -78,11 +81,29 @@ func Quote(quote *model.Quote) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer><span><a target=\"_blank\" href=\"")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var5 templ.SafeURL = templ.URL(fmt.Sprintf("https://t.me/share/url?url=https://sh.org.ru/quote/%d", quote.ID))
+ templ_7745c5c3_Err = Rate(quote, 0).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if quote.Archive {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<abbr title=\"Цитата из старого цитатника\">Архив</abbr> ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a target=\"_blank\" href=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 templ.SafeURL = templ.URL(fmt.Sprintf("https://t.me/share/url?url=%s/quote/%d", host, quote.ID))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -91,7 +112,7 @@ func Quote(quote *model.Quote) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var6 templ.SafeURL = templ.URL(fmt.Sprintf("https://vk.com/share.php?url=https://sh.org.ru/quote/%d", quote.ID))
+ var templ_7745c5c3_Var6 templ.SafeURL = templ.URL(fmt.Sprintf("https://vk.com/share.php?url=%s/quote/%d", host, quote.ID))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -100,22 +121,12 @@ func Quote(quote *model.Quote) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var7 templ.SafeURL = templ.URL(fmt.Sprintf("https://connect.ok.ru/offer?url=https://sh.org.ru/quote/%d", quote.ID))
+ var templ_7745c5c3_Var7 templ.SafeURL = templ.URL(fmt.Sprintf("https://connect.ok.ru/offer?url=%s/quote/%d", host, quote.ID))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><i class=\"fa fa-odnoklassniki-square\" aria-hidden=\"true\"></i></a></span> <span>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if quote.Archive {
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<abbr title=\"Цитата из старого цитатника\">Архив</abbr>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></footer></article>")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><i class=\"fa fa-odnoklassniki-square\" aria-hidden=\"true\"></i></a></span></footer></article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -144,6 +155,7 @@ func QuotePage(quote *model.Quote) templ.Component {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
+ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host
templ_7745c5c3_Var9 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
@@ -164,7 +176,7 @@ func QuotePage(quote *model.Quote) templ.Component {
})
templ_7745c5c3_Err = Layout(HeaderParams{
Title: "Цитата #" + strconv.Itoa(int(quote.ID)),
- URL: fmt.Sprintf("https://sh.org.ru/quote/%d", quote.ID),
+ URL: fmt.Sprintf("%s/quote/%d", host, quote.ID),
Description: templ.EscapeString(quote.Quote),
}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
diff --git a/pkg/tpl/random.templ b/pkg/tpl/random.templ
index 39ce3ff..2d25a03 100644
--- a/pkg/tpl/random.templ
+++ b/pkg/tpl/random.templ
@@ -1,12 +1,33 @@
package tpl
import (
+ "sh.org.ru/pkg/config"
+ "sh.org.ru/pkg/middleware"
"sh.org.ru/pkg/model"
)
templ Random(quotes []model.Quote) {
- for _, q := range quotes {
- @Quote(&q)
+ {{ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host }}
+ @Layout(HeaderParams{
+ Title: "Цитатник Рунета",
+ Description: "Новый цитатник Рунета",
+ URL: host,
+ }) {
+ <div id="random">
+ for _, q := range quotes {
+ @Quote(&q)
+ }
+ <a
+ role="button"
+ hx-get="/random"
+ hx-swap="outerHTML"
+ hx-select="#random"
+ hx-target="#random"
+ hx-indicator="#loader"
+ >
+ Загрузить ещё...
+ </a>
+ <span aria-busy="true" id="loader" class="htmx-indicator">Загрузка...</span>
+ </div>
}
- <a role="button" hx-get="/random" hx-swap="outerHTML">Загрузить ещё...</a>
}
diff --git a/pkg/tpl/random_templ.go b/pkg/tpl/random_templ.go
index 79f5685..803bf22 100644
--- a/pkg/tpl/random_templ.go
+++ b/pkg/tpl/random_templ.go
@@ -9,6 +9,8 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
+ "sh.org.ru/pkg/config"
+ "sh.org.ru/pkg/middleware"
"sh.org.ru/pkg/model"
)
@@ -33,13 +35,40 @@ func Random(quotes []model.Quote) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- for _, q := range quotes {
- templ_7745c5c3_Err = Quote(&q).Render(ctx, templ_7745c5c3_Buffer)
+ host := ctx.Value(middleware.ContextKey("config")).(*config.Config).Host
+ templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"random\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a role=\"button\" hx-get=\"/random\" hx-swap=\"outerHTML\">Загрузить ещё...</a>")
+ for _, q := range quotes {
+ templ_7745c5c3_Err = Quote(&q).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a role=\"button\" hx-get=\"/random\" hx-swap=\"outerHTML\" hx-select=\"#random\" hx-target=\"#random\" hx-indicator=\"#loader\">Загрузить ещё...</a> <span aria-busy=\"true\" id=\"loader\" class=\"htmx-indicator\">Загрузка...</span></div>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = Layout(HeaderParams{
+ Title: "Цитатник Рунета",
+ Description: "Новый цитатник Рунета",
+ URL: host,
+ }).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/pkg/tpl/rate.templ b/pkg/tpl/rate.templ
new file mode 100644
index 0000000..bd63df8
--- /dev/null
+++ b/pkg/tpl/rate.templ
@@ -0,0 +1,31 @@
+package tpl
+
+import (
+ "fmt"
+ "sh.org.ru/pkg/model"
+ "strconv"
+)
+
+templ Rate(quote *model.Quote, act int) {
+ <nav class="rate">
+ <a
+ hx-post={ fmt.Sprintf("/rate/%d", quote.ID) }
+ hx-target="closest .rate"
+ hx-vals='{"vote": "up"}'
+ href="#"
+ >
+ <i class="fa fa-plus"></i>
+ </a>
+ &nbsp;
+ { strconv.Itoa(quote.Rating) }
+ &nbsp;
+ <a
+ hx-post={ fmt.Sprintf("/rate/%d", quote.ID) }
+ hx-target="closest .rate"
+ hx-vals='{"vote": "down"}'
+ href="#"
+ >
+ <i class="fa fa-minus"></i>
+ </a>
+ </nav>
+}
diff --git a/pkg/tpl/rate_templ.go b/pkg/tpl/rate_templ.go
new file mode 100644
index 0000000..6c27477
--- /dev/null
+++ b/pkg/tpl/rate_templ.go
@@ -0,0 +1,85 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.778
+package tpl
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+ "fmt"
+ "sh.org.ru/pkg/model"
+ "strconv"
+)
+
+func Rate(quote *model.Quote, act int) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"rate\"><a hx-post=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/rate/%d", quote.ID))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/rate.templ`, Line: 12, Col: 46}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"closest .rate\" hx-vals=\"{&#34;vote&#34;: &#34;up&#34;}\" href=\"#\"><i class=\"fa fa-plus\"></i></a> &nbsp; ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(quote.Rating))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/rate.templ`, Line: 20, Col: 30}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" &nbsp; <a hx-post=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/rate/%d", quote.ID))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/tpl/rate.templ`, Line: 23, Col: 46}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-target=\"closest .rate\" hx-vals=\"{&#34;vote&#34;: &#34;down&#34;}\" href=\"#\"><i class=\"fa fa-minus\"></i></a></nav>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+var _ = templruntime.GeneratedTemplate