management server listener #17
@ -22,27 +22,14 @@ type AdminRouter struct {
|
|||||||
ts *tailscale.LocalClient
|
ts *tailscale.LocalClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type handler struct {
|
type handler map[string]func()
|
||||||
get func()
|
|
||||||
post func()
|
|
||||||
put func()
|
|
||||||
delete func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h handler) Handle(w http.ResponseWriter, req *http.Request) {
|
func (h handler) Handle(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method == http.MethodHead && h.get != nil {
|
if f, ok := h[req.Method]; ok {
|
||||||
h.get()
|
f()
|
||||||
} else if req.Method == http.MethodGet && h.get != nil {
|
return
|
||||||
h.get()
|
|
||||||
} else if req.Method == http.MethodPost && h.post != nil {
|
|
||||||
h.post()
|
|
||||||
} else if req.Method == http.MethodPut && h.put != nil {
|
|
||||||
h.put()
|
|
||||||
} else if req.Method == http.MethodDelete && h.delete != nil {
|
|
||||||
h.delete()
|
|
||||||
} else {
|
|
||||||
badMethod(w)
|
|
||||||
}
|
}
|
||||||
|
badMethod(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeJSONerror(w http.ResponseWriter, err string, status int) {
|
func writeJSONerror(w http.ResponseWriter, err string, status int) {
|
||||||
@ -65,11 +52,11 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/records":
|
case "/api/records":
|
||||||
handler{
|
handler{
|
||||||
get: func() { getRecords(router.rcol, w, r) },
|
http.MethodGet: func() { getRecords(router.rcol, w, r) },
|
||||||
}.Handle(w, r)
|
}.Handle(w, r)
|
||||||
case "/api/books":
|
case "/api/books":
|
||||||
handler{
|
handler{
|
||||||
get: func() { getBooks(router.lib, w, r) },
|
http.MethodGet: func() { getBooks(router.lib, w, r) },
|
||||||
}.Handle(w, r)
|
}.Handle(w, r)
|
||||||
default:
|
default:
|
||||||
static(router.static).ServeHTTP(w, r)
|
static(router.static).ServeHTTP(w, r)
|
||||||
@ -80,13 +67,13 @@ func (router *AdminRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/whoami":
|
case "/api/whoami":
|
||||||
handler{
|
handler{
|
||||||
get: func() { getWhoAmI(router.ts, w, r) },
|
http.MethodGet: func() { getWhoAmI(router.ts, w, r) },
|
||||||
}.Handle(w, r)
|
}.Handle(w, r)
|
||||||
case "/api/books":
|
case "/api/books":
|
||||||
handler{
|
handler{
|
||||||
get: func() { getBooks(router.lib, w, r) },
|
http.MethodGet: func() { getBooks(router.lib, w, r) },
|
||||||
post: func() { addBook(router.lib, w, r) },
|
http.MethodPost: func() { addBook(router.lib, w, r) },
|
||||||
delete: func() { deleteBook(router.lib, w, r) },
|
http.MethodDelete: func() { deleteBook(router.lib, w, r) },
|
||||||
}.Handle(w, r)
|
}.Handle(w, r)
|
||||||
default:
|
default:
|
||||||
static(router.static).ServeHTTP(w, r)
|
static(router.static).ServeHTTP(w, r)
|
||||||
|
217
frontend/admin/app.js
Normal file
217
frontend/admin/app.js
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
var sortState = {
|
||||||
|
sortBy: "sortAuthor",
|
||||||
|
sortOrder: "asc",
|
||||||
|
};
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
fetch("/api/books")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((books) => {
|
||||||
|
// prepare response
|
||||||
|
books.forEach(apiResponseParsing);
|
||||||
|
document.getElementById("search").addEventListener("input", (e) => {
|
||||||
|
renderTable(
|
||||||
|
search(
|
||||||
|
books,
|
||||||
|
e.target.value,
|
||||||
|
document.getElementById("childrens").checked
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
document.getElementById("childrens").addEventListener("change", (e) => {
|
||||||
|
renderTable(
|
||||||
|
search(
|
||||||
|
books,
|
||||||
|
document.getElementById("search").value,
|
||||||
|
e.target.checked
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
renderTable(
|
||||||
|
search(books, "", document.getElementById("childrens").checked)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(books, sortField) {
|
||||||
|
if (sortField) {
|
||||||
|
if (sortState.sortBy === sortField && sortState.sortOrder === "asc") {
|
||||||
|
sortState.sortOrder = "desc";
|
||||||
|
} else {
|
||||||
|
sortState.sortOrder = "asc";
|
||||||
|
}
|
||||||
|
sortState.sortBy = sortField;
|
||||||
|
}
|
||||||
|
books.sort((one, two) =>
|
||||||
|
(one[sortState.sortBy] + one["sortTitle"]).localeCompare(
|
||||||
|
two[sortState.sortBy] + two["sortTitle"]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (sortState.sortOrder === "desc") {
|
||||||
|
books.reverse();
|
||||||
|
}
|
||||||
|
books.forEach((e, i) => (e.rowNumber = i)); // re-key
|
||||||
|
|
||||||
|
// rendering
|
||||||
|
var bookElement = document.getElementById("books");
|
||||||
|
bookElement.innerHTML = TableTemplate(books);
|
||||||
|
|
||||||
|
var bookCount = document.getElementById("bookCount");
|
||||||
|
bookCount.innerHTML = `${books.length} books`;
|
||||||
|
|
||||||
|
// add listeners for selecting book to view
|
||||||
|
Array.from(bookElement.querySelectorAll("tbody tr"))
|
||||||
|
.slice(1) // remove header from Array
|
||||||
|
.forEach((row) => {
|
||||||
|
row.addEventListener("click", (e) => {
|
||||||
|
// add listener to swap current book
|
||||||
|
document.getElementById("current").innerHTML = BookTemplate(
|
||||||
|
books[e.currentTarget.id]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// add sorting callbacks
|
||||||
|
Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach(
|
||||||
|
(row) => {
|
||||||
|
row.addEventListener("click", function (e) {
|
||||||
|
renderTable(books, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// mark currently active column
|
||||||
|
bookElement
|
||||||
|
.querySelector("tbody tr th[data-sort-by=" + sortState.sortBy + "]")
|
||||||
|
.classList.add(sortState.sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiResponseParsing(book) {
|
||||||
|
book.sortTitle = titleCleaner(book.title);
|
||||||
|
if (!book["isbn-10"] && book["isbn-13"]) {
|
||||||
|
book["isbn-10"] = ISBNfromEAN(book["isbn-13"]);
|
||||||
|
}
|
||||||
|
if (!book.coverURL && book["isbn-10"]) {
|
||||||
|
book.coverURL =
|
||||||
|
`https://images-na.ssl-images-amazon.com/images/P/` +
|
||||||
|
book["isbn-10"] +
|
||||||
|
`.01.LZZ.jpg`;
|
||||||
|
}
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
function search(books, searchBy, includeChildrensBooks) {
|
||||||
|
searchBy = searchCleaner(searchBy);
|
||||||
|
books = books.filter(
|
||||||
|
({ title, authors, genre, publisher, series, year, childrens }) => {
|
||||||
|
var inSearch = true;
|
||||||
|
if (searchBy !== "") {
|
||||||
|
inSearch = Object.values({
|
||||||
|
title,
|
||||||
|
authors: authors.join(" "),
|
||||||
|
genre,
|
||||||
|
publisher,
|
||||||
|
series,
|
||||||
|
year,
|
||||||
|
}).find((field) => searchCleaner(field).indexOf(searchBy) !== -1);
|
||||||
|
}
|
||||||
|
if (!includeChildrensBooks) {
|
||||||
|
return inSearch && !childrens;
|
||||||
|
}
|
||||||
|
return inSearch;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return books;
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleCleaner(title) {
|
||||||
|
return title
|
||||||
|
.replace('"', "")
|
||||||
|
.replace(":", "")
|
||||||
|
.replace(/^(An?|The)\s/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchCleaner(str) {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll('"', "")
|
||||||
|
.replaceAll(":", "")
|
||||||
|
.replaceAll("'", "")
|
||||||
|
.replaceAll(" ", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ISBNfromEAN(EAN) {
|
||||||
|
ISBN = EAN.slice(3, 12);
|
||||||
|
var checkdigit =
|
||||||
|
(11 - (ISBN.split("").reduce((s, n, k) => s + n * (10 - k), 0) % 11)) % 11;
|
||||||
|
return ISBN + (checkdigit === 10 ? "X" : checkdigit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookTemplate({
|
||||||
|
"isbn-13": isbn13,
|
||||||
|
"isbn-10": isbn10,
|
||||||
|
authors,
|
||||||
|
coverURL,
|
||||||
|
description,
|
||||||
|
format,
|
||||||
|
notes,
|
||||||
|
publisher,
|
||||||
|
series,
|
||||||
|
signed,
|
||||||
|
title,
|
||||||
|
volume,
|
||||||
|
year,
|
||||||
|
}) {
|
||||||
|
return `<img ${coverURL ? `src="${coverURL}"` : ``}/>
|
||||||
|
<div class="bookDetails">
|
||||||
|
<h1>${title}</h1>
|
||||||
|
<h2>${authors}</h2>
|
||||||
|
<span>${[isbn10, isbn13].filter((v) => v != "").join(" / ")}</span><br/>
|
||||||
|
<span>${publisher}, ${year}</span><br/>
|
||||||
|
${
|
||||||
|
series
|
||||||
|
? `<span>${series}${volume ? `, Volume ${volume}` : ""}</span><br/>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${signed ? "<span>Signed by the author ✒</span><br/>" : ""}
|
||||||
|
<span>${format}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRowTemplate({
|
||||||
|
"isbn-13": isbn13,
|
||||||
|
"isbn-10": isbn10,
|
||||||
|
authors,
|
||||||
|
publisher,
|
||||||
|
rowNumber,
|
||||||
|
signed,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
}) {
|
||||||
|
return `<tr class="tRow" id="${rowNumber}">
|
||||||
|
<td class="title">
|
||||||
|
${title} ${
|
||||||
|
signed ? '<span class="signed" title="Signed by the author" >✒</span>' : ""
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="author">${authors}</td>
|
||||||
|
<td class="publisher">${publisher}</td>
|
||||||
|
<td class="year">${year}</td>
|
||||||
|
<td class="isbn">${isbn13 ? isbn13 : isbn10}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableTemplate(books) {
|
||||||
|
return `<table class="bookTable">
|
||||||
|
<tr>
|
||||||
|
<th data-sort-by="sortTitle" class="tHeader title">Title</th>
|
||||||
|
<th data-sort-by="sortAuthor" class="tHeader author">Author</th>
|
||||||
|
<th data-sort-by="publisher" class="tHeader publisher">Publisher</th>
|
||||||
|
<th data-sort-by="year" class="tHeader year">Year</th>
|
||||||
|
<th class="tHeader isbn">ISBN</th>
|
||||||
|
</tr>${books.reduce((acc, book) => {
|
||||||
|
return acc.concat(TableRowTemplate(book));
|
||||||
|
}, "")} </table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddBookTemplate() {
|
||||||
|
return `<div class="addBook">add book form goes here</div>`;
|
||||||
|
}
|
BIN
frontend/admin/favicon.ico
Normal file
BIN
frontend/admin/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
frontend/admin/favicon.png
Normal file
BIN
frontend/admin/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
@ -1,5 +1,47 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<body>
|
<html lang="en">
|
||||||
hello world
|
<head>
|
||||||
</body>
|
<title>Library</title>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,700&display=swap"
|
||||||
|
as="style"
|
||||||
|
rel="stylesheet preload prefetch"
|
||||||
|
/>
|
||||||
|
<script type="text/javascript" src="app.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
</script>
|
||||||
|
<meta name="description" content="A personal library record." />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div id="header">
|
||||||
|
<h1>Library</h1>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://git.yetaga.in/alazyreader/library"
|
||||||
|
>git</a
|
||||||
|
>
|
||||||
|
<a href="#">add book</a>
|
||||||
|
<div id="searchBox">
|
||||||
|
<label for="childrens" class="bookCount"
|
||||||
|
>Include Childrens Books?</label
|
||||||
|
>
|
||||||
|
<input id="childrens" type="checkbox" name="childrens" />
|
||||||
|
<span id="bookCount" class="bookCount">_ books</span>
|
||||||
|
<input
|
||||||
|
id="search"
|
||||||
|
type="text"
|
||||||
|
name="search"
|
||||||
|
placeholder="Search..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="current">No Book Selected</div>
|
||||||
|
<div id="books"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
284
frontend/admin/style.css
Normal file
284
frontend/admin/style.css
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0 | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: "";
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* site CSS starts here */
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
height: 30px;
|
||||||
|
width: calc(100vw - 20px);
|
||||||
|
padding: 4px 10px;
|
||||||
|
background-color: #f7f3dc;
|
||||||
|
border-bottom: 2px solid #d8d0a0;
|
||||||
|
font-family: "Libre Baskerville", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header h1 {
|
||||||
|
font-size: xx-large;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header .bookCount {
|
||||||
|
font-size: small;
|
||||||
|
color: #a29c77;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBox {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 7px;
|
||||||
|
text-align: right;
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBox input#search {
|
||||||
|
width: 300px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: #f9f8ed;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid #d8d0a0;
|
||||||
|
font-family: "Libre Baskerville", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBox input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchBox input::placeholder {
|
||||||
|
font-family: "Libre Baskerville", sans-serif;
|
||||||
|
color: #d8d0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current {
|
||||||
|
background-color: #f7f3dc;
|
||||||
|
width: calc(40vw - 40px);
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#books {
|
||||||
|
width: calc(60vw - 40px);
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
font-family: "Libre Baskerville", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable th[data-sort-by] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable th[data-sort-by]::after {
|
||||||
|
content: "\2195";
|
||||||
|
position: relative;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable th.asc::after {
|
||||||
|
content: "\2191";
|
||||||
|
font-size: small;
|
||||||
|
position: relative;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable th.desc::after {
|
||||||
|
content: "\2193";
|
||||||
|
font-size: small;
|
||||||
|
position: relative;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable td,
|
||||||
|
.bookTable th {
|
||||||
|
padding: 5px;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tRow:nth-child(odd) {
|
||||||
|
background: #f9f8ed;
|
||||||
|
border-bottom: 1px solid #d8d0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable .tRow {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookTable .tRow .title {
|
||||||
|
font-style: italic;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current h1 {
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current h2 {
|
||||||
|
font-size: large;
|
||||||
|
padding: 7px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current img {
|
||||||
|
opacity: 0.5;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current .bookDetails {
|
||||||
|
position: relative;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
width: 75%;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current .description p {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
@ -8,10 +8,13 @@ import (
|
|||||||
//go:embed files
|
//go:embed files
|
||||||
var static embed.FS
|
var static embed.FS
|
||||||
|
|
||||||
|
//go:embed admin
|
||||||
|
var admin embed.FS
|
||||||
|
|
||||||
func Root() (fs.FS, error) {
|
func Root() (fs.FS, error) {
|
||||||
return fs.Sub(static, "files")
|
return fs.Sub(static, "files")
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminRoot() (fs.FS, error) {
|
func AdminRoot() (fs.FS, error) {
|
||||||
return fs.Sub(static, "admin")
|
return fs.Sub(admin, "admin")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user