diff options
author | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-10-08 03:43:08 +0300 |
---|---|---|
committer | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-10-08 03:50:53 +0300 |
commit | e849e705c30cceec3cf7336a21bed96c8a911e90 (patch) | |
tree | 93f559bcd4cf3e53193930d112e564a2b7462ac8 /pkg | |
parent | 3ee654f6fb3cdf119630bfba8066c96ec26428c3 (diff) |
Добавил рейтинг
Добавил страницу топа
Добавил rss/xml/json feed
Diffstat (limited to 'pkg')
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(), "es) 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(), "es) + 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(), "es) + 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(), "es) + 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(), "es) - 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> Обновить капчу + </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> Обновить капчу</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)) }>←</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> ·  - <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> ·  - <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> ·  + <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> ·  + <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> + + { strconv.Itoa(quote.Rating) } + + <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=\"{"vote": "up"}\" href=\"#\"><i class=\"fa fa-plus\"></i></a> ") + 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(" <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=\"{"vote": "down"}\" 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 |