Skip to content

Commit fd472d2

Browse files
authored
Improve delete using htmx only
1 parent 55c92a5 commit fd472d2

36 files changed

+426
-303
lines changed

Diff for: internal/webserver/controller/author/search.go

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package author
22

33
import (
4-
"fmt"
54
"log"
65
"strconv"
76

@@ -50,16 +49,26 @@ func (a *Controller) Search(c *fiber.Ctx) error {
5049
searchResults = a.hlRepository.HighlightedPaginatedResult(int(session.ID), searchResults)
5150
}
5251

53-
err = c.Render("author/results", fiber.Map{
52+
templateVars := fiber.Map{
5453
"Author": author,
5554
"Results": searchResults,
5655
"Paginator": view.Pagination(model.MaxPagesNavigator, searchResults, map[string]string{}),
57-
"Title": fmt.Sprintf("Coreander - %s", author.Name),
56+
"Title": author.Name,
5857
"EmailSendingConfigured": emailSendingConfigured,
5958
"EmailFrom": a.sender.From(),
6059
"WordsPerMinute": a.config.WordsPerMinute,
61-
}, "layout")
60+
"URL": view.URL(c),
61+
}
62+
63+
if c.Get("hx-request") == "true" {
64+
if err = c.Render("partials/docs-list", templateVars); err != nil {
65+
log.Println(err)
66+
return fiber.ErrInternalServerError
67+
}
68+
return nil
69+
}
6270

71+
err = c.Render("author/results", templateVars, "layout")
6372
if err != nil {
6473
log.Println(err)
6574
return fiber.ErrInternalServerError

Diff for: internal/webserver/controller/document/detail.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (d *Controller) Detail(c *fiber.Ctx) error {
4242
return fiber.ErrNotFound
4343
}
4444

45-
title := fmt.Sprintf("%s", document.Title)
45+
title := document.Title
4646
if len(document.Authors) > 0 {
4747
title = fmt.Sprintf("%s - %s", strings.Join(document.Authors, ", "), document.Title)
4848
}
@@ -58,7 +58,7 @@ func (d *Controller) Detail(c *fiber.Ctx) error {
5858
msg = "Document uploaded successfully."
5959
}
6060

61-
return c.Render("document", fiber.Map{
61+
return c.Render("document/detail", fiber.Map{
6262
"Title": title,
6363
"Document": document,
6464
"EmailSendingConfigured": emailSendingConfigured,

Diff for: internal/webserver/controller/document/reader.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ func (d *Controller) Reader(c *fiber.Ctx) error {
2525
return fiber.ErrNotFound
2626
}
2727

28-
template := "reader"
29-
30-
title := fmt.Sprintf("%s", document.Title)
28+
title := document.Title
3129
authors := strings.Join(document.Authors, ", ")
3230
if authors != "" {
3331
title = fmt.Sprintf("%s - %s", authors, document.Title)
3432
}
35-
return c.Render(template, fiber.Map{
33+
return c.Render("document/reader", fiber.Map{
3634
"Title": title,
3735
"Author": strings.Join(document.Authors, ", "),
3836
"Description": document.Description,

Diff for: internal/webserver/controller/document/search.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,29 @@ func (d *Controller) Search(c *fiber.Ctx) error {
4848
searchResults = d.hlRepository.HighlightedPaginatedResult(int(session.ID), searchResults)
4949
}
5050

51-
err = c.Render("results", fiber.Map{
51+
templateVars := fiber.Map{
5252
"Keywords": keywords,
5353
"Results": searchResults,
5454
"Paginator": view.Pagination(model.MaxPagesNavigator, searchResults, map[string]string{"search": keywords}),
5555
"Title": "Search results",
5656
"EmailSendingConfigured": emailSendingConfigured,
5757
"EmailFrom": d.sender.From(),
5858
"WordsPerMinute": d.config.WordsPerMinute,
59-
}, "layout")
59+
"URL": view.URL(c),
60+
}
6061

61-
if err != nil {
62+
if c.Get("hx-request") == "true" {
63+
if err = c.Render("partials/docs-list", templateVars); err != nil {
64+
log.Println(err)
65+
return fiber.ErrInternalServerError
66+
}
67+
return nil
68+
}
69+
70+
if err = c.Render("document/results", templateVars, "layout"); err != nil {
6271
log.Println(err)
6372
return fiber.ErrInternalServerError
6473
}
74+
6575
return nil
6676
}

Diff for: internal/webserver/controller/document/upload.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
)
1717

1818
func (d *Controller) UploadForm(c *fiber.Ctx) error {
19-
return c.Render("upload", fiber.Map{
19+
return c.Render("document/upload", fiber.Map{
2020
"Title": "Upload document",
2121
"MaxSize": d.config.UploadDocumentMaxSize,
2222
}, "layout")
@@ -26,7 +26,7 @@ func (d *Controller) Upload(c *fiber.Ctx) error {
2626
file, err := c.FormFile("filename")
2727
if err != nil {
2828
if errors.Is(err, fasthttp.ErrMissingFile) {
29-
return c.Status(fiber.StatusBadRequest).Render("upload", fiber.Map{
29+
return c.Status(fiber.StatusBadRequest).Render("document/upload", fiber.Map{
3030
"Title": "Upload document",
3131
"Error": "Invalid file type",
3232
}, "layout")
@@ -35,21 +35,21 @@ func (d *Controller) Upload(c *fiber.Ctx) error {
3535

3636
allowedTypes := []string{"application/epub+zip", "application/pdf"}
3737
if !slices.Contains(allowedTypes, file.Header.Get("Content-Type")) {
38-
return c.Status(fiber.StatusBadRequest).Render("upload", fiber.Map{
38+
return c.Status(fiber.StatusBadRequest).Render("document/upload", fiber.Map{
3939
"Title": "Upload document",
4040
"Error": "Invalid file type",
4141
}, "layout")
4242
}
4343

4444
if file.Size > int64(d.config.UploadDocumentMaxSize*1024*1024) {
45-
return c.Status(fiber.StatusRequestEntityTooLarge).Render("upload", fiber.Map{
45+
return c.Status(fiber.StatusRequestEntityTooLarge).Render("document/upload", fiber.Map{
4646
"Title": "Upload Document",
4747
"Error": fmt.Sprintf("Document too large, the maximum allowed size is %d megabytes", d.config.UploadDocumentMaxSize),
4848
}, "layout")
4949
}
5050

5151
destination := filepath.Join(d.config.LibraryPath, file.Filename)
52-
internalServerErrorStatus := c.Status(fiber.StatusInternalServerError).Render("upload", fiber.Map{
52+
internalServerErrorStatus := c.Status(fiber.StatusInternalServerError).Render("document/upload", fiber.Map{
5353
"Title": "Upload Document",
5454
"Error": "Error uploading document",
5555
}, "layout")

Diff for: internal/webserver/controller/highlight/list.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package highlight
22

33
import (
4-
"fmt"
54
"log"
65
"strconv"
76

@@ -17,6 +16,7 @@ func (h *Controller) List(c *fiber.Ctx) error {
1716
page, err := strconv.Atoi(c.Query("page"))
1817
if err != nil {
1918
page = 1
19+
c.Query("page", "1")
2020
}
2121

2222
var session model.Session
@@ -63,25 +63,31 @@ func (h *Controller) List(c *fiber.Ctx) error {
6363
highlights,
6464
)
6565

66-
url := fmt.Sprintf("/highlights?view=list&page=%d", page)
67-
6866
layout := "layout"
6967
if c.Query("view") == "list" {
7068
layout = ""
7169
}
7270

73-
err = c.Render("highlights", fiber.Map{
71+
templateVars := fiber.Map{
7472
"Results": paginatedResults,
7573
"Paginator": view.Pagination(model.MaxPagesNavigator, paginatedResults, nil),
7674
"Title": "Highlights",
7775
"EmailSendingConfigured": emailSendingConfigured,
7876
"EmailFrom": h.sender.From(),
7977
"WordsPerMinute": h.wordsPerMinute,
80-
"Url": url,
81-
}, layout)
78+
"URL": view.URL(c),
79+
}
8280

83-
if err != nil {
81+
if c.Get("hx-request") == "true" {
82+
if err = c.Render("partials/docs-list", templateVars); err != nil {
83+
log.Println(err)
84+
return fiber.ErrInternalServerError
85+
}
86+
return nil
87+
}
88+
if err = c.Render("highlight/index", templateVars, layout); err != nil {
8489
log.Println(err)
90+
return fiber.ErrInternalServerError
8591
}
8692

8793
return nil

Diff for: internal/webserver/controller/user/list.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@ func (u *Controller) List(c *fiber.Ctx) error {
1616
}
1717

1818
users, _ := u.repository.List(page, model.ResultsPerPage)
19-
return c.Render("user/index", fiber.Map{
19+
20+
templateVars := fiber.Map{
2021
"Title": "Users",
2122
"Users": users.Hits(),
2223
"Paginator": view.Pagination(model.MaxPagesNavigator, users, map[string]string{}),
2324
"Admins": u.repository.Admins(),
24-
}, "layout")
25+
"URL": view.URL(c),
26+
}
27+
28+
if c.Get("hx-request") == "true" {
29+
return c.Render("partials/users-list", templateVars)
30+
}
31+
32+
return c.Render("user/index", templateVars, "layout")
2533
}

Diff for: internal/webserver/embedded/css/display.css

+17-1
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,20 @@ a.collapse-control.collapsed:after {
152152

153153
.htmx-request.spinner-border ~ .bi-envelope-fill {
154154
display: none;
155-
}
155+
}
156+
157+
.list-group-placeholder {
158+
display: none;
159+
}
160+
161+
.htmx-request .list-group-placeholder {
162+
display: inline-block;
163+
}
164+
165+
.htmx-request.list-group-placeholder {
166+
display: inline-block;
167+
}
168+
169+
.htmx-request.list-group-placeholder ~ div {
170+
display: none;
171+
}

Diff for: internal/webserver/embedded/js/datetime.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22

33
import { DateTime } from "./luxon.min.js";
44

5-
document.addEventListener('DOMContentLoaded', function() {
5+
const datetimeFormatter = () => {
66
const datetime = document.querySelectorAll('.datetime span');
77
datetime.forEach(function(element) {
88
const dt = DateTime.fromISO(element.textContent);
9-
element.textContent = dt.toRelative({ locale: document.documentElement.lang });
9+
if (dt.isValid) {
10+
element.textContent = dt.toRelative({ locale: document.documentElement.lang });
11+
}
1012
});
11-
});
13+
}
14+
15+
document.addEventListener('DOMContentLoaded', datetimeFormatter());
16+
17+
const observer = new MutationObserver(datetimeFormatter);
18+
19+
// Start observing the target node for configured mutations
20+
const node = document.getElementById("list");
21+
observer.observe(node, { attributes: true, childList: false, subtree: true });

Diff for: internal/webserver/embedded/js/delete.js

+16-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use strict"
22

3+
import { handleResponseError } from './handle-response-error.js'
4+
35
// We use several conventions to be able to use the same code to delete different resources.
46
// The link that initiates the action needs to have an attribute called data-id which must contain an unique identifier
57
// for the resource to delete.
@@ -9,34 +11,25 @@
911

1012
const deleteModal = document.getElementById('delete-modal');
1113
const deleteForm = document.getElementById('delete-form');
12-
let id
1314

1415
deleteModal.addEventListener('show.bs.modal', event => {
1516
const link = event.relatedTarget
16-
id = link.getAttribute('data-id')
17+
deleteForm.setAttribute('hx-delete', link.getAttribute('data-url'))
18+
htmx.process(deleteForm)
1719
})
1820

19-
deleteModal.addEventListener('hidden.bs.modal', event => {
20-
let message = document.getElementById('error-message-container');
21-
message.classList.add("visually-hidden");
21+
document.body.addEventListener('htmx:responseError', function (evt) {
22+
const del = evt.detail.elt.getAttribute("hx-delete")
23+
if (!del) {
24+
return
25+
}
26+
27+
return handleResponseError(evt)
2228
})
2329

24-
deleteForm.addEventListener('submit', event => {
25-
event.preventDefault();
26-
fetch(deleteForm.getAttribute("action") + '/' + id, {
27-
method: "DELETE"
28-
})
29-
.then((response) => {
30-
if (response.ok || response.status == "403") {
31-
location.reload();
32-
} else {
33-
let message = document.getElementById("error-message-container");
34-
message.classList.remove("visually-hidden");
35-
message.innerHTML = deleteForm.getAttribute("data-error-message");
36-
}
37-
})
38-
.catch(function (error) {
39-
// Catch errors
40-
console.log(error);
41-
});
30+
document.body.addEventListener('htmx:afterRequest', function (evt) {
31+
const del = evt.detail.elt.getAttribute("hx-delete")
32+
if (!evt.detail.failed && del && !del.includes("/highlights")) {
33+
htmx.trigger("#list", "update")
34+
}
4235
})
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const handleResponseError = (evt) => {
2+
if (evt.detail.xhr.status === 403) {
3+
location.reload()
4+
return
5+
}
6+
7+
const toast = document.getElementById('live-toast-danger')
8+
toast.querySelector(".toast-body").innerHTML = evt.detail.elt.getAttribute("data-error-message")
9+
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toast)
10+
toastBootstrap.show()
11+
}

Diff for: internal/webserver/embedded/js/send-email.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use strict"
22

3+
import { handleResponseError } from './handle-response-error.js'
4+
35
document.body.addEventListener('htmx:configRequest', function (evt) {
46
const post = evt.detail.elt.getAttribute("hx-post")
57

@@ -17,15 +19,12 @@ document.body.addEventListener('htmx:configRequest', function (evt) {
1719
})
1820

1921
document.body.addEventListener('htmx:responseError', function (evt) {
20-
if (evt.detail.xhr.status === 403) {
21-
location.reload()
22+
const post = evt.detail.elt.getAttribute("hx-post")
23+
if (!post || !post.includes("/send")) {
2224
return
2325
}
2426

25-
const toast = document.getElementById('live-toast-danger')
26-
toast.querySelector(".toast-body").innerHTML = evt.detail.elt.getAttribute("data-error-message")
27-
const toastBootstrap = bootstrap.Toast.getOrCreateInstance(toast)
28-
toastBootstrap.show()
27+
handleResponseError(evt)
2928
})
3029

3130
document.body.addEventListener('htmx:afterRequest', function (evt) {

Diff for: internal/webserver/embedded/translations/es.yml

-5
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
"Search for more titles by %s": "Buscar más títulos de %s"
1313
"Search for more titles in %s": "Buscar más títulos en %s"
1414
"Search tips": Consejos de búsqueda
15-
"Use %s prefix in search box to search by authors only": Usa el prefijo %s en la caja de búsqueda para buscar solo por autor/a
16-
"Use %s prefix in search box to search by title only": Usa el prefijo %s en la caja de búsqueda para buscar solo por título
17-
"Use %s prefix in search box to search by series only": Usa el prefijo %s en la caja de búsqueda para buscar solo por serie
18-
"Use %s prefix in search box to search by subjects only": Usa el prefijo %s en la caja de búsqueda para buscar solo por tema
19-
"Enclose your search terms in double quotes to require all those terms and with the same order": Entrecomilla tu búsqueda con comillas dobles para requerir todos los términos y con el mismo orden
2015
"Send to email": Enviar a correo electrónico
2116
"Send": Enviar
2217
"Actions": Acciones

Diff for: internal/webserver/embedded/translations/fr.yml

-5
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
"Search for more titles by %s": "Rechercher plus de titres par %s"
1313
"Search for more titles in %s": "Rechercher plus de titres de %s"
1414
"Search tips": Conseils de recherche
15-
"Use %s prefix in search box to search by authors only": Utilisez le préfixe %s dans la zone de recherche pour rechercher uniquement par auteur
16-
"Use %s prefix in search box to search by title only": Utilisez le préfixe %s dans la zone de recherche pour effectuer une recherche par titre uniquement
17-
"Use %s prefix in search box to search by series only": Utilisez le préfixe %s dans la zone de recherche pour rechercher uniquement par série
18-
"Use %s prefix in search box to search by subjects only": Utilisez le préfixe %s dans la zone de recherche pour rechercher uniquement par sujet
19-
"Enclose your search terms in double quotes to require all those terms and with the same order": Placez vos termes de recherche entre guillemets pour exiger tous ces termes et dans le même ordre
2015
"Send to email": Envoyer par e-mail
2116
"Send": Envoyer
2217
"Actions": Actions

0 commit comments

Comments
 (0)