aboutsummaryrefslogtreecommitdiffstats
path: root/walrss
diff options
context:
space:
mode:
authorAKP <tom@tdpain.net>2022-04-03 20:59:21 +0100
committerAKP <tom@tdpain.net>2022-04-03 20:59:21 +0100
commitc85c530bc13c1e43af08a8014a680f86b98b3a93 (patch)
tree9fc08baec1a9879ef3919a55a8c91571da2cfcdc /walrss
parent2bf9ce74de03a2d75e7c19a380119deef77d8fba (diff)
Implement many things
Signed-off-by: AKP <tom@tdpain.net>
Diffstat (limited to 'walrss')
-rw-r--r--walrss/internal/core/feeds.go76
-rw-r--r--walrss/internal/core/validation.go20
-rw-r--r--walrss/internal/db/db.go7
-rw-r--r--walrss/internal/http/edit.go50
-rw-r--r--walrss/internal/http/http.go11
-rw-r--r--walrss/internal/http/mainpage.go6
-rw-r--r--walrss/internal/http/new.go36
-rw-r--r--walrss/internal/http/views/main.qtpl.html151
-rw-r--r--walrss/internal/http/views/main.qtpl.html.go261
-rw-r--r--walrss/internal/http/views/page.qtpl.html1
-rw-r--r--walrss/internal/http/views/page.qtpl.html.go1
-rw-r--r--walrss/internal/urls/urls.go29
12 files changed, 590 insertions, 59 deletions
diff --git a/walrss/internal/core/feeds.go b/walrss/internal/core/feeds.go
new file mode 100644
index 0000000..c9e4022
--- /dev/null
+++ b/walrss/internal/core/feeds.go
@@ -0,0 +1,76 @@
+package core
+
+import (
+ "errors"
+ "github.com/codemicro/walrss/walrss/internal/db"
+ "github.com/codemicro/walrss/walrss/internal/state"
+ "github.com/lithammer/shortuuid/v4"
+ bh "github.com/timshannon/bolthold"
+)
+
+func NewFeed(st *state.State, userID, name, url string) (*db.Feed, error) {
+ if err := validateFeedName(name); err != nil {
+ return nil, err
+ }
+
+ if err := validateURL(url); err != nil {
+ return nil, err
+ }
+
+ feed := &db.Feed{
+ ID: shortuuid.New(),
+ URL: url,
+ Name: name,
+ UserID: userID,
+ }
+
+ if err := st.Data.Insert(feed.ID, feed); err != nil {
+ return nil, err
+ }
+
+ return feed, nil
+}
+
+func GetFeedsForUser(st *state.State, userID string) ([]*db.Feed, error) {
+ var feeds []*db.Feed
+ if err := st.Data.Find(&feeds, bh.Where("UserID").Eq(userID)); err != nil {
+ return nil, err
+ }
+ return feeds, nil
+}
+
+func GetFeed(st *state.State, id string) (*db.Feed, error) {
+ feed := new(db.Feed)
+ if err := st.Data.FindOne(feed, bh.Where("ID").Eq(id)); err != nil {
+ if errors.Is(err, bh.ErrNotFound) {
+ return nil, ErrNotFound
+ }
+ return nil, err
+ }
+ return feed, nil
+}
+
+func DeleteFeed(st *state.State, id string) error {
+ if err := st.Data.Delete(id, new(db.Feed)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func UpdateFeed(st *state.State, feed *db.Feed) error {
+ if err := validateFeedName(feed.Name); err != nil {
+ return err
+ }
+
+ if err := validateURL(feed.URL); err != nil {
+ return err
+ }
+
+ if err := st.Data.Update(feed.ID, feed); err != nil {
+ if errors.Is(err, bh.ErrNotFound) {
+ return ErrNotFound
+ }
+ return err
+ }
+ return nil
+}
diff --git a/walrss/internal/core/validation.go b/walrss/internal/core/validation.go
index 501c023..e7c323c 100644
--- a/walrss/internal/core/validation.go
+++ b/walrss/internal/core/validation.go
@@ -1,7 +1,9 @@
package core
import (
+ "net/url"
"regexp"
+ "strings"
)
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
@@ -19,3 +21,21 @@ func validatePassword(password string) error {
}
return nil
}
+
+func validateFeedName(name string) error {
+ if strings.TrimSpace(name) == "" {
+ return NewUserError("feed name cannot be blank")
+ }
+ return nil
+}
+
+func validateURL(inputURL string) error {
+ u, err := url.ParseRequestURI(inputURL)
+ if err != nil {
+ return NewUserError("invalid URL")
+ }
+ if s := strings.ToLower(u.Scheme); !(s == "http" || s == "https") {
+ return NewUserError("invalid URL request scheme - must be HTTP or HTTPS")
+ }
+ return nil
+}
diff --git a/walrss/internal/db/db.go b/walrss/internal/db/db.go
index eb8590a..5139af8 100644
--- a/walrss/internal/db/db.go
+++ b/walrss/internal/db/db.go
@@ -28,3 +28,10 @@ type User struct {
Hour int `boltholdIndex:"Hour"`
}
}
+
+type Feed struct {
+ ID string `boltholdKey:""`
+ URL string
+ Name string
+ UserID string `boldholdIndex:"UserID"`
+}
diff --git a/walrss/internal/http/edit.go b/walrss/internal/http/edit.go
index 0c5bd95..2cfc545 100644
--- a/walrss/internal/http/edit.go
+++ b/walrss/internal/http/edit.go
@@ -81,3 +81,53 @@ func (s *Server) editTimings(ctx *fiber.Ctx) error {
SelectedTime: user.Schedule.Hour,
}).RenderScheduleCard())
}
+
+func (s *Server) editFeedItem(ctx *fiber.Ctx) error {
+ currentUserID := getCurrentUserID(ctx)
+ if currentUserID == "" {
+ return requestFragmentSignIn(ctx, urls.Index)
+ }
+
+ feedID := ctx.Params("id")
+
+ feed, err := core.GetFeed(s.state, feedID)
+ if err != nil {
+ return err
+ }
+
+ switch ctx.Method() {
+ case fiber.MethodGet:
+ return ctx.SendString(views.RenderFeedEditRow(feed.ID, feed.Name, feed.URL))
+ case fiber.MethodDelete:
+ if err := core.DeleteFeed(s.state, feed.ID); err != nil {
+ return err
+ }
+ return nil
+ case fiber.MethodPut:
+ feed.Name = ctx.FormValue("name")
+ feed.URL = ctx.FormValue("url")
+
+ if err := core.UpdateFeed(s.state, feed); err != nil {
+ return err
+ }
+ return ctx.SendString(views.RenderFeedRow(feed.ID, feed.Name, feed.URL))
+ }
+
+ panic("unreachable")
+}
+
+func (s *Server) cancelEditFeedItem(ctx *fiber.Ctx) error {
+ currentUserID := getCurrentUserID(ctx)
+ if currentUserID == "" {
+ return requestFragmentSignIn(ctx, urls.Index)
+ }
+
+ feedID := ctx.Params("id")
+
+ feed, err := core.GetFeed(s.state, feedID)
+ if err != nil {
+ return err
+ }
+
+ return ctx.SendString(views.RenderFeedRow(feed.ID, feed.Name, feed.URL))
+}
diff --git a/walrss/internal/http/http.go b/walrss/internal/http/http.go
index 603002b..813cb83 100644
--- a/walrss/internal/http/http.go
+++ b/walrss/internal/http/http.go
@@ -1,6 +1,7 @@
package http
import (
+ "fmt"
"github.com/codemicro/walrss/walrss/internal/core"
"github.com/codemicro/walrss/walrss/internal/http/views"
"github.com/codemicro/walrss/walrss/internal/state"
@@ -28,6 +29,8 @@ func New(st *state.State) (*Server, error) {
DisableStartupMessage: !st.Config.Debug,
AppName: "Walrss",
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
+ fmt.Println("Got ERROR", err)
+
code := fiber.StatusInternalServerError
msg := "Internal Server Error"
@@ -81,6 +84,14 @@ func (s *Server) registerHandlers() {
s.app.Put(urls.EditEnabledState, s.editEnabledState)
s.app.Put(urls.EditTimings, s.editTimings)
+
+ s.app.Get(urls.EditFeedItem, s.editFeedItem)
+ s.app.Put(urls.EditFeedItem, s.editFeedItem)
+ s.app.Delete(urls.EditFeedItem, s.editFeedItem)
+ s.app.Get(urls.CancelEditFeedItem, s.cancelEditFeedItem)
+
+ s.app.Get(urls.NewFeedItem, s.newFeedItem)
+ s.app.Post(urls.NewFeedItem, s.newFeedItem)
}
func (s *Server) Run() error {
diff --git a/walrss/internal/http/mainpage.go b/walrss/internal/http/mainpage.go
index 6c60753..1d94d2a 100644
--- a/walrss/internal/http/mainpage.go
+++ b/walrss/internal/http/mainpage.go
@@ -17,9 +17,15 @@ func (s *Server) mainPage(ctx *fiber.Ctx) error {
return err
}
+ feeds, err := core.GetFeedsForUser(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,
+ Feeds: feeds,
})
}
diff --git a/walrss/internal/http/new.go b/walrss/internal/http/new.go
new file mode 100644
index 0000000..0932123
--- /dev/null
+++ b/walrss/internal/http/new.go
@@ -0,0 +1,36 @@
+package http
+
+import (
+ "github.com/codemicro/walrss/walrss/internal/core"
+ "github.com/codemicro/walrss/walrss/internal/http/views"
+ "github.com/codemicro/walrss/walrss/internal/urls"
+ "github.com/gofiber/fiber/v2"
+)
+
+func (s *Server) newFeedItem(ctx *fiber.Ctx) error {
+
+ currentUserID := getCurrentUserID(ctx)
+ if currentUserID == "" {
+ return requestFragmentSignIn(ctx, urls.Index)
+ }
+
+ switch ctx.Method() {
+ case fiber.MethodGet:
+ return ctx.SendString(views.RenderNewFeedItemRow())
+ case fiber.MethodPost:
+ feed, err := core.NewFeed(
+ s.state,
+ currentUserID,
+ ctx.FormValue("name"),
+ ctx.FormValue("url"),
+ )
+
+ if err != nil {
+ return err
+ }
+
+ return ctx.SendString(views.RenderFeedRow(feed.ID, feed.Name, feed.URL))
+ }
+
+ panic("unreachable")
+}
diff --git a/walrss/internal/http/views/main.qtpl.html b/walrss/internal/http/views/main.qtpl.html
index 60a19eb..34a92c6 100644
--- a/walrss/internal/http/views/main.qtpl.html
+++ b/walrss/internal/http/views/main.qtpl.html
@@ -1,11 +1,13 @@
{% import "github.com/codemicro/walrss/walrss/internal/db" %}
{% import "github.com/codemicro/walrss/walrss/internal/urls" %}
+{% import "github.com/lithammer/shortuuid/v4" %}
{% code type MainPage struct {
BasePage
EnableDigests bool
SelectedDay db.SendDay
SelectedTime int
+ Feeds []*db.Feed
} %}
{% func (p *MainPage) Title() %}{% endfunc %}
@@ -31,15 +33,19 @@
new bootstrap.Toast(toast, {delay: delay}).show();
}
- function errorHandler() {
- toastBody.innerText = "Internal error: action incomplete";
+ function errorHandler(text) {
+ toastBody.innerText = "Error: " + text;
toast.classList.remove("bg-success")
toast.classList.add("bg-danger")
showToast(5000)
}
- document.body.addEventListener("htmx:sendError", errorHandler);
- document.body.addEventListener("htmx:responseError", errorHandler);
+ document.body.addEventListener("htmx:sendError", function () {
+ errorHandler("could not communicate with server");
+ });
+ document.body.addEventListener("htmx:responseError", function (evt) {
+ errorHandler(evt.detail.xhr.response)
+ });
document.body.addEventListener("successResponse", function () {
toastBody.innerText = "Success!"
@@ -62,31 +68,28 @@
<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>
+ <tr>
+ <th scope="col">Name</th>
+ <th scope="col">URL</th>
+ <th scope="col">
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button
+ type="button"
+ class="btn btn-primary"
+ hx-get="{%s= urls.NewFeedItem %}"
+ hx-target="#feedListing"
+ hx-swap="beforeend"
+ >
+ <i class="bi bi-plus"></i>
+ </button>
+ </div>
+ </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 id="feedListing">
+ {% for _, feed := range p.Feeds %}
+ {%= RenderFeedRow(feed.ID, feed.Name, feed.URL) %}
+ {% endfor %}
</tbody>
</table>
@@ -96,6 +99,98 @@
</div>
{% endfunc %}
+{% func RenderFeedRow(id, name, url string) %}
+<tr id="feed-{%s= id %}" class="align-middle" hx-target="this" hx-swap="outerHTML">
+ <th id="feed-{%s= id %}-name" scope="row">{%s name %}</th>
+ <td id="feed-{%s= id %}-url" >{%s url %}</td>
+ <td>
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button type="button" class="btn btn-outline-primary" hx-get="{%s= urls.Expand(urls.EditFeedItem, id) %}">
+ <i class="bi bi-pencil-square"></i>
+ </button>
+ <button
+ type="button"
+ class="btn btn-outline-danger"
+ hx-delete="{%s= urls.Expand(urls.EditFeedItem, id) %}"
+ hx-confirm="This will permanently delete this item. Are you sure?"
+ >
+ <i class="bi bi-trash"></i>
+ </button>
+ </div>
+ </td>
+</tr>
+{% endfunc %}
+
+{% func RenderFeedEditRow(id, name, url string) %}
+<tr id="feed-{%s= id %}" class="align-middle" hx-target="this" hx-swap="outerHTML">
+ <th scope="row"><input
+ class="form-control form-control-sm"
+ type="text"
+ name="name"
+ id="feed-{%s= id %}-name"
+ value="{%j name %}"
+ ></th>
+ <td><input
+ class="form-control form-control-sm"
+ type="url"
+ name="url"
+ id="feed-{%s= id %}-url"
+ value="{%j url %}"
+ ></td>
+ <td>
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button
+ type="button"
+ class="btn btn-outline-success"
+ hx-put="{%s= urls.Expand(urls.EditFeedItem, id) %}"
+ hx-include="#feed-{%s= id %}-name, #feed-{%s= id %}-url"
+ >
+ <i class="bi bi-check"></i>
+ </button>
+ <button type="button" class="btn btn-outline-danger" hx-get="{%s= urls.Expand(urls.CancelEditFeedItem, id) %}"><i class="bi bi-x"></i></button>
+ </div>
+ </td>
+</tr>
+{% endfunc %}
+
+{% func RenderNewFeedItemRow() %}
+{% code id := shortuuid.New() %}
+<tr id="{%s= id %}" class="align-middle" hx-target="this" hx-swap="outerHTML">
+ <th scope="row"><input
+ id="{%s= id %}-name-input"
+ class="form-control form-control-sm"
+ type="text"
+ name="name"
+ placeholder="Name"
+ ></th>
+ <td><input
+ id="{%s= id %}-url-input"
+ class="form-control form-control-sm"
+ type="url"
+ name="url"
+ placeholder="URL"
+ ></td>
+ <td>
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button
+ type="button"
+ class="btn btn-outline-success"
+ hx-post="{%s= urls.NewFeedItem %}"
+ hx-include="#{%s= id %}-name-input, #{%s= id %}-url-input">
+ <i class="bi bi-check"></i>
+ </button>
+ <button type="button" class="btn btn-outline-danger" id="{%s= id %}-cancel"><i class="bi bi-x"></i></button>
+ </div>
+ </td>
+
+ <script>
+ document.getElementById("{%s= id %}-cancel").addEventListener("click", function () {
+ document.getElementById("{%s= id %}").outerHTML = "";
+ });
+ </script>
+</tr>
+{% endfunc %}
+
{% func (p *MainPage) RenderScheduleCard() %}
<div class="card mt-3" id="scheduleCard" hx-target="this" hx-swap="outerHTML">
<div class="card-header">
diff --git a/walrss/internal/http/views/main.qtpl.html.go b/walrss/internal/http/views/main.qtpl.html.go
index d18a5ac..55be033 100644
--- a/walrss/internal/http/views/main.qtpl.html.go
+++ b/walrss/internal/http/views/main.qtpl.html.go
@@ -7,6 +7,8 @@ import "github.com/codemicro/walrss/walrss/internal/db"
import "github.com/codemicro/walrss/walrss/internal/urls"
+import "github.com/lithammer/shortuuid/v4"
+
import (
qtio422016 "io"
@@ -23,6 +25,7 @@ type MainPage struct {
EnableDigests bool
SelectedDay db.SendDay
SelectedTime int
+ Feeds []*db.Feed
}
func (p *MainPage) StreamTitle(qw422016 *qt422016.Writer) {
@@ -67,15 +70,19 @@ func (p *MainPage) StreamBody(qw422016 *qt422016.Writer) {
new bootstrap.Toast(toast, {delay: delay}).show();
}
- function errorHandler() {
- toastBody.innerText = "Internal error: action incomplete";
+ function errorHandler(text) {
+ toastBody.innerText = "Error: " + text;
toast.classList.remove("bg-success")
toast.classList.add("bg-danger")
showToast(5000)
}
- document.body.addEventListener("htmx:sendError", errorHandler);
- document.body.addEventListener("htmx:responseError", errorHandler);
+ document.body.addEventListener("htmx:sendError", function () {
+ errorHandler("could not communicate with server");
+ });
+ document.body.addEventListener("htmx:responseError", function (evt) {
+ errorHandler(evt.detail.xhr.response)
+ });
document.body.addEventListener("successResponse", function () {
toastBody.innerText = "Success!"
@@ -100,31 +107,36 @@ func (p *MainPage) StreamBody(qw422016 *qt422016.Writer) {
<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>
+ <tr>
+ <th scope="col">Name</th>
+ <th scope="col">URL</th>
+ <th scope="col">
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button
+ type="button"
+ class="btn btn-primary"
+ hx-get="`)
+ qw422016.N().S(urls.NewFeedItem)
+ qw422016.N().S(`"
+ hx-target="#feedListing"
+ hx-swap="beforeend"
+ >
+ <i class="bi bi-plus"></i>
+ </button>
+ </div>
+ </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 id="feedListing">
+ `)
+ for _, feed := range p.Feeds {
+ qw422016.N().S(`
+ `)
+ StreamRenderFeedRow(qw422016, feed.ID, feed.Name, feed.URL)
+ qw422016.N().S(`
+ `)
+ }
+ qw422016.N().S(`
</tbody>
</table>
@@ -149,6 +161,199 @@ func (p *MainPage) Body() string {
return qs422016
}
+func StreamRenderFeedRow(qw422016 *qt422016.Writer, id, name, url string) {
+ qw422016.N().S(`
+<tr id="feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`" class="align-middle" hx-target="this" hx-swap="outerHTML">
+ <th id="feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-name" scope="row">`)
+ qw422016.E().S(name)
+ qw422016.N().S(`</th>
+ <td id="feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-url" >`)
+ qw422016.E().S(url)
+ qw422016.N().S(`</td>
+ <td>
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button type="button" class="btn btn-outline-primary" hx-get="`)
+ qw422016.N().S(urls.Expand(urls.EditFeedItem, id))
+ qw422016.N().S(`">
+ <i class="bi bi-pencil-square"></i>
+ </button>
+ <button
+ type="button"
+ class="btn btn-outline-danger"
+ hx-delete="`)
+ qw422016.N().S(urls.Expand(urls.EditFeedItem, id))
+ qw422016.N().S(`"
+ hx-confirm="This will permanently delete this item. Are you sure?"
+ >
+ <i class="bi bi-trash"></i>
+ </button>
+ </div>
+ </td>
+</tr>
+`)
+}
+
+func WriteRenderFeedRow(qq422016 qtio422016.Writer, id, name, url string) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamRenderFeedRow(qw422016, id, name, url)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func RenderFeedRow(id, name, url string) string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteRenderFeedRow(qb422016, id, name, url)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func StreamRenderFeedEditRow(qw422016 *qt422016.Writer, id, name, url string) {
+ qw422016.N().S(`
+<tr id="feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`" class="align-middle" hx-target="this" hx-swap="outerHTML">
+ <th scope="row"><input
+ class="form-control form-control-sm"
+ type="text"
+ name="name"
+ id="feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-name"
+ value="`)
+ qw422016.E().J(name)
+ qw422016.N().S(`"
+ ></th>
+ <td><input
+ class="form-control form-control-sm"
+ type="url"
+ name="url"
+ id="feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-url"
+ value="`)
+ qw422016.E().J(url)
+ qw422016.N().S(`"
+ ></td>
+ <td>
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button
+ type="button"
+ class="btn btn-outline-success"
+ hx-put="`)
+ qw422016.N().S(urls.Expand(urls.EditFeedItem, id))
+ qw422016.N().S(`"
+ hx-include="#feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-name, #feed-`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-url"
+ >
+ <i class="bi bi-check"></i>
+ </button>
+ <button type="button" class="btn btn-outline-danger" hx-get="`)
+ qw422016.N().S(urls.Expand(urls.CancelEditFeedItem, id))
+ qw422016.N().S(`"><i class="bi bi-x"></i></button>
+ </div>
+ </td>
+</tr>
+`)
+}
+
+func WriteRenderFeedEditRow(qq422016 qtio422016.Writer, id, name, url string) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamRenderFeedEditRow(qw422016, id, name, url)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func RenderFeedEditRow(id, name, url string) string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteRenderFeedEditRow(qb422016, id, name, url)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
+func StreamRenderNewFeedItemRow(qw422016 *qt422016.Writer) {
+ qw422016.N().S(`
+`)
+ id := shortuuid.New()
+
+ qw422016.N().S(`
+<tr id="`)
+ qw422016.N().S(id)
+ qw422016.N().S(`" class="align-middle" hx-target="this" hx-swap="outerHTML">
+ <th scope="row"><input
+ id="`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-name-input"
+ class="form-control form-control-sm"
+ type="text"
+ name="name"
+ placeholder="Name"
+ ></th>
+ <td><input
+ id="`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-url-input"
+ class="form-control form-control-sm"
+ type="url"
+ name="url"
+ placeholder="URL"
+ ></td>
+ <td>
+ <div class="btn-group btn-group-sm" role="group" aria-label="Basic example">
+ <button
+ type="button"
+ class="btn btn-outline-success"
+ hx-post="`)
+ qw422016.N().S(urls.NewFeedItem)
+ qw422016.N().S(`"
+ hx-include="#`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-name-input, #`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-url-input">
+ <i class="bi bi-check"></i>
+ </button>
+ <button type="button" class="btn btn-outline-danger" id="`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-cancel"><i class="bi bi-x"></i></button>
+ </div>
+ </td>
+
+ <script>
+ document.getElementById("`)
+ qw422016.N().S(id)
+ qw422016.N().S(`-cancel").addEventListener("click", function () {
+ document.getElementById("`)
+ qw422016.N().S(id)
+ qw422016.N().S(`").outerHTML = "";
+ });
+ </script>
+</tr>
+`)
+}
+
+func WriteRenderNewFeedItemRow(qq422016 qtio422016.Writer) {
+ qw422016 := qt422016.AcquireWriter(qq422016)
+ StreamRenderNewFeedItemRow(qw422016)
+ qt422016.ReleaseWriter(qw422016)
+}
+
+func RenderNewFeedItemRow() string {
+ qb422016 := qt422016.AcquireByteBuffer()
+ WriteRenderNewFeedItemRow(qb422016)
+ qs422016 := string(qb422016.B)
+ qt422016.ReleaseByteBuffer(qb422016)
+ return qs422016
+}
+
func (p *MainPage) StreamRenderScheduleCard(qw422016 *qt422016.Writer) {
qw422016.N().S(`
<div class="card mt-3" id="scheduleCard" hx-target="this" hx-swap="outerHTML">
diff --git a/walrss/internal/http/views/page.qtpl.html b/walrss/internal/http/views/page.qtpl.html
index 9e9c0c2..872b468 100644
--- a/walrss/internal/http/views/page.qtpl.html
+++ b/walrss/internal/http/views/page.qtpl.html
@@ -15,6 +15,7 @@ Page prints a page implementing Page interface.
<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">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<style>
[disabled] {
diff --git a/walrss/internal/http/views/page.qtpl.html.go b/walrss/internal/http/views/page.qtpl.html.go
index b0900f9..1d66228 100644
--- a/walrss/internal/http/views/page.qtpl.html.go
+++ b/walrss/internal/http/views/page.qtpl.html.go
@@ -39,6 +39,7 @@ func StreamRenderPage(qw422016 *qt422016.Writer, p Page) {
<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">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<style>
[disabled] {
diff --git a/walrss/internal/urls/urls.go b/walrss/internal/urls/urls.go
index bd3d878..a5dd782 100644
--- a/walrss/internal/urls/urls.go
+++ b/walrss/internal/urls/urls.go
@@ -1,5 +1,10 @@
package urls
+import (
+ "fmt"
+ "strings"
+)
+
const (
Index = "/"
@@ -7,7 +12,25 @@ const (
AuthSignIn = Auth + "/signin"
AuthRegister = Auth + "/register"
- Edit = "/edit"
- EditEnabledState = Edit + "/enabled"
- EditTimings = Edit + "/timings"
+ Edit = "/edit"
+ EditEnabledState = Edit + "/enabled"
+ EditTimings = Edit + "/timings"
+ EditFeedItem = Edit + "/feed/:id"
+ CancelEditFeedItem = Edit + "/feed/:id/cancel"
+
+ New = "/new"
+ NewFeedItem = New + "/feed"
)
+
+func Expand(template string, replacements ...interface{}) string {
+ spt := strings.Split(template, "/")
+ for i, part := range spt {
+ if len(part) == 0 {
+ continue
+ }
+ if part[0] == ':' {
+ spt[i] = "%s"
+ }
+ }
+ return fmt.Sprintf(strings.Join(spt, "/"), replacements...)
+}