aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAKP <tom@tdpain.net>2022-04-02 21:10:52 +0100
committerAKP <tom@tdpain.net>2022-04-02 21:10:52 +0100
commit32ba36903f66d7613534f49df81fd834ba1c5925 (patch)
tree735836a8cc2a19ab475c6164856a92b2ce4d0b09
parent02e7f1d8cec640c3696c33a711480490f2f6aba0 (diff)
Implement schedule updates
Signed-off-by: AKP <tom@tdpain.net>
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--walrss/internal/core/sessions.go5
-rw-r--r--walrss/internal/core/users.go25
-rw-r--r--walrss/internal/db/db.go29
-rw-r--r--walrss/internal/db/sendDay.go80
-rw-r--r--walrss/internal/http/auth.go8
-rw-r--r--walrss/internal/http/edit.go74
-rw-r--r--walrss/internal/http/http.go37
-rw-r--r--walrss/internal/http/mainpage.go25
-rw-r--r--walrss/internal/http/views/layoutComponents.qtpl.html2
-rw-r--r--walrss/internal/http/views/layoutComponents.qtpl.html.go2
-rw-r--r--walrss/internal/http/views/main.qtpl.html137
-rw-r--r--walrss/internal/http/views/main.qtpl.html.go235
-rw-r--r--walrss/internal/http/views/page.qtpl.html17
-rw-r--r--walrss/internal/http/views/page.qtpl.html.go18
-rw-r--r--walrss/internal/urls/urls.go6
17 files changed, 675 insertions, 28 deletions
diff --git a/go.mod b/go.mod
index bc60677..6a3149e 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/rs/zerolog v1.26.1 // indirect
+ github.com/stevelacy/daz v0.1.4 // indirect
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.34.0 // indirect
diff --git a/go.sum b/go.sum
index 77abf86..9faeebd 100644
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
+github.com/stevelacy/daz v0.1.4 h1:ugmff/D7D764wZjXSgSryEINE/bi+Xddllw3JQQGbWk=
+github.com/stevelacy/daz v0.1.4/go.mod h1:AbK6DzjiIL15r4bQtcFvOBAvDGMXoh+uIG26NRUugt0=
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a h1:oIi7H/bwFUYKYhzKbHc+3MvHRWqhQwXVB4LweLMiVy0=
github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a/go.mod h1:iSvujNDmpZ6eQX+bg/0X3lF7LEmZ8N77g2a/J/+Zt2U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
diff --git a/walrss/internal/core/sessions.go b/walrss/internal/core/sessions.go
index 8e1ab73..6beb13b 100644
--- a/walrss/internal/core/sessions.go
+++ b/walrss/internal/core/sessions.go
@@ -4,6 +4,7 @@ import (
"crypto/rand"
"encoding/hex"
goalone "github.com/bwmarrin/go-alone"
+ "strings"
"time"
)
@@ -21,7 +22,7 @@ func init() {
}
func GenerateSessionToken(userID string) string {
- combined := combineStringAndSalt(userID, sessionSalt)
+ combined := append([]byte(userID), sessionSalt...)
return hex.EncodeToString(sessionSigner.Sign(combined))
}
@@ -36,5 +37,5 @@ func ValidateSessionToken(input string) (string, time.Time, error) {
}
parsed := sessionSigner.Parse(signed)
- return string(parsed.Payload), parsed.Timestamp, nil
+ return strings.TrimSuffix(string(parsed.Payload), string(sessionSalt)), parsed.Timestamp, nil
}
diff --git a/walrss/internal/core/users.go b/walrss/internal/core/users.go
index 2343de7..e10da99 100644
--- a/walrss/internal/core/users.go
+++ b/walrss/internal/core/users.go
@@ -57,9 +57,9 @@ func AreUserCredentialsCorrect(st *state.State, email, password string) (bool, e
return true, nil
}
-func GetUserByEmail(st *state.State, userID string) (*db.User, error) {
+func GetUserByID(st *state.State, userID string) (*db.User, error) {
user := new(db.User)
- if err := st.Data.FindOne(user, bh.Where("Email").Eq(userID)); err != nil {
+ if err := st.Data.FindOne(user, bh.Where("ID").Eq(userID)); err != nil {
if errors.Is(err, bh.ErrNotFound) {
return nil, ErrNotFound
}
@@ -67,3 +67,24 @@ func GetUserByEmail(st *state.State, userID string) (*db.User, error) {
}
return user, nil
}
+
+func GetUserByEmail(st *state.State, email string) (*db.User, error) {
+ user := new(db.User)
+ if err := st.Data.FindOne(user, bh.Where("Email").Eq(email)); err != nil {
+ if errors.Is(err, bh.ErrNotFound) {
+ return nil, ErrNotFound
+ }
+ return nil, err
+ }
+ return user, nil
+}
+
+func UpdateUser(st *state.State, user *db.User) error {
+ if err := st.Data.Update(user.ID, user); err != nil {
+ if errors.Is(err, bh.ErrNotFound) {
+ return ErrNotFound
+ }
+ return err
+ }
+ return nil
+}
diff --git a/walrss/internal/db/db.go b/walrss/internal/db/db.go
index d9769e7..eb8590a 100644
--- a/walrss/internal/db/db.go
+++ b/walrss/internal/db/db.go
@@ -1,9 +1,15 @@
package db
-import bh "github.com/timshannon/bolthold"
+import (
+ "encoding/json"
+ bh "github.com/timshannon/bolthold"
+)
func New(filename string) (*bh.Store, error) {
- store, err := bh.Open(filename, 0644, nil)
+ store, err := bh.Open(filename, 0644, &bh.Options{
+ Encoder: json.Marshal,
+ Decoder: json.Unmarshal,
+ })
if err != nil {
return nil, err
}
@@ -17,21 +23,8 @@ type User struct {
Salt []byte
Schedule struct {
- Day SendDay `boltholdIndex:"Day"`
- Hour int `boltholdIndex:"Hour"`
+ Active bool `boltholdIndex:"Active"`
+ Day SendDay `boltholdIndex:"Day"`
+ Hour int `boltholdIndex:"Hour"`
}
}
-
-type SendDay uint32
-
-const (
- SendDayNever = iota
- SendDaily
- SendOnMonday
- SendOnTuesday
- SendOnWednesday
- SendOnThursday
- SendOnFriday
- SendOnSaturday
- SendOnSunday
-)
diff --git a/walrss/internal/db/sendDay.go b/walrss/internal/db/sendDay.go
new file mode 100644
index 0000000..5146a56
--- /dev/null
+++ b/walrss/internal/db/sendDay.go
@@ -0,0 +1,80 @@
+package db
+
+import (
+ "errors"
+ "strings"
+)
+
+type SendDay uint32
+
+const (
+ SendDayNever SendDay = iota
+ SendDaily
+ SendOnMonday
+ SendOnTuesday
+ SendOnWednesday
+ SendOnThursday
+ SendOnFriday
+ SendOnSaturday
+ SendOnSunday
+ LastSendDay
+)
+
+func (s SendDay) String() string {
+ var x string
+
+ switch s {
+ case SendDayNever:
+ x = "never"
+ case SendDaily:
+ x = "daily"
+ case SendOnMonday:
+ x = "Monday"
+ case SendOnTuesday:
+ x = "Tuesday"
+ case SendOnWednesday:
+ x = "Wednesday"
+ case SendOnThursday:
+ x = "Thursday"
+ case SendOnFriday:
+ x = "Friday"
+ case SendOnSaturday:
+ x = "Saturday"
+ case SendOnSunday:
+ x = "Sunday"
+ }
+
+ return x
+}
+
+func (s SendDay) MarshalText() ([]byte, error) {
+ return []byte(s.String()), nil
+}
+
+func (s *SendDay) UnmarshalText(x []byte) error {
+
+ switch strings.ToLower(string(x)) {
+ case "never":
+ *s = SendDayNever
+ case "daily":
+ *s = SendDaily
+ case "monday":
+ *s = SendOnMonday
+ case "tuesday":
+ *s = SendOnTuesday
+ case "wednesday":
+ *s = SendOnWednesday
+ case "thursday":
+ *s = SendOnThursday
+ case "friday":
+ *s = SendOnFriday
+ case "saturday":
+ *s = SendOnSaturday
+ case "sunday":
+ *s = SendOnSunday
+ default:
+ return errors.New("unrecognised day")
+ }
+
+ return nil
+}
diff --git a/walrss/internal/http/auth.go b/walrss/internal/http/auth.go
index 62e4295..ae917e6 100644
--- a/walrss/internal/http/auth.go
+++ b/walrss/internal/http/auth.go
@@ -58,7 +58,9 @@ success:
}
func (s *Server) authSignIn(ctx *fiber.Ctx) error {
- page := &views.SignInPage{}
+ page := &views.SignInPage{
+ Problem: ctx.Query("problem"),
+ }
if getCurrentUserID(ctx) != "" {
goto success
@@ -103,7 +105,9 @@ func (s *Server) authSignIn(ctx *fiber.Ctx) error {
return views.SendPage(ctx, page)
success:
- return ctx.Redirect(urls.Index)
+ return ctx.Redirect(
+ ctx.Query("next", urls.Index),
+ )
incorrectUsernameOrPassword:
ctx.Status(fiber.StatusUnauthorized)
return views.SendPage(ctx, &views.SignInPage{Problem: "Incorrect username or password"})
diff --git a/walrss/internal/http/edit.go b/walrss/internal/http/edit.go
new file mode 100644
index 0000000..8599f07
--- /dev/null
+++ b/walrss/internal/http/edit.go
@@ -0,0 +1,74 @@
+package http
+
+import (
+ "github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/codemicro/walrss/walrss/internal/db"
+ "github.com/codemicro/walrss/walrss/internal/urls"
+ "github.com/gofiber/fiber/v2"
+ "strconv"
+ "strings"
+)
+
+func (s *Server) editEnabledState(ctx *fiber.Ctx) error {
+ currentUserID := getCurrentUserID(ctx)
+ if currentUserID == "" {
+ return requestStandardSignIn(ctx)
+ }
+
+ user, err := core.GetUserByID(s.state, currentUserID)
+ if err != nil {
+ return err
+ }
+
+ if strings.ToLower(ctx.FormValue("enable", "off")) == "on" {
+ user.Schedule.Active = true
+ } else {
+ user.Schedule.Active = false
+ }
+
+ if err := core.UpdateUser(s.state, user); err != nil {
+ return err
+ }
+
+ ctx.Set("HX-Redirect", urls.Index)
+ return nil
+}
+
+func (s *Server) editTimings(ctx *fiber.Ctx) error {
+ currentUserID := getCurrentUserID(ctx)
+ if currentUserID == "" {
+ return requestStandardSignIn(ctx)
+ }
+
+ user, err := core.GetUserByID(s.state, currentUserID)
+ if err != nil {
+ return err
+ }
+
+ if n, err := strconv.ParseInt(ctx.FormValue("day"), 10, 32); err != nil {
+ return core.AsUserError(fiber.StatusBadRequest, err)
+ } else {
+ x := db.SendDay(n)
+ if x > db.SendOnSunday || x < 0 {
+ return core.NewUserError("invalid day: out of range 0<=x<=%d", int(db.SendOnSunday))
+ }
+ user.Schedule.Day = x
+ }
+
+ if n, err := strconv.ParseInt(ctx.FormValue("time"), 10, 8); err != nil {
+ return core.AsUserError(fiber.StatusBadRequest, err)
+ } else {
+ x := int(n)
+ if x > 23 || x < 0 {
+ return core.NewUserError("invalid time: out of range 0<=x<=23")
+ }
+ user.Schedule.Hour = x
+ }
+
+ if err := core.UpdateUser(s.state, user); err != nil {
+ return err
+ }
+
+ ctx.Set("HX-Redirect", urls.Index)
+ return nil
+}
diff --git a/walrss/internal/http/http.go b/walrss/internal/http/http.go
index 037e310..cf0589e 100644
--- a/walrss/internal/http/http.go
+++ b/walrss/internal/http/http.go
@@ -2,10 +2,13 @@ package http
import (
"github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/codemicro/walrss/walrss/internal/http/views"
"github.com/codemicro/walrss/walrss/internal/state"
"github.com/codemicro/walrss/walrss/internal/urls"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
+ "github.com/stevelacy/daz"
+ "net/url"
"time"
)
@@ -68,11 +71,16 @@ func (s *Server) registerHandlers() {
return ctx.Next()
})
+ s.app.Get(urls.Index, s.mainPage)
+
s.app.Get(urls.AuthRegister, s.authRegister)
s.app.Post(urls.AuthRegister, s.authRegister)
s.app.Get(urls.AuthSignIn, s.authSignIn)
s.app.Post(urls.AuthSignIn, s.authSignIn)
+
+ s.app.Put(urls.EditEnabledState, s.editEnabledState)
+ s.app.Put(urls.EditTimings, s.editTimings)
}
func (s *Server) Run() error {
@@ -93,3 +101,32 @@ func getCurrentUserID(ctx *fiber.Ctx) string {
}
return ""
}
+
+func requestStandardSignIn(ctx *fiber.Ctx) error {
+ rdu := ctx.OriginalURL() // TODO: Could this use of OriginalURL be insecure?
+
+ queryParams := make(url.Values)
+ queryParams.Add("problem", "Please sign in first.")
+ queryParams.Add("next", rdu)
+ nextURL := urls.AuthSignIn + "?" + queryParams.Encode()
+
+ ctx.Status(fiber.StatusUnauthorized)
+
+ // Instead of plainly redirecting, we use a HTML redirect here. This is to clear the HTTP verb used for this
+ // request. For example - if the request was made with DELETE, using ctx.Redirect will preserve that verb. Using
+ // this method will restart with a GET verb.
+ return views.SendPage(ctx, &views.PolyPage{
+ TitleString: "Please sign in first",
+ BodyContent: daz.H("p", "Please sign in first. If your browser doesn't automatically redirect you, click ", daz.H("a", daz.Attr{"href": nextURL}, "here"), ".")(),
+ ExtraHeadContent: daz.H("meta", daz.Attr{"http-equiv": "Refresh", "content": "0; " + nextURL})(),
+ })
+}
+
+func requestFragmentSignIn(ctx *fiber.Ctx, nextURL string) error {
+ queryParams := make(url.Values)
+ queryParams.Add("problem", "Please sign in first.")
+ queryParams.Add("next", nextURL)
+
+ ctx.Set("HX-Redirect", urls.AuthSignIn+"?"+queryParams.Encode())
+ return nil
+}
diff --git a/walrss/internal/http/mainpage.go b/walrss/internal/http/mainpage.go
new file mode 100644
index 0000000..6c60753
--- /dev/null
+++ b/walrss/internal/http/mainpage.go
@@ -0,0 +1,25 @@
+package http
+
+import (
+ "github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/codemicro/walrss/walrss/internal/http/views"
+ "github.com/gofiber/fiber/v2"
+)
+
+func (s *Server) mainPage(ctx *fiber.Ctx) error {
+ currentUserID := getCurrentUserID(ctx)
+ if currentUserID == "" {
+ return requestStandardSignIn(ctx)
+ }
+
+ user, err := core.GetUserByID(s.state, currentUserID)
+ if err != nil {
+ return err
+ }
+
+ return views.SendPage(ctx, &views.MainPage{
+ EnableDigests: user.Schedule.Active,
+ SelectedDay: user.Schedule.Day,
+ SelectedTime: user.Schedule.Hour,
+ })
+}
diff --git a/walrss/internal/http/views/layoutComponents.qtpl.html b/walrss/internal/http/views/layoutComponents.qtpl.html
index bc3ef1c..4643094 100644
--- a/walrss/internal/http/views/layoutComponents.qtpl.html
+++ b/walrss/internal/http/views/layoutComponents.qtpl.html
@@ -1,7 +1,7 @@
{% import "github.com/codemicro/walrss/walrss/internal/urls" %}
{% func navbar() %}
-<nav class="navbar navbar-light bg-light mb-3">
+<nav class="navbar navbar-light mb-3" style="background-color: #EFCB68">
<div class="container-fluid">
<a class="navbar-brand" href="{%s= urls.Index %}">Walrss</a>
</div>
diff --git a/walrss/internal/http/views/layoutComponents.qtpl.html.go b/walrss/internal/http/views/layoutComponents.qtpl.html.go
index 28af874..24f2918 100644
--- a/walrss/internal/http/views/layoutComponents.qtpl.html.go
+++ b/walrss/internal/http/views/layoutComponents.qtpl.html.go
@@ -18,7 +18,7 @@ var (
func streamnavbar(qw422016 *qt422016.Writer) {
qw422016.N().S(`
-<nav class="navbar navbar-light bg-light mb-3">
+<nav class="navbar navbar-light mb-3" style="background-color: #EFCB68">
<div class="container-fluid">
<a class="navbar-brand" href="`)
qw422016.N().S(urls.Index)
diff --git a/walrss/internal/http/views/main.qtpl.html b/walrss/internal/http/views/main.qtpl.html
new file mode 100644
index 0000000..65272ab
--- /dev/null
+++ b/walrss/internal/http/views/main.qtpl.html
@@ -0,0 +1,137 @@
+{% import "github.com/codemicro/walrss/walrss/internal/db" %}
+{% import "github.com/codemicro/walrss/walrss/internal/urls" %}
+
+{% code type MainPage struct {
+ BasePage
+ EnableDigests bool
+ SelectedDay db.SendDay
+ SelectedTime int
+} %}
+
+{% func (p *MainPage) Title() %}{% endfunc %}
+{% func (p *MainPage) Body() %}
+{%= navbar() %}
+
+<div class="container">
+ <h1>My settings</h1>
+
+ <div class="card mt-3">
+ <div class="card-header">
+ Schedule
+ </div>
+ <div class="card-body">
+
+ <div class="mb-2">
+ <input
+ type="checkbox"
+ id="enableCheckbox"
+ name="enable"
+ {% if p.EnableDigests %}checked{% endif %}
+ hx-put="{%s= urls.EditEnabledState %}"
+ hx-indicator="#enableCheckboxIndicator"
+ >
+ <label for="enableCheckbox">Enable digest delivery</label>
+ <i class="ml-2 iconLoading htmx-indicator" id="enableCheckboxIndicator"></i>
+ </div>
+
+ <form
+ class="row row-cols-lg-auto g-3 align-items-center"
+ hx-put="{%s urls.EditTimings %}"
+ hx-indicator="#submitScheduleIndicator"
+ >
+ <div class="col-12">
+ Deliver my digests
+ </div>
+
+ <div class="col-12">
+ <label class="visually-hidden" for="daySelection">Day of week</label>
+ <select
+ class="form-select"
+ id="daySelection"
+ name="day"
+ {% if !p.EnableDigests %}disabled{% endif %}
+ >
+ {% for i := db.SendDaily; i <= db.SendOnSunday; i += 1 %}
+ <option
+ value="{%d int(i) %}"
+ {% if p.SelectedDay == i %}selected{% endif %}
+ >
+ {% if i != db.SendDaily %}on {% endif %}{%s i.String() %}
+ </option>
+ {% endfor %}
+ </select>
+ </div>
+
+ <div class="col-12">at</div>
+
+ <div class="col-12">
+ <label class="visually-hidden" for="timeSelection">Time of day</label>
+ <select
+ class="form-select"
+ id="timeSelection"
+ name="time"
+ {% if !p.EnableDigests %}disabled{% endif %}
+ >
+ {% for i := 0; i <= 23; i += 1 %}
+ <option
+ value="{%d i %}"
+ {% if p.SelectedTime == i %}selected{% endif %}
+ >
+ {%d i %}:00
+ </option>
+ {% endfor %}
+ </select>
+ </div>
+
+ <div class="col-12">UTC</div>
+
+ <div class="col-12">
+ <button type="submit" class="btn btn-primary" {% if !p.EnableDigests %}disabled{% endif %}>Save</button>
+ <i class="iconLoading align-middle htmx-indicator" style="margin-left: 1rem; width: 2rem;" id="submitScheduleIndicator"></i>
+ </div>
+ </form>
+
+ </div>
+ </div>
+
+ <div class="card mt-3">
+ <div class="card-header">
+ Feeds
+ </div>
+ <div class="card-body">
+
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col">#</th>
+ <th scope="col">First</th>
+ <th scope="col">Last</th>
+ <th scope="col">Handle</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th scope="row">1</th>
+ <td>Mark</td>
+ <td>Otto</td>
+ <td>@mdo</td>
+ </tr>
+ <tr>
+ <th scope="row">2</th>
+ <td>Jacob</td>
+ <td>Thornton</td>
+ <td>@fat</td>
+ </tr>
+ <tr>
+ <th scope="row">3</th>
+ <td colspan="2">Larry the Bird</td>
+ <td>@twitter</td>
+ </tr>
+ </tbody>
+ </table>
+
+ </div>
+ </div>
+
+</div>
+{% endfunc %} \ No newline at end of file
diff --git a/walrss/internal/http/views/main.qtpl.html.go b/walrss/internal/http/views/main.qtpl.html.go
new file mode 100644
index 0000000..a6c7b1b
--- /dev/null
+++ b/walrss/internal/http/views/main.qtpl.html.go
@@ -0,0 +1,235 @@
+// Code generated by qtc from "main.qtpl.html". DO NOT EDIT.
+// See https://github.com/valyala/quicktemplate for details.
+
+package views
+
+import "github.com/codemicro/walrss/walrss/internal/db"
+
+import "github.com/codemicro/walrss/walrss/internal/urls"
+
+import (
+ qtio422016 "io"
+
+ qt422016 "github.com/valyala/quicktemplate"
+)
+
+var (
+ _ = qtio422016.Copy
+ _ = qt422016.AcquireByteBuffer
+)
+
+type MainPage struct {
+ BasePage
+ EnableDigests bool
+ SelectedDay db.SendDay
+ SelectedTime int
+}
+
+func (p *MainPage) StreamTitle(qw422016 *qt422016.Writer) {
+}
+
+func (p *MainPage) WriteTitle(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamTitle(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *MainPage) Title() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteTitle(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func (p *MainPage) StreamBody(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`
+`)
+ streamnavbar(qw422016)
+ qw422016.N().S(`
+
+<div class="container">
+ <h1>My settings</h1>
+
+ <div class="card mt-3">
+ <div class="card-header">
+ Schedule
+ </div>
+ <div class="card-body">
+
+ <div class="mb-2">
+ <input
+ type="checkbox"
+ id="enableCheckbox"
+ name="enable"
+ `)
+ if p.EnableDigests {
+ qw422016.N().S(`checked`)
+ }
+ qw422016.N().S(`
+ hx-put="`)
+ qw422016.N().S(urls.EditEnabledState)
+ qw422016.N().S(`"
+ hx-indicator="#enableCheckboxIndicator"
+ >
+ <label for="enableCheckbox">Enable digest delivery</label>
+ <i class="ml-2 iconLoading htmx-indicator" id="enableCheckboxIndicator"></i>
+ </div>
+
+ <form
+ class="row row-cols-lg-auto g-3 align-items-center"
+ hx-put="`)
+ qw422016.E().S(urls.EditTimings)
+ qw422016.N().S(`"
+ hx-indicator="#submitScheduleIndicator"
+ >
+ <div class="col-12">
+ Deliver my digests
+ </div>
+
+ <div class="col-12">
+ <label class="visually-hidden" for="daySelection">Day of week</label>
+ <select
+ class="form-select"
+ id="daySelection"
+ name="day"
+ `)
+ if !p.EnableDigests {
+ qw422016.N().S(`disabled`)
+ }
+ qw422016.N().S(`
+ >
+ `)
+ for i := db.SendDaily; i <= db.SendOnSunday; i += 1 {
+ qw422016.N().S(`
+ <option
+ value="`)
+ qw422016.N().D(int(i))
+ qw422016.N().S(`"
+ `)
+ if p.SelectedDay == i {
+ qw422016.N().S(`selected`)
+ }
+ qw422016.N().S(`
+ >
+ `)
+ if i != db.SendDaily {
+ qw422016.N().S(`on `)
+ }
+ qw422016.E().S(i.String())
+ qw422016.N().S(`
+ </option>
+ `)
+ }
+ qw422016.N().S(`
+ </select>
+ </div>
+
+ <div class="col-12">at</div>
+
+ <div class="col-12">
+ <label class="visually-hidden" for="timeSelection">Time of day</label>
+ <select
+ class="form-select"
+ id="timeSelection"
+ name="time"
+ `)
+ if !p.EnableDigests {
+ qw422016.N().S(`disabled`)
+ }
+ qw422016.N().S(`
+ >
+ `)
+ for i := 0; i <= 23; i += 1 {
+ qw422016.N().S(`
+ <option
+ value="`)
+ qw422016.N().D(i)
+ qw422016.N().S(`"
+ `)
+ if p.SelectedTime == i {
+ qw422016.N().S(`selected`)
+ }
+ qw422016.N().S(`
+ >
+ `)
+ qw422016.N().D(i)
+ qw422016.N().S(`:00
+ </option>
+ `)
+ }
+ qw422016.N().S(`
+ </select>
+ </div>
+
+ <div class="col-12">UTC</div>
+
+ <div class="col-12">
+ <button type="submit" class="btn btn-primary" `)
+ if !p.EnableDigests {
+ qw422016.N().S(`disabled`)
+ }
+ qw422016.N().S(`>Save</button>
+ <i class="iconLoading align-middle htmx-indicator" style="margin-left: 1rem; width: 2rem;" id="submitScheduleIndicator"></i>
+ </div>
+ </form>
+
+ </div>
+ </div>
+
+ <div class="card mt-3">
+ <div class="card-header">
+ Feeds
+ </div>
+ <div class="card-body">
+
+ <table class="table">
+ <thead>
+ <tr>
+ <th scope="col">#</th>
+ <th scope="col">First</th>
+ <th scope="col">Last</th>
+ <th scope="col">Handle</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th scope="row">1</th>
+ <td>Mark</td>
+ <td>Otto</td>
+ <td>@mdo</td>
+ </tr>
+ <tr>
+ <th scope="row">2</th>
+ <td>Jacob</td>
+ <td>Thornton</td>
+ <td>@fat</td>
+ </tr>
+ <tr>
+ <th scope="row">3</th>
+ <td colspan="2">Larry the Bird</td>
+ <td>@twitter</td>
+ </tr>
+ </tbody>
+ </table>
+
+ </div>
+ </div>
+
+</div>
+`)
+}
+
+func (p *MainPage) WriteBody(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ p.StreamBody(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func (p *MainPage) Body() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ p.WriteBody(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
diff --git a/walrss/internal/http/views/page.qtpl.html b/walrss/internal/http/views/page.qtpl.html
index 968ea7d..f885de9 100644
--- a/walrss/internal/http/views/page.qtpl.html
+++ b/walrss/internal/http/views/page.qtpl.html
@@ -13,7 +13,22 @@ Page prints a page implementing Page interface.
<head>
<title>{%s= makePageTitle(p) %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <script src="https://unpkg.com/htmx.org@1.7.0"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+ <style>
+ [disabled] {
+ cursor: not-allowed;
+ }
+
+ i.iconLoading {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='64px' height='64px' viewBox='0 0 128 128' xml:space='preserve'%3E%3Cg%3E%3Cpath d='M75.4 126.63a11.43 11.43 0 0 1-2.1-22.65 40.9 40.9 0 0 0 30.5-30.6 11.4 11.4 0 1 1 22.27 4.87h.02a63.77 63.77 0 0 1-47.8 48.05v-.02a11.38 11.38 0 0 1-2.93.37z' fill='%23000000'/%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'%3E%3C/animateTransform%3E%3C/g%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-size: cover;
+ display: inline-block;
+ width: 1rem;
+ aspect-ratio: 1/1;
+ }
+ </style>
{%= p.HeadContent() %}
</head>
<body>
@@ -55,5 +70,5 @@ type PolyPage struct {
%}
{% func (p *PolyPage) Title() %}{%s= p.TitleString %}{% endfunc %}
-{% func (p *PolyPage) Body() %}{%s= p.BodyContent %}{% endfunc %}
+{% func (p *PolyPage) Body() %}{%= navbar() %}<div class="container">{%s= p.BodyContent %}</div>{% endfunc %}
{% func (p *PolyPage) HeadContent() %}{%s= p.ExtraHeadContent %}{% endfunc %} \ No newline at end of file
diff --git a/walrss/internal/http/views/page.qtpl.html.go b/walrss/internal/http/views/page.qtpl.html.go
index 125a3b0..4d53006 100644
--- a/walrss/internal/http/views/page.qtpl.html.go
+++ b/walrss/internal/http/views/page.qtpl.html.go
@@ -37,7 +37,22 @@ func StreamRenderPage(qw422016 *qt422016.Writer, p Page) {
qw422016.N().S(makePageTitle(p))
qw422016.N().S(`</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <script src="https://unpkg.com/htmx.org@1.7.0"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+ <style>
+ [disabled] {
+ cursor: not-allowed;
+ }
+
+ i.iconLoading {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='64px' height='64px' viewBox='0 0 128 128' xml:space='preserve'%3E%3Cg%3E%3Cpath d='M75.4 126.63a11.43 11.43 0 0 1-2.1-22.65 40.9 40.9 0 0 0 30.5-30.6 11.4 11.4 0 1 1 22.27 4.87h.02a63.77 63.77 0 0 1-47.8 48.05v-.02a11.38 11.38 0 0 1-2.93.37z' fill='%23000000'/%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'%3E%3C/animateTransform%3E%3C/g%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-size: cover;
+ display: inline-block;
+ width: 1rem;
+ aspect-ratio: 1/1;
+ }
+ </style>
`)
p.StreamHeadContent(qw422016)
qw422016.N().S(`
@@ -201,7 +216,10 @@ func (p *PolyPage) Title() string {
}
func (p *PolyPage) StreamBody(qw422016 *qt422016.Writer) {
+ streamnavbar(qw422016)
+ qw422016.N().S(`<div class="container">`)
qw422016.N().S(p.BodyContent)
+ qw422016.N().S(`</div>`)
}
func (p *PolyPage) WriteBody(qq422016 qtio422016.Writer) {
diff --git a/walrss/internal/urls/urls.go b/walrss/internal/urls/urls.go
index 1d38f96..bd3d878 100644
--- a/walrss/internal/urls/urls.go
+++ b/walrss/internal/urls/urls.go
@@ -6,4 +6,8 @@ const (
Auth = "/auth"
AuthSignIn = Auth + "/signin"
AuthRegister = Auth + "/register"
-) \ No newline at end of file
+
+ Edit = "/edit"
+ EditEnabledState = Edit + "/enabled"
+ EditTimings = Edit + "/timings"
+)