From 336140c1fa66c18fe9334645c819753b0484b3a3 Mon Sep 17 00:00:00 2001
From: danoloan10
Date: Thu, 20 Jul 2023 23:10:00 +0200
Subject: [PATCH] Funcionalidad de borrado sin javascript
---
admin/static/main.js | 114 ----------------------------
main.go | 40 ++++++++--
storage/entry.go | 24 ++++++
storage/entrylist.go | 175 +++++++++++++++++++++++++++++++++++++++++++
storage/jstime.go | 42 +++++++++++
templates/admin.html | 10 +--
templates/del.html | 25 +++++++
7 files changed, 304 insertions(+), 126 deletions(-)
delete mode 100644 admin/static/main.js
create mode 100644 storage/entry.go
create mode 100644 storage/entrylist.go
create mode 100644 storage/jstime.go
create mode 100644 templates/del.html
diff --git a/admin/static/main.js b/admin/static/main.js
deleted file mode 100644
index ae2ab4f..0000000
--- a/admin/static/main.js
+++ /dev/null
@@ -1,114 +0,0 @@
-function refreshHeight(elem) {
- elem.style.maxHeight = `${elem.scrollHeight}px`;
-}
-function validate(form) {
- var valid = true;
- if (form['artist'].value == '') {
- form['artist'].parentElement.setAttribute('error',
- 'Se necesita el nombre del artista');
- valid = false;
- }
- if (form['album'].value == '' && form['track'].value == '') {
- form['album'].parentElement.setAttribute('error',
- 'Se necesita al menos uno de entre «canción» o «áblum»');
- form['track'].parentElement.setAttribute('error',
- 'Se necesita al menos uno de entre «canción» o «áblum»');
- valid = false;
- }
- refreshHeight(form.closest('.action-wrap'));
- return valid;
-}
-
-function deleteSong() {
- var button = this;
- var data = button.closest('.data-elem');
- var index = data.getAttribute('data-info');
- if (confirm('¿Seguro?')) {
- fetch(`del?p=${index}`).then(response => {
- var wrap = button.closest('.action-wrap');
- if (response.ok) {
- data.parentElement.removeChild(data);
- } else {
- button.closest('.action')
- .setAttribute('result', 'Error en la solicitud');
- }
- refreshHeight(wrap);
- });
- }
-}
-
-fetch('../list')
- .then(response => {
- if (response.ok) {
- return response.json();
- }
- })
- .then(list => {
- list.forEach((elem, index) => {
- var newButton = document.createElement('button');
- var title = elem['track'] == undefined ? elem['album'] : elem['track'];
- newButton.setAttribute('class', 'data-elem');
- newButton.onclick = deleteSong;
- newButton.setAttribute('data-info', index);
- newButton.innerText = `${elem['artist']} - ${title}`;
- document.querySelector('#dellist').appendChild(newButton);
- });
- })
- .catch(reason => console.log(reason));
-
-document.querySelectorAll('.action-wrap').forEach(elem => {
- var cajetin = elem.querySelector('.cajetin');
- elem.setAttribute('collapsed', 'true');
- elem.style.maxHeight = `${cajetin.scrollHeight}px`;
- cajetin.onclick = function() {
- var form = elem.querySelector('form');
- if (elem.getAttribute('collapsed') == 'true') {
- refreshHeight(elem);
- elem.setAttribute('collapsed', 'false');
- } else {
- elem.style.maxHeight = `${cajetin.scrollHeight}px`;
- elem.setAttribute('collapsed', 'true');
- }
- }
-});
-
-document.querySelectorAll('.form-elem > input').forEach(elem => {
- elem.addEventListener('input', () => {
- elem.parentElement.removeAttribute('error');
- elem.closest('form').removeAttribute('result');
- refreshHeight(elem.closest('.action-wrap'));
- });
-});
-
-const add = document.querySelector('#add');
-add.addEventListener('submit', (event) => {
- event.preventDefault();
- if (validate(add)) {
- var xmlHttp = new XMLHttpRequest();
- var json = {}
- var data = new FormData(add);
- data.forEach((v, k) => json[k] = v);
- if (!json['date']) {
- const date = new Date(Date.now());
- const year = `${date.getFullYear()}`;
- const month = `${date.getMonth() < 10 ? '0' : '' }${date.getMonth()+1}`;
- const day = `${date.getDate() < 10 ? '0' : '' }${date.getDate()+1}`;
- json['date'] = `${year}-${month}-${day}`;
- }
- fetch('add', {
- method: 'POST',
- body: JSON.stringify(json),
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(response => {
- if (response.ok) {
- add.setAttribute('result', 'Canción añadida');
- } else {
- add.setAttribute('result', 'Error en la solicitud');
- }
- refreshHeight(add.closest('.action-wrap'));
- });
- }
- return false;
-});
diff --git a/main.go b/main.go
index 923e989..fd25295 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,7 @@ package main
import (
"crypto/sha512"
"encoding/hex"
+ "errors"
"html/template"
"io"
"net/http"
@@ -72,24 +73,48 @@ func AddController(c echo.Context) error {
}
func DelController(c echo.Context) error {
- var res Response
+ var url string
var status int
var pos int
- pos, err := strconv.Atoi(c.QueryParam("p"))
+ pos, err := strconv.Atoi(c.Param("index"))
if err == nil {
err = list.DeleteElement(pos)
}
if err == nil {
- res = Response{true, "OK"}
- status = http.StatusOK
+ url = "../.."
+ status = http.StatusFound
} else {
- res = Response{false, "Failed to delete from list"}
status = http.StatusInternalServerError
}
- return c.JSON(status, res)
+ return c.Redirect(status, url)
+}
+
+func DelConfirmController(c echo.Context) error {
+ var entries []*storage.Entry
+ failed := c.QueryParam("failed")
+ indexStr := c.Param("index")
+ index, err := strconv.Atoi(indexStr)
+ if err == nil {
+ entries, err = list.GetEntries()
+ }
+ if index >= len(entries) {
+ err = errors.New("Index out of range")
+ }
+ if err == nil {
+ err = c.Render(http.StatusOK, "del.html", struct {
+ Index int
+ Elem *storage.Entry
+ Failed string
+ }{
+ Index: index,
+ Elem: entries[index],
+ Failed: failed,
+ })
+ }
+ return err
}
func auth(username, password string, c echo.Context) (bool, error) {
@@ -122,7 +147,8 @@ func main() {
admin := e.Group("/admin", middleware.BasicAuth(auth))
admin.POST("/add", AddController)
- admin.GET("/del", DelController)
+ admin.POST("/del/:index", DelController)
+ admin.GET("/del/:index", DelConfirmController)
admin.GET("/", Template("admin.html").TemplateController)
admin.Static("/static", "admin/static")
diff --git a/storage/entry.go b/storage/entry.go
new file mode 100644
index 0000000..f410039
--- /dev/null
+++ b/storage/entry.go
@@ -0,0 +1,24 @@
+package storage
+
+import "fmt"
+
+type (
+ Entry struct {
+ Artist string `json:"artist" form:"artist"`
+ Linkto string `json:"linkto,omitempty" form:"linkto,omitempty"`
+ Track string `json:"track,omitempty" form:"track,omitempty"`
+ Album string `json:"album,omitempty" form:"album,omitempty"`
+ Cover string `json:"cover,omitempty" form:"cover,omitempty"`
+ Date JSTime `json:"date" form:"date"`
+ }
+)
+
+func (entry *Entry) Showname() string {
+ var name string
+ if len(entry.Track) > 0 {
+ name = entry.Track
+ } else {
+ name = entry.Album
+ }
+ return fmt.Sprintf("%s - %s", entry.Artist, name)
+}
diff --git a/storage/entrylist.go b/storage/entrylist.go
new file mode 100644
index 0000000..9b9969d
--- /dev/null
+++ b/storage/entrylist.go
@@ -0,0 +1,175 @@
+package storage
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/gorilla/feeds"
+)
+
+type (
+ EntryList struct {
+ Filename string
+ ModTime time.Time
+ v []*Entry
+ mu sync.RWMutex
+ }
+)
+
+func (list *EntryList) Len() int { return len(list.v) }
+func (list *EntryList) Swap(i, j int) { list.v[i], list.v[j] = list.v[j], list.v[i] }
+func (list *EntryList) Less(i, j int) bool {
+ return time.Time(list.v[i].Date).After(time.Time(list.v[j].Date))
+}
+
+func (list *EntryList) modified() (mod bool) {
+ info, err := os.Stat(list.Filename)
+ if err == nil {
+ mod = list.ModTime.Before(info.ModTime())
+ list.ModTime = info.ModTime()
+ } else {
+ mod = true
+ }
+ return
+}
+
+func (list *EntryList) write() (err error) {
+ data, err := json.MarshalIndent(list.v, "", " ")
+ if err == nil {
+ err = ioutil.WriteFile(list.Filename, data, 0644)
+ }
+ if err == nil {
+ list.ModTime = time.Now()
+ }
+ return
+}
+
+func (list *EntryList) read() (err error) {
+ if list.modified() {
+ list.v = make([]*Entry, 0, len(list.v))
+ if data, err := ioutil.ReadFile(list.Filename); err == nil {
+ err = json.Unmarshal(data, &list.v)
+ }
+ }
+ return
+}
+
+func (list *EntryList) GetEntries() (entry []*Entry, err error) {
+ list.mu.RLock()
+ defer list.mu.RUnlock()
+
+ err = list.read()
+ entry = list.v
+
+ return
+}
+
+func (list *EntryList) AddEntry(entry *Entry) (err error) {
+ list.mu.Lock()
+ defer list.mu.Unlock()
+
+ if err = list.read(); err == nil {
+ list.v = append(list.v, entry)
+ sort.Sort(list)
+ err = list.write()
+ }
+
+ return
+}
+
+func (list *EntryList) ReadSliceList(oval int, nval int) (entries []*Entry, err error) {
+ list.mu.RLock()
+ defer list.mu.RUnlock()
+
+ if nval <= 0 {
+ nval = list.Len()
+ }
+
+ if oval <= 0 {
+ oval = 0
+ }
+
+ err = list.read()
+ if err == nil {
+ if oval >= list.Len() {
+ entries = nil
+ } else if oval+nval >= list.Len() {
+ entries = list.v[oval:]
+ } else {
+ entries = list.v[oval : oval+nval]
+ }
+ }
+
+ return
+}
+
+func (list *EntryList) DeleteElement(pos int) (err error) {
+ list.mu.Lock()
+ defer list.mu.Unlock()
+
+ if err = list.read(); err == nil {
+ if pos >= 0 && pos < list.Len() {
+ if pos == 0 {
+ list.v = list.v[pos+1:]
+ } else if pos == list.Len()-1 {
+ list.v = list.v[:pos]
+ } else {
+ list.v = append(list.v[:pos], list.v[pos+1:]...)
+ }
+
+ err = list.write()
+ }
+ }
+
+ return
+}
+
+// TODO rutas relativas
+func (list *EntryList) GetFeed() (feed *feeds.Feed) {
+ list.mu.RLock()
+ defer list.mu.RUnlock()
+
+ err := list.read()
+
+ if err == nil {
+ feed = &feeds.Feed{
+ Title: "danoloan.es muse",
+ Link: &feeds.Link{Href: "https://danoloan.es/muse"},
+ Description: "bitácora musical de danoloan",
+ Author: &feeds.Author{Name: "danoloan", Email: "danolo@danoloan.es"},
+ }
+
+ feed.Items = make([]*feeds.Item, 0, list.Len())
+ for _, elem := range list.v {
+ item := &feeds.Item{
+ Created: time.Time(elem.Date),
+ }
+
+ if elem.Track != "" {
+ item.Title = elem.Track
+ } else if elem.Album != "" {
+ item.Title = elem.Album
+ }
+
+ item.Description = fmt.Sprintf("%s - %s
\n", item.Title, elem.Artist)
+ if elem.Cover != "" {
+ item.Description = fmt.Sprintf("%s\n", item.Description, elem.Cover)
+ }
+
+ if elem.Linkto != "" {
+ item.Link = &feeds.Link{Href: elem.Linkto}
+ } else {
+ item.Link = &feeds.Link{Href: "https://danoloan.es/muse"}
+ }
+
+ feed.Items = append(feed.Items, item)
+ }
+ }
+
+ return
+}
diff --git a/storage/jstime.go b/storage/jstime.go
new file mode 100644
index 0000000..507901f
--- /dev/null
+++ b/storage/jstime.go
@@ -0,0 +1,42 @@
+package storage
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type (
+ JSTime time.Time
+)
+
+const (
+ jsTimeLite = "2006-01-02"
+ jsTimeLayout = "2006-01-02 15:04:05"
+)
+
+func (ct JSTime) UnmarshalJSON(b []byte) (err error) {
+ s := strings.Trim(string(b), `"`)
+ loc, _ := time.LoadLocation("Europe/Madrid")
+ nt, err := time.ParseInLocation(jsTimeLite, s, loc)
+ ct = JSTime(nt)
+ return
+}
+
+func (ct JSTime) MarshalJSON() ([]byte, error) {
+ return []byte(strconv.Quote(ct.String())), nil
+}
+
+func (ct JSTime) String() string {
+ t := time.Time(ct)
+ return fmt.Sprintf("%s", t.Format(jsTimeLite))
+}
+
+func (ct JSTime) Unix() int64 {
+ return time.Time(ct).Unix()
+}
+
+func (ct JSTime) Local() JSTime {
+ return JSTime(time.Time(ct).Local())
+}
diff --git a/templates/admin.html b/templates/admin.html
index b77f7cc..f082508 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -5,7 +5,7 @@
- splog: zona del admin 😎
+ muse: zona del admin 😎
Zona del admin 😎
@@ -35,10 +35,10 @@
diff --git a/templates/del.html b/templates/del.html
new file mode 100644
index 0000000..0370e8c
--- /dev/null
+++ b/templates/del.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ muse: zona del admin 😎
+
+
+ Zona del admin 😎
+
+
+
+ {{ if .Failed }}
+ El borrado falló ☹️
+ {{ end }}
+
+
+