Plantillas y dropdowns CSS

This commit is contained in:
danoloan10 2023-07-05 18:55:41 +02:00
parent f5b190e762
commit b9c8278246
6 changed files with 137 additions and 287 deletions

View File

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1">
<link href="/static/style.css" rel="stylesheet" type="text/css" />
<link href="../static/admin.css" rel="stylesheet" type="text/css" />
<title>splog: zona del admin 😎</title>
</head>
<body>
<h1> Zona del admin 😎 </h1>
<div class="action-wrap">
<button class="cajetin">Añadir canción</button>
<form id="add" class="action">
<div class="form-elem"> <input name="artist" type="text" placeholder="Artista"/> </div>
<div class="form-elem"> <input name="track" type="text" placeholder="Canción"/> </div>
<div class="form-elem"> <input name="album" type="text" placeholder="Álbum"/> </div>
<div class="form-elem"> <input name="linkto" type="text" placeholder="Enlace"/> </div>
<div class="form-elem"> <input name="date" type="date" placeholder="Fecha"/> </div>
<hr/>
<input type="submit" value="Añadir">
</form>
</div>
<div class="action-wrap">
<button class="cajetin">Eliminar canción</button>
<div class="action">
<div class="scroll" id="dellist"></div>
</div>
</div>
<footer>
<a href="..">Volver al inicio</a>
</footer>
<script src="static/main.js"></script>
</body>
</html>

259
main.go
View File

@ -1,281 +1,91 @@
package main
import (
"os"
"encoding/json"
"crypto/sha512"
"fmt"
"net/http"
"io/ioutil"
"sort"
"strconv"
"strings"
"sync"
"time"
"encoding/hex"
"html/template"
"io"
"net/http"
"strconv"
"time"
"muse/storage"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/gorilla/feeds"
)
type (
JSTime time.Time
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"`
}
Response struct {
Ok bool `json:"ok"`
Message string `json:"message"`
}
)
type (
entryList struct {
filename string
modtime time.Time
v []Entry
mu sync.RWMutex
TemplateRenderer struct {
template *template.Template
}
Template string
)
const (
jsTimeLayout = "2006-01-02"
adminUsername = "danolo"
adminPassword = "bd4cad796950f50352225de3c773d8f3c39622bc17f34ad661eabe615cdf6d32751c5751e0648dc17d890f40330018334a2ae899878f200f6dc80121ddb70cc9"
)
var (
list *entryList = &entryList{
filename: "/var/lib/muse.json",
modtime: time.Unix(0, 0),
list *storage.EntryList = &storage.EntryList{
Filename: "/var/lib/muse.json",
ModTime: time.Unix(0, 0),
}
)
func (ct *JSTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), `"`)
nt, err := time.Parse(jsTimeLayout, s)
*ct = JSTime(nt)
return
// TemplateRenderer
func (renderer *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return renderer.template.ExecuteTemplate(w, name, data)
}
func (ct JSTime) MarshalJSON() ([]byte, error) {
return []byte(ct.String()), nil
}
func (ct *JSTime) String() string {
t := time.Time(*ct)
return fmt.Sprintf("%q", t.Format(jsTimeLayout))
}
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)
func (template Template) TemplateController(c echo.Context) (err error) {
entries, err := list.GetEntries()
if err == nil {
mod = list.modtime.Before(info.ModTime())
list.modtime = info.ModTime()
} else {
mod = true
err = c.Render(http.StatusOK, string(template), entries)
}
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) 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("<p>%s - %s</p>\n", item.Title, elem.Artist)
if elem.Cover != ""{
item.Description = fmt.Sprintf("%s<img src=\"%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
}
func ListController(c echo.Context) (res error) {
var err error
var oval, nval int
oval, err = strconv.Atoi(c.QueryParam("o"))
if err != nil {
oval = 0
}
nval, err = strconv.Atoi(c.QueryParam("n"))
if err != nil {
nval = 0
}
slice, err := list.readSliceList(oval, nval)
if err == nil {
res = c.JSON(http.StatusOK, slice)
} else {
response := Response{ false, "Error fetching song list" }
res = c.JSON(http.StatusInternalServerError, response)
}
return
}
func AddController(c echo.Context) (error) {
func AddController(c echo.Context) error {
var res Response
var status int
var err error
entry := new(Entry)
entry := new(storage.Entry)
if err = c.Bind(entry); err != nil {
res = Response{ false, err.Error() }
res = Response{false, err.Error()}
status = http.StatusBadRequest
} else if err = list.addEntry(entry); err != nil {
res = Response{ false, "Failed to write into list" }
} else if err = list.AddEntry(entry); err != nil {
res = Response{false, "Failed to write into list"}
status = http.StatusInternalServerError
} else {
res = Response{ true, "OK" }
res = Response{true, "OK"}
status = http.StatusOK
}
return c.JSON(status, res)
}
func DelController(c echo.Context) (error) {
func DelController(c echo.Context) error {
var res Response
var status int
var pos int
pos, err := strconv.Atoi(c.QueryParam("p"))
if err == nil {
err = list.deleteElement(pos)
err = list.DeleteElement(pos)
}
if err == nil {
res = Response{ true, "OK" }
res = Response{true, "OK"}
status = http.StatusOK
} else {
res = Response{ false, "Failed to delete from list" }
res = Response{false, "Failed to delete from list"}
status = http.StatusInternalServerError
}
@ -292,7 +102,7 @@ func auth(username, password string, c echo.Context) (bool, error) {
}
func RSSController(c echo.Context) (err error) {
if blob, err := list.getFeed().ToRss(); err == nil {
if blob, err := list.GetFeed().ToRss(); err == nil {
c.Blob(http.StatusOK, "application/xml", []byte(blob))
}
return
@ -301,16 +111,19 @@ func RSSController(c echo.Context) (err error) {
func main() {
e := echo.New()
e.GET("/list", ListController)
e.Renderer = &TemplateRenderer{
template: template.Must(template.ParseGlob("templates/*.html")),
}
e.GET("/", Template("index.html").TemplateController)
e.GET("/rss", RSSController)
e.Static("/", "index.html")
e.Static("/static", "static")
admin := e.Group("/admin", middleware.BasicAuth(auth))
admin.POST("/add", AddController)
admin.GET("/del", DelController)
admin.Static("/", "admin/index.html")
admin.GET("/", Template("admin.html").TemplateController)
admin.Static("/static", "admin/static")
e.Logger.Fatal(e.Start(":30303"))

View File

@ -1,3 +1,7 @@
* {
max-width: inherit;
}
input {
width: 100%;
box-sizing: border-box;
@ -17,17 +21,6 @@ footer > a {
display: inline-block;
}
.action {
padding: 20px;
}
.action[result]::after {
content: attr(result);
display: block;
padding-top: 1em;
text-align: center;
font-size: 0.75em;
}
.action-wrap {
margin: 0.5em auto;
border-radius: 6px;
@ -61,11 +54,41 @@ footer > a {
overflow: auto;
}
.cajetin {
/* Dropdown */
button.dropdown {
width: 100%;
display: block;
padding: 0.5em;
}
button.dropdown {
padding: 0;
}
button.dropdown > input[type=checkbox] {
display: none;
}
button.dropdown > label {
padding: 0.5em;
display: block;
cursor: pointer;
}
button.dropdown:has(input[type=checkbox]:checked) ~ .action {
max-height: 50vh;
padding: 20px;
}
.action {
max-height: 0;
padding: 0;
transition: max-height 0.2s linear;
}
.action[result]::after {
content: attr(result);
display: block;
padding-top: 1em;
text-align: center;
font-size: 0.75em;
}
.data-elem {
display: block;

View File

@ -58,12 +58,14 @@ button,
.button {
cursor: pointer;
}
input[type=checkbox].button:hover ~ button,
input[type=button]:hover,
input[type=submit]:hover,
button:hover,
.button:hover {
background-color: #444;
}
input[type=checkbox].button:active ~ button,
input[type=button]:active,
input[type=submit]:active,
button:active,
@ -100,6 +102,7 @@ hr {
background-color: #eee;
}
input[type=checkbox].button:hover ~ button,
input[type=button]:hover,
input[type=submit]:hover,
button:hover,

50
templates/admin.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1">
<link href="/static/style.css" rel="stylesheet" type="text/css" />
<link href="../static/admin.css" rel="stylesheet" type="text/css" />
<title>splog: zona del admin 😎</title>
</head>
<body>
<h1> Zona del admin 😎 </h1>
<div class="action-wrap">
<button class="dropdown">
<input id="check-add" class="dropdown" type="checkbox"/>
<label for="check-add">
Añadir canción
</label>
</button>
<form id="add" class="action">
<div class="form-elem"> <input name="artist" type="text" placeholder="Artista"/> </div>
<div class="form-elem"> <input name="track" type="text" placeholder="Canción"/> </div>
<div class="form-elem"> <input name="album" type="text" placeholder="Álbum"/> </div>
<div class="form-elem"> <input name="linkto" type="text" placeholder="Enlace"/> </div>
<div class="form-elem"> <input name="date" type="date" placeholder="Fecha"/> </div>
<hr/>
<input type="submit" value="Añadir">
</form>
</div>
<div class="action-wrap">
<button class="dropdown">
<input id="check-del" class="dropdown" type="checkbox"/>
<label for="check-del">
Eliminar canción
</label>
</button>
<div class="action">
<div class="scroll">
{{ range . }}
<button class="data-elem">
{{ .Showname }}
</button>
{{ end }}
</div>
</div>
</div>
<footer>
<a href="..">Volver al inicio</a>
</footer>
</body>
</html>

View File

@ -20,7 +20,7 @@ body {
display: inline-table;
background-color: #333;
color: #ddd;
transition: transform .1s;
transition: transform .1s;
box-sizing: border-box;
word-break: break-word;
font-size: calc(0.5em + 1vw);
@ -31,6 +31,10 @@ body {
background-color: #373737;
}
main > a {
font-size: 0px;
}
.title {
margin: 10px 20px;
font-weight: bold;
@ -122,8 +126,6 @@ nav > a:hover {
}
</style>
<script src="static/prog.js"></script>
<script src="static/main.js"></script>
</head>
<body>
<header>
@ -148,22 +150,16 @@ nav > a:hover {
<a href="admin/">Zona del admin <span class="emoji">😎</span></a>
</nav>
<main>
<noscript>
<div style="font-size: 15px; text-align: left; max-width: 600px; margin:auto">
<p>
Esta página utiliza código JavaScript en el navegador
para generar los contenidos en pantalla.
Por favor, active JavaScript para poder usarla.
</p>
<p>
El código utilizado por la página es bastante sencillo,
si lo quiere revisar pinche
<a href="static/main.js">aquí</a>.
{{ range . }}
<a href="{{ .Linkto }}">
<div class="elem">
<img src="{{ .Cover }}" class="cover" loading="lazy">
<div class="title">{{ .Showname }}</div>
<div class="date">{{ .Date }}</div>
</p>
</div>
</noscript>
</a>
{{ end }}
</main>
<div id="load">
</div>
</body>
</html>