mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-14 12:26:53 +01:00
feature (refactoring): start of the frontend refactoring
This commit is contained in:
parent
c2059c839d
commit
42f5434dfe
35 changed files with 1337 additions and 0 deletions
16
public/README.org
Normal file
16
public/README.org
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
WIP of the complete frontend rewrite in vanilla JS
|
||||
|
||||
Admin section:
|
||||
- [-] pages:
|
||||
- [ ] backend
|
||||
- [ ] settings
|
||||
- [ ] logs
|
||||
- [ ] about
|
||||
- [ ] setup
|
||||
- [X] home
|
||||
- [ ] login
|
||||
- [ ] side bar
|
||||
- [ ] form
|
||||
|
||||
End user section:
|
||||
TODO
|
||||
187
public/assets/css/reset.css
Normal file
187
public/assets/css/reset.css
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/* latin-ext */
|
||||
@font-face {
|
||||
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');
|
||||
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-style: normal;
|
||||
font-weight: 400;
|
||||
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-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');
|
||||
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-style: normal;
|
||||
font-weight: 600;
|
||||
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;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-color: #fafafa;
|
||||
--color: #57595A;
|
||||
--emphasis: #466372;
|
||||
--primary: #9AD1ED;
|
||||
--emphasis-primary: #c5e2f1;
|
||||
--emphasis-secondary: #466372;
|
||||
--light: #909090;
|
||||
--super-light: #f9fafc;
|
||||
--error: #f26d6d;
|
||||
--success: #63d9b1;
|
||||
--dark: #313538;
|
||||
}
|
||||
body.dark-mode {
|
||||
--bg-color: #242424;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Roboto", sans-serif;
|
||||
-webkit-text-size-adjust:100%;
|
||||
}
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: var(--bg-color);
|
||||
color: var(--color);
|
||||
}
|
||||
body, html, #app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
body.dark-mode{ background: var(--bg-color); }
|
||||
body.dark-mode .component_modal > div,
|
||||
body.dark-mode .component_page_admin{ --bg-color: #f2f3f5; }
|
||||
|
||||
.center{ text-align: center; }
|
||||
a { text-decoration: none; }
|
||||
|
||||
|
||||
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
select:-moz-focusring {
|
||||
color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
transition: border 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
a:focus,
|
||||
a:active,
|
||||
button::-moz-focus-inner,
|
||||
input[type="reset"]::-moz-focus-inner,
|
||||
input[type="button"]::-moz-focus-inner,
|
||||
input[type="submit"]::-moz-focus-inner,
|
||||
select::-moz-focus-inner,
|
||||
input[type="file"]>input[type="button"]::-moz-focus-inner {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
select:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 #000;
|
||||
}
|
||||
|
||||
.connect-form input:hover,
|
||||
.connect-form textarea:hover,
|
||||
.connect-form input:focus,
|
||||
.connect-form textarea:focus {
|
||||
border-color: rgb(154, 209, 237)!important;
|
||||
}
|
||||
|
||||
.drag-drop {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.drag-drop.dragging>div {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
/* CONNECTION FORM */
|
||||
|
||||
.login-form button.active {
|
||||
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.20);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 2px;
|
||||
-moz-border-radius: 2px;
|
||||
-ms-border-radius: 2px;
|
||||
-o-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.scroll-y {
|
||||
overflow-y: scroll;
|
||||
scrollbar-3dlight-color: #7d7e94;
|
||||
scrollbar-arrow-color: #c1c1d1;
|
||||
scrollbar-darkshadow-color: #2d2c4d;
|
||||
scrollbar-face-color: rgba(0, 0, 0, .1);
|
||||
scrollbar-highlight-color: #7d7e94;
|
||||
scrollbar-shadow-color: #2d2c4d;
|
||||
scrollbar-track-color: rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hidden{
|
||||
position:absolute;
|
||||
left:-10000px;
|
||||
top:auto;
|
||||
width:1px;
|
||||
height:1px;
|
||||
overflow:hidden;
|
||||
}
|
||||
39
public/components/loader.js
Normal file
39
public/components/loader.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
class Loader extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.innerHTML = this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<style>${CSS}</style>
|
||||
<div className="component_loader">
|
||||
<svg width="120px" height="120px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<rect x="0" y="0" width="100" height="100" fill="none"></rect>
|
||||
<circle cx="50" cy="50" r="40" stroke="rgba(100%,100%,100%,0.679)" fill="none" strokeWidth="10" strokeLinecap="round"></circle>
|
||||
<circle cx="50" cy="50" r="40" stroke="#6f6f6f" fill="none" strokeWidth="6" strokeLinecap="round">
|
||||
<animate attributeName="stroke-dashoffset" dur="2s" repeatCount="indefinite" from="0" to="502"></animate>
|
||||
<animate attributeName="stroke-dasharray" dur="2s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate>
|
||||
</circle>
|
||||
</svg>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("data-loader", Loader);
|
||||
|
||||
const CSS = `
|
||||
.component_loader{
|
||||
text-align: center;
|
||||
margin: 50px auto 0 auto;
|
||||
}
|
||||
|
||||
.loader-appear{
|
||||
opacity: 0;
|
||||
}
|
||||
.loader-appear.loader-appear-active{
|
||||
transition: opacity 0.2s ease-out;
|
||||
transition-delay: 0.5s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
`
|
||||
13
public/components/modal.js
Normal file
13
public/components/modal.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { createElement } from "../common/skeleton/index.js";
|
||||
|
||||
export function prompt(label, okFn, errFn) {
|
||||
const $node = createElement(`
|
||||
<dialog open>
|
||||
<p>Greetings, one and all!</p>
|
||||
<form method="dialog">
|
||||
<button>OK</button>
|
||||
</form>
|
||||
</dialog>`);
|
||||
document.body.appendChild($node);
|
||||
okFn("OK")
|
||||
}
|
||||
39
public/index.html
Normal file
39
public/index.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<base href="/">
|
||||
<script type="module" src="./pages/boot/nyancat.js"></script>
|
||||
<link rel="stylesheet" href="./assets/css/reset.css">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<div role="main" id="app">
|
||||
<data-nyancat></data-nyancat>
|
||||
</div>
|
||||
<script type="module" src="./components/loader.js"></script>
|
||||
<script type="module">
|
||||
import main from "./lib/skeleton/index.js";
|
||||
const routes = {
|
||||
"/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": "pages/adminpage/index.js",
|
||||
"/": "pages/home/index.js",
|
||||
};
|
||||
main(document.getElementById("app"), routes, {
|
||||
loader: `<data-loader></data-loader>`
|
||||
});
|
||||
</script>
|
||||
<noscript>
|
||||
<div>
|
||||
<h2>Error: Javascript is off</h2>
|
||||
<p>You need to enable Javascript to run this application</p>
|
||||
</div>
|
||||
</noscript>
|
||||
</body>
|
||||
</html>
|
||||
110
public/lib/animate/animation.js
Normal file
110
public/lib/animate/animation.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import rxjs from "../rxjs/index.js";
|
||||
|
||||
export async function requestAnimation() {
|
||||
return new Promise((done) => requestAnimationFrame(done));
|
||||
}
|
||||
|
||||
export async function enterAnimation($node, timeout) {
|
||||
$node.classList.remove("leave", "leave-active", "enter", "enter-active");
|
||||
await requestAnimation();
|
||||
$node.classList.add("enter");
|
||||
await requestAnimation();
|
||||
$node.classList.add("enter-active")
|
||||
await rxjs.timer(timeout).toPromise();
|
||||
$node.classList.remove("enter", "enter-active");
|
||||
}
|
||||
|
||||
export async function leaveAnimation($node, timeout) {
|
||||
$node.classList.remove("leave", "leave-active", "enter", "enter-active");
|
||||
await requestAnimation();
|
||||
$node.classList.add("leave");
|
||||
await requestAnimation();
|
||||
$node.classList.add("leave-active")
|
||||
await rxjs.timer(timeout).toPromise();
|
||||
}
|
||||
|
||||
export function slideXIn(size) {
|
||||
return function (querySelector, t){
|
||||
return `
|
||||
${querySelector}.enter {
|
||||
opacity: 0;
|
||||
transform: translateX(${size}px);
|
||||
}
|
||||
${querySelector}.enter.enter-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: all ${t}ms ease;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function slideYIn(size) {
|
||||
return function (querySelector, t){
|
||||
return `
|
||||
${querySelector}.enter {
|
||||
opacity: 0;
|
||||
transform: translateY(${size}px);
|
||||
}
|
||||
${querySelector}.enter.enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: all ${t}ms ease;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function slideXOut(size) {
|
||||
return function (querySelector, t){
|
||||
return `
|
||||
${querySelector}.leave {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
${querySelector}.leave.leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(${size}px);
|
||||
transition: all ${t}ms ease;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function slideYOut(size) {
|
||||
return function (querySelector, t){
|
||||
return `
|
||||
${querySelector}.leave {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
${querySelector}.leave.leave-active {
|
||||
opacity: 0;
|
||||
transform: translateY(${size}px);
|
||||
transition: all ${t}ms ease;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function opacityIn() {
|
||||
return function (querySelector, t){
|
||||
return `
|
||||
${querySelector}.enter {
|
||||
opacity: 0;
|
||||
}
|
||||
${querySelector}.enter.enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity ${t}ms ease;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function opacityOut() {
|
||||
return function (querySelector, t){
|
||||
return `
|
||||
${querySelector}.leave {
|
||||
opacity: 1;
|
||||
}
|
||||
${querySelector}.leave.leave-active {
|
||||
opacity: 0;
|
||||
transition: opacity ${t}ms ease;
|
||||
}`;
|
||||
}
|
||||
}
|
||||
40
public/lib/animate/index.js
Normal file
40
public/lib/animate/index.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { onDestroy, createElement } from "../skeleton/index.js";
|
||||
import rxjs from "../rxjs/index.js";
|
||||
import {
|
||||
requestAnimation, enterAnimation, leaveAnimation,
|
||||
slideXIn, opacityOut,
|
||||
} from "./animation.js";
|
||||
|
||||
export {
|
||||
slideXIn, slideXOut,
|
||||
slideYIn, slideYOut,
|
||||
opacityIn, opacityOut
|
||||
} from "./animation.js";
|
||||
|
||||
export function animate($node, opts = {}) {
|
||||
const { timeoutEnter = 200, timeoutLeave = 100 } = opts;
|
||||
return rxjs.of({ $node: $node, timeoutEnter, timeoutLeave });
|
||||
}
|
||||
|
||||
export function CSSTransition(opts = {}) {
|
||||
const { enter = slideXIn(3), leave = opacityOut() } = opts;
|
||||
return rxjs.pipe(
|
||||
rxjs.tap(({$node, timeoutEnter, timeoutLeave}) => {
|
||||
if ($node.classList.value === "") { // if node has no class, assign a random one
|
||||
$node.classList.add((Math.random() + 1).toString(36).substring(2));
|
||||
}
|
||||
const className = (" " + $node.classList.value).split(" ").join(".");
|
||||
|
||||
let css = "";
|
||||
if (timeoutEnter && typeof enter === "function") css += enter(className, timeoutEnter);
|
||||
if (timeoutLeave && typeof leave === "function") css += leave(className, timeoutLeave);
|
||||
if (css) $node.appendChild(createElement(`<style>${css}</style>`));
|
||||
}),
|
||||
rxjs.tap(({$node, timeoutEnter, timeoutLeave}) => {
|
||||
if (timeoutEnter && enter) enterAnimation($node, timeoutEnter);
|
||||
if (timeoutLeave && leave) onDestroy(async () => {
|
||||
await leaveAnimation($node, timeoutLeave);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
31
public/lib/rxjs/dom.js
Normal file
31
public/lib/rxjs/dom.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import rxjs from "./index.js";
|
||||
|
||||
export function textContent($node, selector = "") {
|
||||
if (selector) $node = $node.querySelector(selector);
|
||||
if (!$node) throw new Error("dom not found for '" + selector + "'");
|
||||
return rxjs.tap((val) => $node.textContent = val);
|
||||
}
|
||||
|
||||
export function htmlContent($node, selector = "") {
|
||||
if (selector) $node = $node.querySelector(selector);
|
||||
if (!$node) throw new Error("dom not found for '" + selector + "'");
|
||||
return rxjs.tap((val) => $node.innerHTML = val);
|
||||
}
|
||||
|
||||
export function setAttribute($node, selector = "", attr = "") {
|
||||
if (selector) $node = $node.querySelector(selector);
|
||||
if (!$node) throw new Error("dom not found for '" + selector + "'");
|
||||
return rxjs.tap((val) => ($node.getAttribute(attr) != val) && $node.setAttribute(attr, val));
|
||||
}
|
||||
|
||||
export function removeAttribute($node, selector = "", attr = "") {
|
||||
if (selector) $node = $node.querySelector(selector);
|
||||
if (!$node) throw new Error("dom not found for '" + selector + "'");
|
||||
return rxjs.tap(() => $node.removeAttribute(attr));
|
||||
}
|
||||
|
||||
export function getAttribute($node, selector = "", attr = "") {
|
||||
if (selector) $node = $node.querySelector(selector);
|
||||
if (!$node) throw new Error("dom not found for '" + selector + "'");
|
||||
return rxjs.map(() => $node.getAttribute(attr))
|
||||
}
|
||||
13
public/lib/rxjs/index.js
Normal file
13
public/lib/rxjs/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { onDestroy } from "../skeleton/index.js";
|
||||
|
||||
// https://github.com/ReactiveX/rxjs/issues/4416#issuecomment-620847759
|
||||
const rxjsModule = await import("./vendor/rxjs.min.js");
|
||||
const ajaxModule = await import("./vendor/rxjs-ajax.min.js")
|
||||
|
||||
export default rxjsModule;
|
||||
export { textContent, htmlContent, setAttribute, getAttribute, removeAttribute } from "./dom.js";
|
||||
export const ajax = ajaxModule.ajax;
|
||||
export function withEffect(obs) {
|
||||
const tmp = obs.subscribe(() => {}, (err) => console.error("withEffect", err));
|
||||
onDestroy(() => tmp.unsubscribe());
|
||||
}
|
||||
2
public/lib/rxjs/vendor/rxjs-ajax.min.js
vendored
Normal file
2
public/lib/rxjs/vendor/rxjs-ajax.min.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/* rxjs@7.8.1 */
|
||||
export{ca as AjaxError,cc as AjaxResponse,cb as AjaxTimeoutError,c9 as ajax}from"./rxjs-shared.min.js";
|
||||
2
public/lib/rxjs/vendor/rxjs-operators.min.js
vendored
Normal file
2
public/lib/rxjs/vendor/rxjs-operators.min.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/* rxjs@7.8.1 */
|
||||
export{an as audit,ao as auditTime,ap as buffer,aq as bufferCount,ar as bufferTime,as as bufferToggle,at as bufferWhen,au as catchError,av as combineAll,c2 as combineLatest,aw as combineLatestAll,ax as combineLatestWith,c3 as concat,ay as concatAll,az as concatMap,aA as concatMapTo,aB as concatWith,aC as connect,aD as count,aE as debounce,aF as debounceTime,aG as defaultIfEmpty,aH as delay,aI as delayWhen,aJ as dematerialize,aK as distinct,aL as distinctUntilChanged,aM as distinctUntilKeyChanged,aN as elementAt,aO as endWith,aP as every,aQ as exhaust,aR as exhaustAll,aS as exhaustMap,aT as expand,aU as filter,aV as finalize,aW as find,aX as findIndex,aY as first,b6 as flatMap,aZ as groupBy,a_ as ignoreElements,a$ as isEmpty,b0 as last,b1 as map,b2 as mapTo,b3 as materialize,b4 as max,c4 as merge,b5 as mergeAll,b7 as mergeMap,b8 as mergeMapTo,b9 as mergeScan,ba as mergeWith,bb as min,bc as multicast,bd as observeOn,c5 as onErrorResumeNext,bf as pairwise,c6 as partition,bg as pluck,bh as publish,bi as publishBehavior,bj as publishLast,bk as publishReplay,c7 as race,bl as raceWith,bm as reduce,br as refCount,bn as repeat,bo as repeatWhen,bp as retry,bq as retryWhen,bs as sample,bt as sampleTime,bu as scan,bv as sequenceEqual,bw as share,bx as shareReplay,by as single,bz as skip,bA as skipLast,bB as skipUntil,bC as skipWhile,bD as startWith,bE as subscribeOn,bF as switchAll,bG as switchMap,bH as switchMapTo,bI as switchScan,bJ as take,bK as takeLast,bL as takeUntil,bM as takeWhile,bN as tap,bO as throttle,bP as throttleTime,bQ as throwIfEmpty,bR as timeInterval,bS as timeout,bT as timeoutWith,bU as timestamp,bV as toArray,bW as window,bX as windowCount,bY as windowTime,bZ as windowToggle,b_ as windowWhen,b$ as withLatestFrom,c8 as zip,c0 as zipAll,c1 as zipWith}from"./rxjs-shared.min.js";
|
||||
2
public/lib/rxjs/vendor/rxjs-shared.min.js
vendored
Normal file
2
public/lib/rxjs/vendor/rxjs-shared.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/lib/rxjs/vendor/rxjs.min.js
vendored
Normal file
2
public/lib/rxjs/vendor/rxjs.min.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/* rxjs@7.8.1 */
|
||||
export{J as ArgumentOutOfRangeError,A as AsyncSubject,B as BehaviorSubject,h as ConnectableObservable,ak as EMPTY,K as EmptyError,al as NEVER,L as NotFoundError,N as Notification,z as NotificationKind,M as ObjectUnsubscribedError,O as Observable,R as ReplaySubject,x as Scheduler,P as SequenceError,b as Subject,y as Subscriber,S as Subscription,T as TimeoutError,U as UnsubscriptionError,c as VirtualAction,V as VirtualTimeScheduler,v as animationFrame,w as animationFrameScheduler,k as animationFrames,l as asap,m as asapScheduler,q as async,r as asyncScheduler,an as audit,ao as auditTime,Q as bindCallback,W as bindNodeCallback,ap as buffer,aq as bufferCount,ar as bufferTime,as as bufferToggle,at as bufferWhen,au as catchError,av as combineAll,X as combineLatest,aw as combineLatestAll,ax as combineLatestWith,Y as concat,ay as concatAll,az as concatMap,aA as concatMapTo,aB as concatWith,am as config,aC as connect,Z as connectable,aD as count,aE as debounce,aF as debounceTime,aG as defaultIfEmpty,_ as defer,aH as delay,aI as delayWhen,aJ as dematerialize,aK as distinct,aL as distinctUntilChanged,aM as distinctUntilKeyChanged,aN as elementAt,$ as empty,aO as endWith,aP as every,aQ as exhaust,aR as exhaustAll,aS as exhaustMap,aT as expand,aU as filter,aV as finalize,aW as find,aX as findIndex,aY as first,I as firstValueFrom,b6 as flatMap,a0 as forkJoin,a1 as from,a2 as fromEvent,a3 as fromEventPattern,a4 as generate,aZ as groupBy,F as identity,a_ as ignoreElements,a5 as iif,a6 as interval,a$ as isEmpty,G as isObservable,b0 as last,H as lastValueFrom,b1 as map,b2 as mapTo,b3 as materialize,b4 as max,a7 as merge,b5 as mergeAll,b7 as mergeMap,b8 as mergeMapTo,b9 as mergeScan,ba as mergeWith,bb as min,bc as multicast,a8 as never,E as noop,j as observable,bd as observeOn,a9 as of,aa as onErrorResumeNext,be as onErrorResumeNextWith,ab as pairs,bf as pairwise,ac as partition,D as pipe,bg as pluck,bh as publish,bi as publishBehavior,bj as publishLast,bk as publishReplay,s as queue,u as queueScheduler,ad as race,bl as raceWith,ae as range,bm as reduce,br as refCount,bn as repeat,bo as repeatWhen,bp as retry,bq as retryWhen,bs as sample,bt as sampleTime,bu as scan,aj as scheduled,bv as sequenceEqual,bw as share,bx as shareReplay,by as single,bz as skip,bA as skipLast,bB as skipUntil,bC as skipWhile,bD as startWith,bE as subscribeOn,bF as switchAll,bG as switchMap,bH as switchMapTo,bI as switchScan,bJ as take,bK as takeLast,bL as takeUntil,bM as takeWhile,bN as tap,bO as throttle,bP as throttleTime,af as throwError,bQ as throwIfEmpty,bR as timeInterval,bS as timeout,bT as timeoutWith,ag as timer,bU as timestamp,bV as toArray,ah as using,bW as window,bX as windowCount,bY as windowTime,bZ as windowToggle,b_ as windowWhen,b$ as withLatestFrom,ai as zip,c0 as zipAll,c1 as zipWith}from"./rxjs-shared.min.js";
|
||||
40
public/lib/skeleton/index.js
Normal file
40
public/lib/skeleton/index.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { init as initRouter, currentRoute } from "./router.js";
|
||||
import { init as initDOM } from "./lifecycle.js";
|
||||
|
||||
export { navigate } from "./router.js";
|
||||
export { onDestroy } from "./lifecycle.js";
|
||||
|
||||
export default async function($root, routes, opts = {}) {
|
||||
const { spinner = "loading ...", spinnerTime = 200, defaultRoute = "/", onload = () => {} } = opts;
|
||||
|
||||
initDOM($root);
|
||||
initRouter($root);
|
||||
|
||||
window.addEventListener("pagechange", async () => {
|
||||
await $root.cleanup();
|
||||
const route = currentRoute(routes, defaultRoute);
|
||||
let ctrl;
|
||||
if (typeof route === "function") {
|
||||
ctrl = route;
|
||||
} else if (typeof route === "string") {
|
||||
const spinnerID = (typeof spinner === "string") && setTimeout(() => $root.innerHTML = spinner, spinnerTime);
|
||||
const module = await import("../../" + route);
|
||||
clearTimeout(spinnerID);
|
||||
if (typeof module.default !== "function") return $root.replaceChildren(createElement(`<div><h1>Error</h1><p>missing default export on ${route}`));
|
||||
ctrl = module.default;
|
||||
}
|
||||
if (typeof ctrl !== "function") return $root.replaceChildren(createElement(`<div><h1>Error</h1><p>Unknown route for ${route}`));
|
||||
ctrl((view) => {
|
||||
if (typeof view === "string") $root.replaceChildren(createElement(view));
|
||||
else if (view instanceof window.Element) $root.replaceChildren(view);
|
||||
else $root.replaceChildren(createElement(`<div><h1>Error</h1><p>Unknown view type: ${typeof view}</p></div>`));
|
||||
onload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function createElement(str) {
|
||||
const $n = window.document.createElement("div");
|
||||
$n.innerHTML = str;
|
||||
return $n.firstElementChild;
|
||||
}
|
||||
131
public/lib/skeleton/index.test.js
Normal file
131
public/lib/skeleton/index.test.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import main, { createElement, onDestroy } from "./index.js";
|
||||
|
||||
describe("router with inline controller", () => {
|
||||
it("can render a string", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const routes = {
|
||||
"/": (render) => render(`<h1 id="test">main</h1>`),
|
||||
};
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
await nextTick();
|
||||
expect($app.querySelector("#test").textContent).toBe("main");
|
||||
});
|
||||
|
||||
it("can render a dom node", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const $node = createElement(`<h1 id="test">main</h1>`);
|
||||
const routes = {
|
||||
"/": (render) => render($node),
|
||||
};
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
// then
|
||||
await nextTick()
|
||||
expect($node instanceof window.Element).toBe(true)
|
||||
expect($app.querySelector("#test").textContent).toBe("main")
|
||||
});
|
||||
|
||||
it("errors when given a non valid route", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const $node = createElement(`<h1 id="test">main</h1>`);
|
||||
const routes = {
|
||||
"/": null,
|
||||
};
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
// then
|
||||
await nextTick()
|
||||
expect($node instanceof window.Element).toBe(true)
|
||||
expect($app.querySelector("h1").textContent).toBe("Error")
|
||||
});
|
||||
|
||||
it("errors when given a non valid render", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const $node = createElement(`<h1 id="test">main</h1>`);
|
||||
const routes = {
|
||||
"/": (render) => render({ json: "object", is: "not_ok" }),
|
||||
};
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
// then
|
||||
await nextTick()
|
||||
expect($node instanceof window.Element).toBe(true)
|
||||
expect($app.querySelector("h1").textContent).toBe("Error")
|
||||
});
|
||||
});
|
||||
|
||||
describe("router with es6 module as a controller", () => {
|
||||
it("render the default import", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const routes = {
|
||||
"/": "./common/skeleton/test/ctrl/ok.js",
|
||||
};
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
// then
|
||||
await nextTick();
|
||||
expect($app.querySelector("h1").textContent.trim()).toBe("hello world");
|
||||
});
|
||||
|
||||
it("error when missing the default render", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const routes = {
|
||||
"/": "./common/skeleton/test/ctrl/nok.js",
|
||||
};
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
// then
|
||||
await nextTick();
|
||||
expect($app.querySelector("h1").textContent.trim()).toBe("Error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("navigation", () => {
|
||||
it("using a link with data-link attribute for SPA", async () => {
|
||||
// given
|
||||
const $app = window.document.createElement("div");
|
||||
const routes = {
|
||||
"/": "./common/skeleton/test/ctrl/link.js",
|
||||
"/something": (render) => render(`<h1>OK</h1>`),
|
||||
};
|
||||
const destroy = jest.fn();
|
||||
|
||||
// when
|
||||
main($app, routes);
|
||||
window.dispatchEvent(new window.Event("pagechange"));
|
||||
await nextTick();
|
||||
expect(window.location.pathname).toBe("/");
|
||||
onDestroy(destroy);
|
||||
$app.querySelector("#spa-link").click();
|
||||
await nextTick();
|
||||
|
||||
// then
|
||||
expect(destroy).toHaveBeenCalled();
|
||||
expect(window.location.pathname).toBe("/something");
|
||||
});
|
||||
});
|
||||
13
public/lib/skeleton/lifecycle.js
Normal file
13
public/lib/skeleton/lifecycle.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
let _cleanup = [];
|
||||
|
||||
export async function init($root) {
|
||||
$root.cleanup = () => {
|
||||
const fns = _cleanup.map((fn) => fn($root))
|
||||
_cleanup = [];
|
||||
return Promise.all(fns);
|
||||
};
|
||||
}
|
||||
|
||||
export async function onDestroy(fn) {
|
||||
_cleanup.push(fn);
|
||||
}
|
||||
30
public/lib/skeleton/router.js
Normal file
30
public/lib/skeleton/router.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
const triggerPageChange = () => window.dispatchEvent(new window.Event("pagechange"));
|
||||
|
||||
export function init($root) {
|
||||
window.addEventListener("DOMContentLoaded", triggerPageChange);
|
||||
window.addEventListener("popstate", triggerPageChange);
|
||||
$root.addEventListener("click", (e) => {
|
||||
const href = _getHref(e.target, $root);
|
||||
return !href ? null : e.preventDefault() || navigate(href);
|
||||
});
|
||||
}
|
||||
|
||||
export function navigate(href) {
|
||||
window.history.pushState("", "", href);
|
||||
triggerPageChange();
|
||||
}
|
||||
|
||||
export function currentRoute(r, defaultRoute) {
|
||||
for (const prefix in r) {
|
||||
if (window.location .pathname.startsWith(prefix)) {
|
||||
return r[prefix];
|
||||
}
|
||||
}
|
||||
return r[defaultRoute] || null;
|
||||
}
|
||||
|
||||
function _getHref ($node, $root) {
|
||||
if ($node.matches("[data-link]")) return $node.getAttribute("href");
|
||||
if (!$node.parentElement || $node.isSameNode($root)) return null;
|
||||
return _getHref($node.parentElement, $root);
|
||||
}
|
||||
89
public/lib/skeleton/router.test.js
Normal file
89
public/lib/skeleton/router.test.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { createElement } from "./index.js";
|
||||
import { currentRoute, init } from "./router.js";
|
||||
import * as routerModule from "./router.js";
|
||||
|
||||
describe("router", () => {
|
||||
it("logic to get the current route", () => {
|
||||
// given
|
||||
let res;
|
||||
const routes = {
|
||||
"/foo": "route /foo",
|
||||
"/bar": "route /bar",
|
||||
}
|
||||
window.location.pathname = "/";
|
||||
|
||||
// when, then
|
||||
expect(window.location.pathname).toBe("/");
|
||||
expect(currentRoute({ "/": "route /", ...routes})).toBe("route /");
|
||||
expect(currentRoute(routes, "/foo")).toBe("route /foo");
|
||||
expect(currentRoute(routes)).toBe(null);
|
||||
});
|
||||
|
||||
it("trigger a page change when DOMContentLoaded", () => {
|
||||
// given
|
||||
const fn = jest.fn();
|
||||
init(createElement(`<div></div>`));
|
||||
window.addEventListener("pagechange", fn);
|
||||
|
||||
// when
|
||||
window.dispatchEvent(new window.Event("DOMContentLoaded"));
|
||||
|
||||
// then
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
it("trigger a page change when history back", () => {
|
||||
// given
|
||||
const fn = jest.fn();
|
||||
init(createElement(`<div></div>`));
|
||||
window.addEventListener("pagechange", fn);
|
||||
|
||||
// when
|
||||
window.dispatchEvent(new window.Event("popstate"));
|
||||
|
||||
// then
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
it("trigger a page change when clicking on a link with [data-link] attribute", () => {
|
||||
// given
|
||||
const fn = jest.fn();
|
||||
const $link = createElement(`<a href="/something" data-link></a>`)
|
||||
init($link);
|
||||
window.addEventListener("pagechange", fn);
|
||||
|
||||
// when
|
||||
$link.click();
|
||||
|
||||
// then
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
it("trigger a page change when clicking on a link with [data-link] attribute - recursive", () => {
|
||||
// given
|
||||
const fn = jest.fn();
|
||||
const $link = createElement(`<a href="/something" data-link><div id="click-here">test</div></a>`)
|
||||
init($link);
|
||||
window.addEventListener("pagechange", fn);
|
||||
|
||||
// when
|
||||
$link.querySelector("#click-here").click();
|
||||
|
||||
// then
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
|
||||
it("does nothing when clicking not on a link", () => {
|
||||
// given
|
||||
const fn = jest.fn();
|
||||
const $app = createElement(`<div>
|
||||
<div id="click-here">test</div>
|
||||
<a href="/something" data-link></a>
|
||||
</div>`);
|
||||
init($app);
|
||||
window.addEventListener("pagechange", fn);
|
||||
|
||||
// when
|
||||
$app.querySelector("#click-here").click();
|
||||
|
||||
// then
|
||||
expect(fn).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
1
public/model/global.js
Normal file
1
public/model/global.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const API_SERVER = "http://127.0.0.1:8000/proxy";
|
||||
1
public/model/index.js
Normal file
1
public/model/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { API_SERVER } from "./global.js";
|
||||
26
public/package.json
Normal file
26
public/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "Filestash",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend for Filestash",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest"
|
||||
},
|
||||
"author": "Mickael Kerjean",
|
||||
"license": "AGPL",
|
||||
"devDependencies": {
|
||||
"jest": "^29.5.0",
|
||||
"jsdom": "^22.1.0"
|
||||
},
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
"setupFiles": [
|
||||
"<rootDir>/common/skeleton/test/jest.setup.js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"test",
|
||||
"example"
|
||||
]
|
||||
}
|
||||
}
|
||||
21
public/pages/adminpage/ctrl_about.js
Normal file
21
public/pages/adminpage/ctrl_about.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { withEffect } from "../../lib/rxjs/index.js";
|
||||
|
||||
import Release from "./model_release.js";
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithAdminMenu from "./decorator_sidemenu.js";
|
||||
|
||||
export default AdminOnly(WithAdminMenu(function(render) {
|
||||
render(createElement(`
|
||||
<div class="component_page_about">
|
||||
<Loader />
|
||||
</div>
|
||||
`));
|
||||
withEffect(Release.get().pipe(rxjs.tap(({ html }) => {
|
||||
render(createElement(`
|
||||
<div class="component_page_about">
|
||||
${html}
|
||||
</div>
|
||||
`));
|
||||
})));
|
||||
}));
|
||||
18
public/pages/adminpage/ctrl_backend.js
Normal file
18
public/pages/adminpage/ctrl_backend.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import { withEffect } from "../../lib/rxjs/index.js";
|
||||
import { animate, CSSTransition } from "../../lib/animate/index.js";
|
||||
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithAdminMenu from "./decorator_sidemenu.js";
|
||||
|
||||
export default AdminOnly(WithAdminMenu(function(render) {
|
||||
const $page = createElement(`
|
||||
<div className="component_settingspage sticky">
|
||||
<form>
|
||||
FORM BUILDER BACKEND
|
||||
</form>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
withEffect(animate($page).pipe(CSSTransition()));
|
||||
}));
|
||||
33
public/pages/adminpage/ctrl_logger.js
Normal file
33
public/pages/adminpage/ctrl_logger.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import { withEffect } from "../../lib/rxjs/index.js";
|
||||
import { animate, CSSTransition } from "../../lib/animate/index.js";
|
||||
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithAdminMenu from "./decorator_sidemenu.js";
|
||||
|
||||
function Page(render) {
|
||||
const $page = createElement(`
|
||||
<div class="component_logpage sticky">
|
||||
<h2>Logging</h2>
|
||||
<div class="component_logger"></div>
|
||||
|
||||
<h2>Activity Report</h2>
|
||||
<div class="component_reporter"></div>
|
||||
<div>
|
||||
`);
|
||||
render($page);
|
||||
withEffect(animate($page).pipe(CSSTransition()));
|
||||
|
||||
renderComponentLog($page.querySelector(".component_logger"));
|
||||
renderComponentAuditor($page.querySelector(".component_reporter"));
|
||||
}
|
||||
|
||||
export default AdminOnly(WithAdminMenu(Page));
|
||||
|
||||
function renderComponentLog($component) {
|
||||
// console.log($component);
|
||||
}
|
||||
|
||||
function renderComponentAuditor($component) {
|
||||
// console.log($component);
|
||||
}
|
||||
18
public/pages/adminpage/ctrl_settings.js
Normal file
18
public/pages/adminpage/ctrl_settings.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createElement } from "../../lib/skeleton/index.js";
|
||||
import { withEffect } from "../../lib/rxjs/index.js";
|
||||
import { animate, CSSTransition } from "../../lib/animate/index.js";
|
||||
|
||||
import AdminOnly from "./decorator_admin_only.js";
|
||||
import WithAdminMenu from "./decorator_sidemenu.js";
|
||||
|
||||
export default AdminOnly(WithAdminMenu(function(render) {
|
||||
const $page = createElement(`
|
||||
<div className="component_settingspage sticky">
|
||||
<form>
|
||||
FORM BUILDER SETTINGS
|
||||
</form>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
withEffect(animate($page).pipe(CSSTransition()));
|
||||
}));
|
||||
37
public/pages/adminpage/decorator_admin_only.js
Normal file
37
public/pages/adminpage/decorator_admin_only.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { createElement, onDestroy } from "../../lib/skeleton/index.js";
|
||||
import { withEffect } from "../../lib/rxjs/index.js";
|
||||
import rxjs from "../../lib/rxjs/index.js";
|
||||
|
||||
import AdminSessionManager from "./model_admin_session.js";
|
||||
|
||||
export default function AdminOnly(ctrl) {
|
||||
return async (render) => {
|
||||
const loader$ = rxjs.timer(1000).subscribe(() => render(`<div>loading</div>`));
|
||||
onDestroy(() => loader$.unsubscribe());
|
||||
|
||||
const handlerUserIsAdminPassthrough = () => ctrl(render);
|
||||
const handlerUserIsNOTAdmin = () => {
|
||||
const $form = createElement(`
|
||||
<div>
|
||||
login page
|
||||
<form>
|
||||
<input type="text" data-bind="password"/><button>submit</button>
|
||||
</form>
|
||||
</div>
|
||||
`);
|
||||
render($form);
|
||||
withEffect(rxjs.fromEvent($form.querySelector("form"), "submit").pipe(
|
||||
rxjs.tap((e) => e.preventDefault()),
|
||||
rxjs.map(() => ({ password: $form.querySelector(`[data-bind="password"]`).value })),
|
||||
AdminSessionManager.startSession(),
|
||||
rxjs.tap((success) => console.log("FAIL LOGIN make things move", success)),
|
||||
));
|
||||
};
|
||||
|
||||
withEffect(AdminSessionManager.state().pipe(
|
||||
rxjs.tap(() => loader$.unsubscribe()),
|
||||
rxjs.map(({ isAdmin }) => isAdmin ? handlerUserIsAdminPassthrough : handlerUserIsNOTAdmin),
|
||||
rxjs.tap((fn) => fn()),
|
||||
));
|
||||
};
|
||||
}
|
||||
30
public/pages/adminpage/decorator_sidemenu.js
Normal file
30
public/pages/adminpage/decorator_sidemenu.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { createElement } from "../../../lib/skeleton/index.js";
|
||||
import rxjs, { withEffect, textContent } from "../../../lib/rxjs/index.js";
|
||||
|
||||
import Release from "./model_release.js";
|
||||
|
||||
export default function(ctrl) {
|
||||
return (render) => {
|
||||
const $page = createElement(`
|
||||
<div class="component_page_admin">
|
||||
<div class="component_menu_sidebar no-select">
|
||||
<h2>Admin console</h2>
|
||||
<ul>
|
||||
<li><a href="/admin/backend" data-link>Backend</a></li>
|
||||
<li><a href="/admin/settings" data-link>Settings</a></li>
|
||||
<li><a href="/admin/logs" data-link>Logs</a></li>
|
||||
<li><a class="version" href="/admin/about" data-link data-bind="version"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="page_container scroll-y" data-bind="admin"></div>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
ctrl(($node) => $page.querySelector(`[data-bind="admin"]`).appendChild($node));
|
||||
|
||||
withEffect(Release.get().pipe(
|
||||
rxjs.map(({ version }) => version),
|
||||
textContent($page, `[data-bind="version"]`),
|
||||
));
|
||||
};
|
||||
}
|
||||
5
public/pages/adminpage/home.js
Normal file
5
public/pages/adminpage/home.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { navigate } from "../../lib/skeleton/index.js";
|
||||
|
||||
export default function() {
|
||||
navigate("/admin/backend");
|
||||
}
|
||||
31
public/pages/adminpage/index.js
Normal file
31
public/pages/adminpage/index.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { createElement, navigate } from "../../lib/skeleton/index.js";
|
||||
import rxjs, { withEffect, textContent } from "../../lib/rxjs/index.js";
|
||||
import { animate, CSSTransition } from "../../lib/animate/index.js";
|
||||
|
||||
export default function(render) {
|
||||
const $page = createElement(`
|
||||
<div>
|
||||
<h1>Admin</h1>
|
||||
<p>
|
||||
Settings <br/>
|
||||
<form>
|
||||
<input id="ftp_username" name="username" type="text" />
|
||||
<input id="ftp_password" name="password" type="text" />
|
||||
</form>
|
||||
<a href="./" data-link>go home</a>
|
||||
</p>
|
||||
</div>`);
|
||||
render($page);
|
||||
withEffect(animate($page).pipe(CSSTransition()));
|
||||
|
||||
const formValues = rxjs.combineLatest(
|
||||
rxjs.
|
||||
fromEvent($page.querySelector("#ftp_username"), "input").
|
||||
pipe(rxjs.map((e) => ({name: e.target.id, value: e.target.value})), rxjs.startWith(null)),
|
||||
rxjs.
|
||||
fromEvent($page.querySelector("#ftp_password"), "input").
|
||||
pipe(rxjs.map((e) => ({name: e.target.id, value: e.target.value}), rxjs.startWith(null)))
|
||||
).subscribe((e) => { // pipe onto reducer function that build our form object
|
||||
console.log("OK", e)
|
||||
});
|
||||
};
|
||||
29
public/pages/adminpage/model_admin_session.js
Normal file
29
public/pages/adminpage/model_admin_session.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import rxjs, { ajax } from "../../lib/rxjs/index.js";
|
||||
import { API_SERVER } from "../../model/index.js";
|
||||
|
||||
window.rxjs = rxjs;
|
||||
|
||||
class AdminSessionManager {
|
||||
constructor() {
|
||||
this.session = new rxjs.ReplaySubject(1);
|
||||
ajax(API_SERVER + "/admin/api/session").subscribe(
|
||||
() => this.session.next({ isAdmin: false }),
|
||||
() => this.session.next({ isAdmin: false }),
|
||||
);
|
||||
}
|
||||
|
||||
state() {
|
||||
return this.session.asObservable().pipe(rxjs.delay(100));
|
||||
}
|
||||
|
||||
startSession() {
|
||||
return rxjs.pipe(
|
||||
rxjs.delay(1000),
|
||||
rxjs.tap(() => this.session.next({ isAdmin: true })),
|
||||
rxjs.mapTo(false),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new AdminSessionManager();
|
||||
24
public/pages/adminpage/model_release.js
Normal file
24
public/pages/adminpage/model_release.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import rxjs, { ajax } from "../../lib/rxjs/index.js";
|
||||
import { API_SERVER } from "../../model/index.js";
|
||||
|
||||
const release$ = ajax({
|
||||
url: API_SERVER + "/about",
|
||||
responseType: "text",
|
||||
}).pipe(rxjs.shareReplay(1));
|
||||
|
||||
class ReleaseImpl {
|
||||
get() {
|
||||
return release$.pipe(
|
||||
rxjs.map((xhr) => {
|
||||
const a = document.createElement("html")
|
||||
a.innerHTML = xhr.response;
|
||||
return {
|
||||
html: a.querySelector("table").outerHTML,
|
||||
version: xhr.responseHeaders["x-powered-by"].trim().replace(/^Filestash\/([v\.0-9]*).*$/, "$1"),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReleaseImpl();
|
||||
164
public/pages/boot/nyancat.js
Normal file
164
public/pages/boot/nyancat.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
class NyanCat extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.innerHTML = this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
return `<style>${CSS()}</style>
|
||||
<div id="n-lder" class="loading">
|
||||
<div id="cat">
|
||||
<div id="hide-behind" class="background-color"></div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="21" preserveAspectRatio="xMinYMin meet" viewBox="0 0 33 21">
|
||||
<g id="nyan_all">
|
||||
<g id="nyan_feet">
|
||||
<g>
|
||||
<path d="m 4,20 0,-3 1,0 0,-1 4,0 0,3 -1,0 0,1 z" style="fill:#000000;"></path>
|
||||
<path d="m 5,19 0,-2 3,0 0,1 -1,0 0,1 z" style="fill:#999999;"></path>
|
||||
<path d="m 10,20 0,-2 4,0 0,1 -1,0 0,1 z" style="fill:#000000;"></path>
|
||||
<path d="m 11,18 2,0 0,1 -2,0 z" style="fill:#999999;"></path>
|
||||
</g>
|
||||
<g transform="matrix(-1,0,0,1,32,0)">
|
||||
<path d="m 10,20 0,-2 4,0 0,1 -1,0 0,1 z" style="fill:#000000;"></path>
|
||||
<path d="m 11,18 2,0 0,1 -2,0 z" style="fill:#999999;"></path>
|
||||
<path d="m 4,20 0,-3 1,0 0,-1 4,0 0,3 -1,0 0,1 z" style="fill:#000000;"></path>
|
||||
<path d="m 5,19 0,-2 3,0 0,1 -1,0 0,1 z" style="fill:#999999;"></path>
|
||||
</g>
|
||||
</g>
|
||||
<g id="nyan_tail">
|
||||
<path d="M 0,10 0,7 4,7 4,8 5,8 5,9 6,9 6,14 5,14 5,13 3,13 3,12 2,12 2,11 1,11 1,10 z" style="fill:#000000;" />
|
||||
<path d="m 1,9 0,-1 2,0 0,1 1,0 0,1 1,0 0,1 1,0 0,1 -2,0 0,-1 -1,0 0,-1 -1,0 0,-1 z" style="fill:#999999;" />
|
||||
</g>
|
||||
<g id="nyan_body">
|
||||
<path d="m 7,1 19,0 0,16 -19,0 z" style="fill:#ffcc99;" />
|
||||
<path d="m 8,14 0,-10 1,0 0,-1 1,0 0,-1 13,0 0,1 1,0 0,1 1,0 0,10 -1,0 0,1 -1,0 0,1 -13,0 0,-1 -1,0 0,-1 z" style="fill:#ff99ff;" />
|
||||
<path d="m 22,5 1,0 0,1 -1,0 z m -4,-2 1,0 0,1 -1,0 z m -3,0 1,0 0,1 -1,0 z m -1,4 1,0 0,1 -1,0 z m 1,3 1,0 0,1 -1,0 z m -2,3 1,0 0,1 -1,0 z m -2,-4 1,0 0,1 -1,0 z m -2,2 1,0 0,1 -1,0 z m 1,3 1,0 0,1 -1,0 z m 0,-10 1,0 0,1 -1,0 z" style="fill:#ff3399;" />
|
||||
<path d="m 8,17 17,0 0,1 -17,0 z m 0,-17 17,0 0,1 -17,0 z m 18,16 0,-14 1,0 0,14 z m -20,0 0,-14 1,0 0,14 z m 1,0 1,0 0,1 -1,0 z m 0,-15 1,0 0,1 -1,0 z m 18,0 1,0 0,1 -1,0 z m 0,15 1,0 0,1 -1,0 z" style="fill:#000000;" />
|
||||
</g>
|
||||
<g id="nyan_head">
|
||||
<path d="m 17,15 0,-5 1,0 0,-4 2,0 0,1 1,0 0,1 1,0 0,1 4,0 0,-1 1,0 0,-1 1,0 0,-1 2,0 0,4 1,0 0,5 -1,0 0,1 -1,0 0,1 -10,0 0,-1 -1,0 0,-1 z" style="fill:#999999;fill-opacity:1;stroke:none" />
|
||||
<path d="m 29,16 1,0 0,1 -1,0 z m 1,-1 1,0 0,1 -1,0 z m 1,-5 1,0 0,5 -1,0 z m -1,-4 1,0 0,4 -1,0 z m -2,-1 2,0 0,1 -2,0 z m -6,3 4,0 0,1 -4,0 z m -4,-3 2,0 0,1 -2,0 z m -1,1 1,0 0,4 -1,0 z m -1,4 1,0 0,5 -1,0 z m 11,-4 1,0 0,1 -1,0 z m -1,1 1,0 0,1 -1,0 z m -5,0 1,0 0,1 -1,0 z m -1,-1 1,0 0,1 -1,0 z m -1,11 10,0 0,1 -10,0 z m -1,-1 1,0 0,1 -1,0 z m -1,-1 1,0 0,1 -1,0 z" style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<path d="m 18,13 2,0 0,2 -2,0 z" style="fill:#ff9999;fill-opacity:1;stroke:none" />
|
||||
<path d="m 29,13 2,0 0,2 -2,0 z" style="fill:#ff9999;fill-opacity:1;stroke:none" />
|
||||
<path d="m 21,16 0,-2 1,0 0,1 2,0 0,-1 1,0 0,1 2,0 0,-1 1,0 0,2 z" style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<path d="m 25,12 1,0 0,1 -1,0 z" style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<g>
|
||||
<path d="m 27,13 0,-1 1,0 0,-1 1,0 0,2 z" style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<path d="m 27,11 1,0 0,1 -1,0 z" style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<path d="m 20,13 0,-1 1,0 0,-1 1,0 0,2 z" style="fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<path d="m 20,11 1,0 0,1 -1,0 z" style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="rbw">
|
||||
<div class="w">
|
||||
<div class="rbw f1">
|
||||
<div class="wv wv-1"></div><div class="wv wv-2"></div><div class="wv wv-3"></div>
|
||||
<div class="wv wv-4"></div><div class="wv wv-5"></div><div class="wv wv-6"></div>
|
||||
</div>
|
||||
<div class="rbw f2">
|
||||
<div class="wv wv-1"></div><div class="wv wv-2"></div><div class="wv wv-3"></div>
|
||||
<div class="wv wv-4"></div><div class="wv wv-5"></div><div class="wv wv-6"></div>
|
||||
</div>
|
||||
<div class="rbw f3">
|
||||
<div class="wv wv-1"></div><div class="wv wv-2"></div><div class="wv wv-3"></div>
|
||||
<div class="wv wv-4"></div><div class="wv wv-5"></div><div class="wv wv-6"></div>
|
||||
</div>
|
||||
<div class="rbw f4">
|
||||
<div class="wv wv-1"></div><div class="wv wv-2"></div><div class="wv wv-3"></div>
|
||||
<div class="wv wv-4"></div><div class="wv wv-5"></div><div class="wv wv-6"></div>
|
||||
</div>
|
||||
<div class="rbw f5">
|
||||
<div class="wv wv-1"></div><div class="wv wv-2"></div><div class="wv wv-3"></div>
|
||||
<div class="wv wv-4"></div><div class="wv wv-5"></div><div class="wv wv-6"></div>
|
||||
</div>
|
||||
<div class="rbw f6">
|
||||
<div class="wv wv-1"></div><div class="wv wv-2"></div><div class="wv wv-3"></div>
|
||||
<div class="wv wv-4"></div><div class="wv wv-5"></div><div class="wv wv-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("data-nyancat", NyanCat);
|
||||
|
||||
function CSS() {
|
||||
return `
|
||||
#n-lder{ max-width: 100%; overflow: hidden; }
|
||||
#n-lder #cat{ position: absolute; top: calc(50% + 45px); left: 0%; margin-left: -250px; margin-top: -125px; width: 100%; height: 150px; }
|
||||
#n-lder.loading #cat{ left: 20%; left: calc(50% + 125px); transition: left 4s ease-out; }
|
||||
#n-lder.loading.done #cat { left: 100%; left: calc(100% + 250px); transition: left 0.5s linear; }
|
||||
#n-lder #cat svg{ height: 160px; width: 250px; position: absolute; }
|
||||
#n-lder #cat #hide-behind{ position: absolute; top: 0; left: 55px; bottom: 0; right: -250px; }
|
||||
|
||||
#n-lder #rbw{ position: absolute; top: calc(50% + 45px); left: 0; overflow: hidden; height: 145px; margin-top: -110px; width: 100%; }
|
||||
#n-lder #rbw .w{ width: 10000px; }
|
||||
#n-lder #rbw .rbw { z-index: -1; font-size: 16em; float: left; position: relative; }
|
||||
#n-lder #rbw .rbw .wv { height: 20px; width: 55px; }
|
||||
#n-lder #rbw .rbw .wv.wv-1 { background: #ff0000; }
|
||||
#n-lder #rbw .rbw .wv.wv-2 { background: #ff9900; }
|
||||
#n-lder #rbw .rbw .wv.wv-3 { background: #ffff00; }
|
||||
#n-lder #rbw .rbw .wv.wv-4 { background: #33ff00; }
|
||||
#n-lder #rbw .rbw .wv.wv-5 { background: #0099ff; }
|
||||
#n-lder #rbw .rbw .wv.wv-6 { background: #6633ff; }
|
||||
|
||||
#n-lder #rbw .rbw{ top: 0px; animation: rbw .6s linear infinite; }
|
||||
#n-lder #rbw .rbw.f1{ animation-delay: 0s; }
|
||||
#n-lder #rbw .rbw.f2{ animation-delay: 0.1s; }
|
||||
#n-lder #rbw .rbw.f3{ animation-delay: 0.2s; }
|
||||
#n-lder #rbw .rbw.f4{ animation-delay: 0.3s; }
|
||||
#n-lder #rbw .rbw.f5{ animation-delay: 0.4s; }
|
||||
#n-lder #rbw .rbw.f6{ animation-delay: 0.5s; }
|
||||
@keyframes rbw {
|
||||
0%{ top: 0px; }
|
||||
50%{ top: 15px; }
|
||||
100%{ top: 0px; }
|
||||
}
|
||||
@keyframes nyan_all {
|
||||
0%{ transform: translateY(0px); }
|
||||
33%{ transform: translateY(0px); }
|
||||
34%{ transform: translateY(1px); }
|
||||
100%{ transform: translateY(1px); }
|
||||
} #n-lder svg g#nyan_all{ animation: nyan_all 0.40s linear infinite; }
|
||||
@keyframes nyan_head {
|
||||
0%{ transform: translateX(0px) translateY(0px); }
|
||||
16%{ transform: translateX(0px) translateY(0px); }
|
||||
17%{ transform: translateX(1px) translateY(0px); }
|
||||
66%{ transform: translateX(1px) translateY(0px); }
|
||||
67%{ transform: translateX(0px) translateY(0px); }
|
||||
83%{ transform: translateX(0px) translateY(0px); }
|
||||
84%{ transform: translateX(0px) translateY(-1px); }
|
||||
100%{ transform: translateX(0px) translateY(-1px); }
|
||||
} #n-lder svg g#nyan_head{ animation: nyan_head 0.4s linear infinite; }
|
||||
@keyframes nyan_walk {
|
||||
0%{ transform: translateX(0px); }
|
||||
16%{ transform: translateX(0px); }
|
||||
17%{ transform: translateX(1px); }
|
||||
33%{ transform: translateX(1px); }
|
||||
34%{ transform: translateX(2px); }
|
||||
50%{ transform: translateX(2px); }
|
||||
51%{ transform: translateX(1px); }
|
||||
100%{ transform: translateX(0px); }
|
||||
} #n-lder svg g#nyan_feet{ animation: nyan_walk 0.5s linear infinite; }
|
||||
@keyframes nyan_tail {
|
||||
0%{ transform: rotate(0); }
|
||||
16%{ transform: rotate(0); }
|
||||
17%{ transform: rotate(-5deg); }
|
||||
33%{ transform: rotate(-5deg); }
|
||||
34%{ transform: rotate(-10deg); }
|
||||
49%{ transform: rotate(-10deg); }
|
||||
50%{ transform: rotate(-20deg); }
|
||||
66%{ transform: rotate(-20deg); }
|
||||
67%{ transform: rotate(-10deg); }
|
||||
83%{ transform: rotate(-10deg); }
|
||||
84%{ transform: rotate(-5deg); }
|
||||
99%{ transform: rotate(-5deg); }
|
||||
100%{ transform: rotate(0deg); }
|
||||
} #n-lder svg g#nyan_tail{ animation: nyan_tail 0.5s linear infinite; transform-origin: 4px 8px; }
|
||||
`;
|
||||
};
|
||||
3
public/pages/home/index.js
Normal file
3
public/pages/home/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function(render) {
|
||||
render(`<h1>HOMEPAGE</h1>`);
|
||||
};
|
||||
7
public/pages/home/index.test.js
Normal file
7
public/pages/home/index.test.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import home from "./index.js";
|
||||
|
||||
test("home", () => {
|
||||
const render = jest.fn();
|
||||
home(render);
|
||||
expect(render).toBeCalledTimes(1);
|
||||
});
|
||||
90
public/server.go
Normal file
90
public/server.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CHROOT = "/"
|
||||
ENDPOINT = "127.0.0.1:8000"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("starting server http://%s ...\n", ENDPOINT)
|
||||
err := http.ListenAndServe(ENDPOINT, http.HandlerFunc(serveStatic))
|
||||
if err != nil {
|
||||
fmt.Printf("Oops %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func serveStatic(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/proxy/") {
|
||||
u, _ := url.Parse("http://127.0.0.1:8334")
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/proxy")
|
||||
httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
var (
|
||||
fs http.Dir
|
||||
f http.File
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
if err != nil {
|
||||
fmt.Printf("%s HTTP %s %s err[%s]\n", now, r.Method, r.URL.Path, err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s HTTP %s %s\n", now, r.Method, r.URL.Path)
|
||||
}()
|
||||
fs = http.Dir(".")
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
http.FileServer(fs).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
f, err = openFile(fs, r, w)
|
||||
if err != nil {
|
||||
r.URL.Path = CHROOT + "index.html"
|
||||
f, err = openFile(fs, r, w)
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("Oops"))
|
||||
return
|
||||
}
|
||||
// TODO: etag
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(r.URL.Path)))
|
||||
io.Copy(w, f)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
func openFile(fs http.Dir, r *http.Request, w http.ResponseWriter) (http.File, error) {
|
||||
encs := [][]string{}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
|
||||
encs = append(encs, []string{".br", "br"})
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
encs = append(encs, []string{".gz", "gzip"})
|
||||
}
|
||||
encs = append(encs, []string{"", ""})
|
||||
|
||||
for _, enc := range encs {
|
||||
f, err := fs.Open(r.URL.Path + enc[0])
|
||||
if err == nil {
|
||||
if enc[1] != "" {
|
||||
w.Header().Set("Content-Encoding", enc[1])
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
Loading…
Reference in a new issue