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 @@
- {{ range . }} - + {{ range $index, $elem := . }} + + {{ $elem.Showname }} + {{ end }}
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 😎

+

+

+ ¿Quieres borrar {{ .Elem.Showname }}? + +
+

+ {{ if .Failed }} +

El borrado falló ☹️

+ {{ end }} + + +