diff --git a/.gitignore b/.gitignore
index 6f6faf72f..9c634ad86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,8 @@ package-lock.json
.tern-project.js
*_test.go
cover.*
-www
\ No newline at end of file
+www
+*.test.js
+__snapshots__
+.gitignore
+filestash-enterprise
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 3266fc4ca..01c137d47 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ build_frontend:
NODE_ENV=production npm run build
build_backend:
- CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -mod=vendor --tags "fts5" -o dist/filestash main.go
+ CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -mod=vendor --tags "fts5" -o dist/filestash cmd/main.go
clean_frontend:
rm -rf server/ctrl/static/www/
diff --git a/client/pages/adminpage/logger.js b/client/pages/adminpage/logger.js
index 476f289a3..a2a6b7b9e 100644
--- a/client/pages/adminpage/logger.js
+++ b/client/pages/adminpage/logger.js
@@ -154,7 +154,6 @@ function AuditComponent() {
});
return () => ctrl.abort();
}, [debouncedSearchParams]);
-
return (
{
diff --git a/main.go b/cmd/main.go
similarity index 90%
rename from main.go
rename to cmd/main.go
index 55f860e2b..3b60b5753 100644
--- a/main.go
+++ b/cmd/main.go
@@ -1,21 +1,18 @@
package main
import (
- _ "embed"
"os"
"sync"
"github.com/gorilla/mux"
+ "github.com/mickael-kerjean/filestash"
. "github.com/mickael-kerjean/filestash/server"
. "github.com/mickael-kerjean/filestash/server/common"
. "github.com/mickael-kerjean/filestash/server/ctrl"
_ "github.com/mickael-kerjean/filestash/server/plugin"
)
-//go:embed server/plugin/index.go
-var EmbedPluginList []byte
-
func main() {
start(Build(App{}))
}
@@ -39,7 +36,7 @@ func start(routes *mux.Router) {
}()
}
go func() {
- InitPluginList(EmbedPluginList)
+ InitPluginList(embed.EmbedPluginList)
for _, fn := range Hooks.Get.Onload() {
go fn()
}
diff --git a/embed.go b/embed.go
new file mode 100644
index 000000000..450b52f9e
--- /dev/null
+++ b/embed.go
@@ -0,0 +1,24 @@
+package embed
+
+import (
+ "embed"
+ "io/fs"
+ "net/http"
+ "os"
+)
+
+var (
+ //go:embed public
+ wwwPublic embed.FS
+ WWWPublic http.FileSystem = http.FS(os.DirFS("./public/"))
+)
+
+//go:embed server/plugin/index.go
+var EmbedPluginList []byte
+
+func init() {
+ if os.Getenv("DEBUG") != "true" {
+ fsPublic, _ := fs.Sub(wwwPublic, "public")
+ WWWPublic = http.FS(fsPublic)
+ }
+}
diff --git a/public/assets/css/designsystem_alert.css b/public/assets/css/designsystem_alert.css
new file mode 100644
index 000000000..25a69cf95
--- /dev/null
+++ b/public/assets/css/designsystem_alert.css
@@ -0,0 +1,26 @@
+.alert {
+ background: var(--bg-color);
+ border-radius: 5px;
+ padding: 20px;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 1px solid rgba(0,0,0,0.05);
+}
+.alert ol, .alert ul {
+ margin: 5px 0;
+ padding: 0 20px;
+}
+.alert.success{
+ background: var(--success);
+}
+.alert.error{
+ background: var(--error);
+ color: var(--bg-color);
+}
+.alert img{
+ max-width: 100%;
+ border-radius: 5px;
+ border: 10px solid white;
+ box-sizing: border-box;
+ margin-top: 5px;
+}
diff --git a/public/assets/css/designsystem_formbuilder.css b/public/assets/css/designsystem_formbuilder.css
index 551491d48..84d6d57a0 100644
--- a/public/assets/css/designsystem_formbuilder.css
+++ b/public/assets/css/designsystem_formbuilder.css
@@ -18,10 +18,6 @@
font-size: 1em;
padding: 0 15px;
}
-.formbuilder img {
- max-height: 110px;
- border: 8px solid rgba(0, 0, 0, 0);
-}
.formbuilder .fileupload-image img {
height: 150px;
width: 100%;
diff --git a/public/assets/css/designsystem_skeleton.css b/public/assets/css/designsystem_skeleton.css
index 295346d35..4e7060e50 100644
--- a/public/assets/css/designsystem_skeleton.css
+++ b/public/assets/css/designsystem_skeleton.css
@@ -1,4 +1,5 @@
.component_skeleton {
+ width: 100%;
height: 30px;
background: linear-gradient(110deg, rgba(0,0,0,0.02) 8%, rgba(0,0,0,0.04) 18%, rgba(0,0,0,0.02) 33%);
border-radius: 5px;
diff --git a/public/assets/css/reset.css b/public/assets/css/reset.css
index b344cde56..f14ab50a0 100644
--- a/public/assets/css/reset.css
+++ b/public/assets/css/reset.css
@@ -10,40 +10,41 @@
@import url("./designsystem_darkmode.css");
@import url("./designsystem_skeleton.css");
@import url("./designsystem_utils.css");
+@import url("./designsystem_alert.css");
/* latin-ext */
@font-face {
- font-family: 'Source Code Pro';
+ font-family: "Source Code Pro";
font-style: normal;
font-weight: 400;
- src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2) format('woff2');
+ src: local("Source Code Pro"), local("SourceCodePro-Regular"), url(/assets/fonts/SourceCodePro-Regular-400-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
- font-family: 'Source Code Pro';
+ font-family: "Source Code Pro";
font-style: normal;
font-weight: 400;
- src: local('Source Code Pro'), local('SourceCodePro-Regular'), url(/assets/fonts/SourceCodePro-Regular-400-latin.woff2) format('woff2');
+ src: local("Source Code Pro"), local("SourceCodePro-Regular"), url(/assets/fonts/SourceCodePro-Regular-400-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
- font-family: 'Source Code Pro';
+ font-family: "Source Code Pro";
font-style: normal;
font-weight: 600;
- src: local('Source Code Pro Semibold'), local('SourceCodePro-Semibold'), url(/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2) format('woff2');
+ src: local("Source Code Pro Semibold"), local("SourceCodePro-Semibold"), url(/assets/fonts/SourceCodePro-Semibold-600-latin-ext.woff2) format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
- font-family: 'Source Code Pro';
+ font-family: "Source Code Pro";
font-style: normal;
font-weight: 600;
- src: local('Source Code Pro Semibold'), local('SourceCodePro-Semibold'), url(/assets/fonts/SourceCodePro-Semibold-600-latin.woff2) format('woff2');
+ src: local("Source Code Pro Semibold"), local("SourceCodePro-Semibold"), url(/assets/fonts/SourceCodePro-Semibold-600-latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
diff --git a/public/assets/logo/android-chrome-192x192.png b/public/assets/logo/android-chrome-192x192.png
new file mode 100644
index 000000000..6ac841577
Binary files /dev/null and b/public/assets/logo/android-chrome-192x192.png differ
diff --git a/public/assets/logo/android-chrome-512x512.png b/public/assets/logo/android-chrome-512x512.png
new file mode 100644
index 000000000..9c66ee501
Binary files /dev/null and b/public/assets/logo/android-chrome-512x512.png differ
diff --git a/public/assets/logo/app_icon.png b/public/assets/logo/app_icon.png
new file mode 100644
index 000000000..5db95f266
Binary files /dev/null and b/public/assets/logo/app_icon.png differ
diff --git a/public/assets/logo/apple-touch-icon.png b/public/assets/logo/apple-touch-icon.png
new file mode 100644
index 000000000..1e2aaf3d2
Binary files /dev/null and b/public/assets/logo/apple-touch-icon.png differ
diff --git a/public/assets/logo/favicon-16x16.png b/public/assets/logo/favicon-16x16.png
new file mode 100644
index 000000000..2f8b0776f
Binary files /dev/null and b/public/assets/logo/favicon-16x16.png differ
diff --git a/public/assets/logo/favicon-32x32.png b/public/assets/logo/favicon-32x32.png
new file mode 100644
index 000000000..bc64c567f
Binary files /dev/null and b/public/assets/logo/favicon-32x32.png differ
diff --git a/public/assets/logo/favicon.ico b/public/assets/logo/favicon.ico
new file mode 100644
index 000000000..208fead06
Binary files /dev/null and b/public/assets/logo/favicon.ico differ
diff --git a/public/assets/logo/mstile-150x150.png b/public/assets/logo/mstile-150x150.png
new file mode 100644
index 000000000..3eab7c9ce
Binary files /dev/null and b/public/assets/logo/mstile-150x150.png differ
diff --git a/public/assets/logo/og-image.png b/public/assets/logo/og-image.png
new file mode 100644
index 000000000..03376d9e2
Binary files /dev/null and b/public/assets/logo/og-image.png differ
diff --git a/public/assets/logo/safari-pinned-tab.svg b/public/assets/logo/safari-pinned-tab.svg
new file mode 100644
index 000000000..b730344fe
--- /dev/null
+++ b/public/assets/logo/safari-pinned-tab.svg
@@ -0,0 +1,36 @@
+
+
+
diff --git a/public/boot/ctrl_boot_backoffice.js b/public/boot/ctrl_boot_backoffice.js
index c56151bdb..47227fd4d 100644
--- a/public/boot/ctrl_boot_backoffice.js
+++ b/public/boot/ctrl_boot_backoffice.js
@@ -1,5 +1,5 @@
import rxjs, { ajax } from "../lib/rx.js";
-import { loadScript } from "../helpers/loader.js";
+import { loadScript, init as initCSS } from "../helpers/loader.js";
import { report } from "../helpers/log.js";
import { $error } from "./common.js";
@@ -9,6 +9,7 @@ export default async function main() {
setup_device(),
setup_blue_death_screen(),
setup_history(),
+ setup_css(),
]);
window.dispatchEvent(new window.Event("pagechange"));
} catch (err) {
@@ -42,3 +43,7 @@ async function setup_blue_death_screen() {
async function setup_history() {
window.history.replaceState({}, "");
}
+
+async function setup_css() {
+ return initCSS()
+}
diff --git a/public/boot/ctrl_boot_frontoffice.js. b/public/boot/ctrl_boot_frontoffice.js
similarity index 100%
rename from public/boot/ctrl_boot_frontoffice.js.
rename to public/boot/ctrl_boot_frontoffice.js
diff --git a/public/boot/router_backoffice.js b/public/boot/router_backoffice.js
new file mode 100644
index 000000000..5a855e32f
--- /dev/null
+++ b/public/boot/router_backoffice.js
@@ -0,0 +1,13 @@
+const routes = {
+ "/admin/backend": "/pages/adminpage/ctrl_backend.js",
+ "/admin/settings": "/pages/adminpage/ctrl_settings.js",
+ "/admin/logs": "/pages/adminpage/ctrl_log.js",
+ "/admin/about": "/pages/adminpage/ctrl_about.js",
+ "/admin/setup": "/pages/adminpage/ctrl_setup.js",
+ "/admin/": "/pages/ctrl_adminpage.js",
+ "/admin": "/pages/ctrl_adminpage.js",
+ "/logout": "/pages/ctrl_logout.js",
+ "": "/pages/ctrl_notfound.js",
+};
+
+export default routes;
diff --git a/public/boot/router_frontoffice.js b/public/boot/router_frontoffice.js
new file mode 100644
index 000000000..1f9baad39
--- /dev/null
+++ b/public/boot/router_frontoffice.js
@@ -0,0 +1,21 @@
+const routes = {
+ "/login": "/pages/ctrl_connectpage.js",
+ "/logout": "/pages/ctrl_logout.js",
+
+ "/": "/pages/ctrl_homepage.js",
+ "/files/.*": "/pages/ctrl_filespage.js",
+ "/view/.*": "/pages/ctrl_viewerpage.js",
+ // /tags/.* -> "pages/ctrl_tags.js",
+ // /s/.* -> "/pages/ctrl_share.js",
+
+ "/admin/backend": "/pages/adminpage/ctrl_backend.js",
+ "/admin/settings": "/pages/adminpage/ctrl_settings.js",
+ "/admin/logs": "/pages/adminpage/ctrl_logger.js",
+ "/admin/about": "/pages/adminpage/ctrl_about.js",
+ "/admin/setup": "/pages/adminpage/ctrl_setup.js",
+ "/admin/": "/pages/ctrl_adminpage.js",
+
+ "": "/pages/ctrl_notfound.js",
+};
+
+export default routes;
diff --git a/public/components/form.js b/public/components/form.js
index b75ea7afe..35dd851a0 100644
--- a/public/components/form.js
+++ b/public/components/form.js
@@ -57,60 +57,85 @@ function $renderInput(options = {}) {
} = props;
let attr = `name="${path.join(".")}" `;
- if (id) attr += `id="${id}" `;
- if (placeholder) attr += `placeholder="${safe(placeholder, "\"")}" `;
- if (!autocomplete) attr += "autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"off\" ";
+ if (id) attr += `id="${safe(id)}" `;
+ if (placeholder) attr += `placeholder="${safe(placeholder)}" `;
+ if (!autocomplete || props.autocomplete === false) attr += "autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"off\" ";
if (required) attr += "required ";
if (readonly) attr += "readonly ";
switch (type) {
- case "text": // TODO
+ case "text":
+ if (!datalist) return createElement(`
+
+ `);
const dataListId = gid("list_");
const $input = createElement(`
-
- `);
- if (!datalist) return $input;
- const $wrapper = window.document.createElement("span");
- const $datalist = window.document.createElement("datalist");
- $wrapper.appendChild($input);
+
+ `);
+ const $wrapper = document.createElement("span");
+ const $datalist = document.createElement("datalist");
$datalist.setAttribute("id", dataListId);
+ $wrapper.appendChild($input);
+ $wrapper.appendChild($datalist);
+ (props.multi ? multicomplete(value, datalist) : (datalist || [])).forEach((value) => {
+ $datalist.appendChild(createElement(`
`))
+ });
+ if (!props.multi) return $wrapper;
+ $input.refresh = () => {
+ const _datalist = $input.getAttribute("datalist").split(",");
+ multicomplete($input.value, _datalist).forEach((value) => {
+ $datalist.appendChild(createElement(`
`));
+ });
+ };
+ $input.oninput = (e) => {
+ for (const $option of $datalist.children) {
+ $option.remove();
+ }
+ $input.refresh();
+ };
return $wrapper;
case "enable":
return createElement(`
-
-
-
-
- `);
+
+
+
+
+ `);
case "number":
return createElement(`
+
+ `);
+ case "password":
+ const $node = createElement(`
+
+ `);
const $icon = $node.querySelector("component-icon");
if ($icon instanceof window.HTMLElement) {
$icon.onclick = function(e) {
@@ -124,77 +149,94 @@ function $renderInput(options = {}) {
case "long_password":
// TODO
case "long_text":
- return createElement(`
-
- `);
+ const $textarea = createElement(`
+
+ `);
+ if (value) $textarea.value = value;
+ return $textarea;
case "bcrypt":
return createElement(`
-
- `);
- // TODO
+ ${attr}
+ value="${safe(value)}"
+ readonly
+ class="component_input"
+ />
+ `);
case "hidden":
return createElement(`
-
- `);
+
+ `);
case "boolean":
return createElement(`
-
-
-
-
- `);
+
+
+
+
+ `);
case "select":
- const renderOption = (name) => `
`;
+ const renderOption = (name) => {
+ const optName = safe(name);
+ const formVal = safe(value || props.default);
+ return `
+
+ `;
+ }
return createElement(`
-
- `);
+
+ `);
case "date":
return createElement(`
-
- `);
+
+ `);
case "datetime":
return createElement(`
-
- `);
+
+ `);
case "image":
return createElement(`
`);
case "file":
// return createElement() // TODO
default:
return createElement(`
-
- `);
+
+ `);
}
};
}
@@ -213,3 +255,10 @@ export function format(name) {
})
.join(" ");
};
+
+export function multicomplete(input, datalist) {
+ input = input.trim().replace(/,$/g, "");
+ const current = input.split(",").map((val) => val.trim()).filter((t) => !!t);
+ const diff = datalist.filter((x) => current.indexOf(x) === -1);
+ return diff.map((candidate) => input.length === 0 ? candidate : `${input}, ${candidate}`);
+}
diff --git a/public/components/icon.js b/public/components/icon.js
index ada4fdb8d..9b264ecb8 100644
--- a/public/components/icon.js
+++ b/public/components/icon.js
@@ -6,7 +6,9 @@ class Icon extends window.HTMLElement {
attributeChangedCallback() {
const alt = this.getAttribute("name");
const img = this._mapOfIcon(alt);
- this.innerHTML = this.render({ alt, img });
+ requestAnimationFrame(() => {
+ this.innerHTML = this.render({ alt, img });
+ });
}
render({ alt, img }) {
diff --git a/public/components/loader.js b/public/components/loader.js
index 83d6c5e1f..cc3cb71e1 100644
--- a/public/components/loader.js
+++ b/public/components/loader.js
@@ -42,7 +42,7 @@ class Loader extends window.HTMLElement {
}
}
-window.customElements.define("component-loader", Loader);
+customElements.define("component-loader", Loader);
export default createElement("
");
export function toggle($node, show = false) {
diff --git a/public/components/modal.js b/public/components/modal.js
index ed369f3a5..f3becf05c 100644
--- a/public/components/modal.js
+++ b/public/components/modal.js
@@ -1,53 +1,63 @@
-import { createElement } from "../lib/skeleton/index.js";
+import { createElement, nop } from "../lib/skeleton/index.js";
import rxjs, { applyMutation } from "../lib/rx.js";
import { animate } from "../lib/animate.js";
-import { qs } from "../lib/dom.js";
+import { qs, qsa } from "../lib/dom.js";
import { CSS } from "../helpers/loader.js";
-let _observables = [];
-const effect = (obs) => _observables.push(obs.subscribe());
-const free = () => {
- for (let i = 0; i < _observables.length; i++) {
- _observables[i].unsubscribe();
+export default class Modal {
+ static open($node, opts = {}) {
+ find().trigger($node, opts);
}
- _observables = [];
-};
+}
-export default class Modal extends HTMLElement {
- async trigger($node, opts = {}) {
- const { onQuit } = opts;
- const $modal = createElement(`
-
-
-
-
-
`);
+`);
+
+class ModalComponent extends window.HTMLElement {
+ async trigger($node, opts = {}) {
+ const $modal = await createModal();
+ const close$ = new rxjs.Subject();
+ const { onQuit = nop, withButtonsLeft = null, withButtonsRight = null } = opts;
+
+ // feature: build the dom
+ qs($modal, `[data-bind="body"]`).replaceChildren($node);
this.replaceChildren($modal);
+ qsa($modal, `.component_popup > div.buttons > button`).forEach(($button, i) => {
+ let currentLabel = null;
+ if (i === 0) currentLabel = withButtonsLeft;
+ else if (i === 1) currentLabel = withButtonsRight;
- // feature: setup the modal body
- effect(rxjs.of([$node]).pipe(
- applyMutation(qs($modal, "[data-bind=\"body\"]"), "appendChild")
+ if (currentLabel === null) return $button.remove();
+ $button.textContent = currentLabel;
+ $button.onclick = () => close$.next(currentLabel);
+ });
+ effect(rxjs.fromEvent($modal, "click").pipe(
+ rxjs.filter((e) => e.target.getAttribute("id") === "modal-box"),
+ rxjs.tap(() => close$.next()),
+ ));
+ effect(rxjs.fromEvent(window, "keydown").pipe(
+ rxjs.filter((e) => e.keyCode === 27),
+ rxjs.tap(() => close$.next()),
));
// feature: closing the modal
- effect(rxjs.merge(
- rxjs.fromEvent($modal, "click").pipe(
- rxjs.filter((e) => e.target.getAttribute("id") === "modal-box")
- ),
- rxjs.fromEvent(window, "keydown").pipe(
- rxjs.filter((e) => e.keyCode === 27)
- )
- ).pipe(
- rxjs.tap(() => typeof onQuit === "function" && onQuit()),
+ effect(close$.pipe(
+ rxjs.tap((label) => onQuit(label)),
rxjs.tap(() => animate(qs($modal, "div > div"), {
time: 200,
keyframes: [
@@ -91,7 +101,7 @@ export default class Modal extends HTMLElement {
rxjs.map(() => {
let size = 300;
const $box = document.querySelector("#modal-box > div");
- if ($box instanceof HTMLElement) size = $box.offsetHeight;
+ if ($box instanceof window.HTMLElement) size = $box.offsetHeight;
size = Math.round((document.body.offsetHeight - size) / 2);
if (size < 0) return 0;
@@ -104,4 +114,19 @@ export default class Modal extends HTMLElement {
}
}
-customElements.define("component-modal", Modal);
+customElements.define("component-modal", ModalComponent);
+
+let _observables = [];
+const effect = (obs) => _observables.push(obs.subscribe());
+const free = () => {
+ for (let i = 0; i < _observables.length; i++) {
+ _observables[i].unsubscribe();
+ }
+ _observables = [];
+};
+
+function find() {
+ const $dom = document.body.querySelector("component-modal");
+ if (!($dom instanceof ModalComponent)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: wrong type modal component");
+ return $dom;
+}
diff --git a/public/components/notification.css b/public/components/notification.css
new file mode 100644
index 000000000..362f8cfb5
--- /dev/null
+++ b/public/components/notification.css
@@ -0,0 +1,59 @@
+.component_notification {
+ position: fixed;
+ bottom: 20px;
+ left: 20px;
+ right: 70px;
+ font-size: 0.95em;
+ z-index: 1001;
+}
+.component_notification .component_notification--container {
+ overflow: hidden;
+ width: 400px;
+ text-align: left;
+ display: inline-block;
+ padding: 15px 20px 15px 15px;
+ border-radius: 2px;
+ box-shadow: rgba(158, 163, 172, 0.3) 5px 5px 20px;
+ display: flex;
+ align-items: center;
+}
+.component_notification .component_notification--container.info {
+ background: var(--color);
+ color: rgba(255, 255, 255, 0.8);
+}
+.component_notification .component_notification--container.error {
+ background: var(--error);
+ color: rgba(0, 0, 0, 0.5);
+}
+.component_notification .component_notification--container.success {
+ background: var(--success);
+ color: rgba(0, 0, 0, 0.5);
+}
+.component_notification .component_notification--container .message {
+ flex: 1 1 auto;
+ max-height: 92px;
+ max-width: 100%;
+ overflow: hidden;
+}
+.component_notification .component_notification--container .close {
+ cursor: pointer;
+ padding: 0 2px;
+}
+.component_notification .component_notification--container .close .component_icon {
+ height: 18px;
+}
+
+@media (max-width: 490px) {
+ .component_notification {
+ bottom: 0px;
+ left: 0px;
+ }
+ .component_notification .component_notification--container {
+ width: 100%;
+ box-sizing: border-box;
+ }
+}
+
+.component_notification .component_notification--container {
+ box-shadow: rgba(0, 0, 0, 0.3) 5px 5px 20px;
+}
diff --git a/public/components/notification.js b/public/components/notification.js
new file mode 100644
index 000000000..91f42810a
--- /dev/null
+++ b/public/components/notification.js
@@ -0,0 +1,83 @@
+import { createElement } from "../lib/skeleton/index.js";
+import { ApplicationError } from "../lib/error.js";
+import { animate, slideYIn, slideYOut } from "../lib/animate.js";
+import { CSS } from "../helpers/loader.js";
+
+const createNotification = async (msg, type) => createElement(`
+
+
+
+
+
${msg}
+
+
+
+
+
+
+`);
+
+class NotificationComponent extends window.HTMLElement {
+ buffer = [];
+
+ constructor() {
+ super();
+ }
+
+ async trigger(message, type) {
+ if (this.buffer.length > 20) this.buffer.pop(); // failsafe
+ this.buffer.push({ message, type });
+ if (this.buffer.length !== 1) {
+ const $close = this.querySelector(".close");
+ if ($close && typeof $close.onclick === "function") $close.onclick();
+ return;
+ }
+ await this.run();
+ }
+
+ async run() {
+ if (this.buffer.length === 0) return;
+ const { message, type } = this.buffer[0];
+ const $notification = await createNotification(message, type);
+ this.replaceChildren($notification);
+ await animate($notification, {
+ keyframes: slideYIn(50),
+ time: 100,
+ });
+ const ids = []
+ await Promise.race([
+ new Promise((done) => ids.push(window.setTimeout(done, this.buffer.length === 1 ? 8000 : 800))),
+ new Promise((done) => ids.push(window.setTimeout(() => $notification.querySelector(".close").onclick = done, 1000))),
+ ]);
+ ids.forEach((id) => window.clearTimeout(id));
+ await animate($notification, {
+ keyframes: slideYOut(10),
+ time: 200,
+ });
+ $notification.remove();
+ this.buffer.shift();
+ await this.run();
+ }
+}
+
+customElements.define("component-notification", NotificationComponent);
+
+function find() {
+ const $dom = document.body.querySelector("component-notification");
+ if (!($dom instanceof NotificationComponent)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: wrong type notification component");
+ return $dom;
+}
+
+export default class Notification {
+ static info(msg) {
+ find().trigger(msg, "info");
+ }
+
+ static success(msg) {
+ find().trigger(msg, "success");
+ }
+
+ static error(msg) {
+ find().trigger(msg, "error");
+ }
+}
diff --git a/public/helpers/loader.js b/public/helpers/loader.js
index 3a3c144a3..d9615d7fa 100644
--- a/public/helpers/loader.js
+++ b/public/helpers/loader.js
@@ -1,3 +1,7 @@
+import { get as getRelease } from "../pages/adminpage/model_release.js";
+
+let version = null;
+
export async function loadScript(url) {
const $script = document.createElement("script");
$script.setAttribute("src", url);
@@ -14,10 +18,15 @@ export async function CSS(baseURL, ...arrayOfFilenames) {
}
async function loadSingleCSS(baseURL, filename) {
- const res = await fetch(baseURL.replace(/(.*)\/[^\/]+$/, "$1/") + filename + "?version=" + "__", {
- cache: "force-cache"
+ const res = await fetch(baseURL.replace(/(.*)\/[^\/]+$/, "$1/") + `${filename}?version=${version}`, {
+ cache: "force-cache",
});
if (res.status !== 200) return `/* ERROR: ${res.status} */`;
else if (!res.headers.get("Content-Type").startsWith("text/css")) return `/* ERROR: wrong type, got "${res.headers.get("Content-Type")}"*/`;
return await res.text();
}
+
+export async function init() {
+ const info = await getRelease().toPromise();
+ version = info.version;
+}
diff --git a/public/helpers/modal.js b/public/helpers/modal.js
deleted file mode 100644
index 679f451b0..000000000
--- a/public/helpers/modal.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Modal from "../components/modal.js";
-
-// prompt, alert, confirm, modal, popup?
-class ModalManager {
- constructor() {
- this.$dom = document.body.querySelector("component-modal");
- }
-
- alert($node, opts) {
- if (this.$dom instanceof Modal) {
- this.$dom.trigger($node, opts);
- }
- }
-}
-
-export default new ModalManager();
diff --git a/public/index.backoffice.html b/public/index.backoffice.html
new file mode 100644
index 000000000..7db3e18e1
--- /dev/null
+++ b/public/index.backoffice.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
Admin Console
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/index.frontoffice.html b/public/index.frontoffice.html
index ee47597b7..4bf8ebcc4 100644
--- a/public/index.frontoffice.html
+++ b/public/index.frontoffice.html
@@ -14,28 +14,10 @@
diff --git a/public/index.html b/public/index.html
index 840c4d5e4..a9558fabf 100644
--- a/public/index.html
+++ b/public/index.html
@@ -13,24 +13,18 @@