VENOM-2 : WIP
This commit is contained in:
parent
c7984873c0
commit
a2931d93f7
9 changed files with 311 additions and 37 deletions
1
public/theme/admin/css/login.css
Normal file
1
public/theme/admin/css/login.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*{box-sizing:border-box}body{display:flex;align-items:center;justify-content:center;height:100vh;margin:0 auto;width:calc(100% - 20px);background-color:#303030;color:#fff;font-family:sans-serif;font-size:16px}main{display:flex;align-items:center;justify-content:center;position:relative;max-width:540px;width:90%;height:480px;background-color:#424242;color:#fff;border-radius:3px;border-bottom:#007769 solid 20px;box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}main:after{z-index:-1;content:'';position:absolute;top:0;left:0;width:calc(100% + 10px);height:calc(100% + 10px);background-color:#3949ab;transform:translate(-5px,-5px) rotate(-5deg);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}p{font-size:1.4rem;text-align:center}a{font-size:.7rem;color:#fff;text-decoration:none;border-bottom:1px solid transparent}a:hover{border-bottom-color:#c51162}form{margin:0 auto;width:80%;min-width:240px}.input-group input{background-color:rgba(0,0,0,.4);border:none;border-bottom:2px solid #3949ab;color:#fff;padding:6px}.input-group input:focus,.input-group.focus input{border-color:#ff0089}.input-group.valid input{border-color:#39ab48}.input-group.invalid input{border-color:#cf1b1b}.input-group{display:flex;flex-direction:column;margin-bottom:3px;position:relative;padding-top:.5rem}.input-group label{font-size:.6rem;position:absolute;top:.5rem;left:6px;height:1rem;vertical-align:middle;transform:translateY(50%);color:#dcdcdc;transition:all .2s ease-out}.input-group input:focus~label,.input-group.focus label{top:0;left:0;transform:translateY(0);font-size:.4rem}.input-group .error{display:none}.input-group.invalid .error{margin-top:2px;display:block;font-size:.5rem;color:#ff3232}.warning{background-color:#c51162}
|
1
public/theme/admin/css/style.css
Normal file
1
public/theme/admin/css/style.css
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.btn{border:none;background:#3949ab radial-gradient(circle at 0 0,#3949ab 0,rgba(0,0,0,.2) 100%) no-repeat;color:#fff;padding:5px 15px;margin:10px 0;cursor:pointer;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-radius:4px;box-shadow:0 2px 5px 0 rgba(0,0,0,.26);overflow:hidden;display:flex;justify-content:center;align-items:center;transition:.5s}.btn--outline{background:0 0;border:1px solid #3949ab}.btn:focus{box-shadow:0 3px 8px 1px rgba(0,0,0,.4)}.btn--accent{background:#ff0089 radial-gradient(circle at 0 0,#ff0089 0,rgba(0,0,0,.2) 100%) no-repeat}.btn--warn{background:#f87229 radial-gradient(circle at 0 0,#f87229 0,rgba(0,0,0,.2) 100%) no-repeat}.btn-fab{border-radius:2rem;width:2em;height:2em;padding:5px}.btn-ripple{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;background:0 0}.btn-ripple__effect{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);opacity:1;width:200%;height:0;padding-bottom:200%;border-radius:50%;background:rgba(190,190,190,.3);-webkit-animation:a-ripple .4s ease-in;animation:a-ripple .4s ease-in}.btn-ripple__effect.to-remove{-webkit-animation:remove-ripple .2s linear;animation:remove-ripple .2s linear}.btn--outline .btn-ripple__effect{background:rgba(57,73,171,.5);z-index:-1}.btn__content{z-index:1;font-weight:400;pointer-events:none}@-webkit-keyframes remove-ripple{from{opacity:1}to{opacity:0}}@keyframes remove-ripple{from{opacity:1}to{opacity:0}}@-webkit-keyframes a-ripple{0%{opacity:0;padding-bottom:0;width:0}25%{opacity:1}100%{width:200%;padding-bottom:200%}}@keyframes a-ripple{0%{opacity:0;padding-bottom:0;width:0}25%{opacity:1}100%{width:200%;padding-bottom:200%}}
|
|
@ -1,22 +0,0 @@
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #333;
|
|
||||||
font-family: sans-serif;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
font-size: 5vw;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
header:hover {
|
|
||||||
color: #ff0323;
|
|
||||||
}
|
|
250
public/theme/admin/js/scripts.js
Normal file
250
public/theme/admin/js/scripts.js
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
class VUtils {
|
||||||
|
static makePublic() {
|
||||||
|
if (VUtils.isUsed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.initHandlers();
|
||||||
|
VUtils.isUsed = true;
|
||||||
|
console.log("[VUtils] is now available in the Global Space! no VUtils. anymore needed");
|
||||||
|
}
|
||||||
|
|
||||||
|
static initHandlers() {
|
||||||
|
window.$ = this.$;
|
||||||
|
window.$$ = this.$$;
|
||||||
|
window.tryCatch = this.tryCatch;
|
||||||
|
VUtils.nodePrototypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
static $(selector, from) {
|
||||||
|
from = from || document;
|
||||||
|
return from.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
static $$(selector, from) {
|
||||||
|
from = from || document;
|
||||||
|
return from.querySelectorAll(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
static tryCatch(data, callback, error) {
|
||||||
|
data = VUtils.wrap(data, []);
|
||||||
|
error = error || console.error;
|
||||||
|
callback = callback || console.log;
|
||||||
|
try {
|
||||||
|
callback(...data);
|
||||||
|
} catch (e) {
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static forEach(items, cb, error) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
VUtils.tryCatch([items[i], i], cb, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get(valueOne, value) {
|
||||||
|
return this.wrap(valueOne, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static mergeKeys(root, target) {
|
||||||
|
root = root || {};
|
||||||
|
let keys = Object.keys(root);
|
||||||
|
for (let key of keys) {
|
||||||
|
target[key] = root[key];
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mergeOptions(target, root) {
|
||||||
|
root = root || {};
|
||||||
|
let keys = Object.keys(root);
|
||||||
|
for (let key of keys) {
|
||||||
|
target[key] = VUtils.get(root[key], target[key]);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wrap(valueOne, valueTwo) {
|
||||||
|
let x = typeof valueTwo;
|
||||||
|
if (!(valueOne instanceof Array) && valueTwo instanceof Array) {
|
||||||
|
return [valueOne];
|
||||||
|
}
|
||||||
|
if (x === 'string' && valueOne instanceof Array) {
|
||||||
|
return valueOne.join(".");
|
||||||
|
}
|
||||||
|
return valueOne === undefined ? valueTwo : valueOne;
|
||||||
|
}
|
||||||
|
|
||||||
|
static nodePrototypes() {
|
||||||
|
Node.prototype.find = function (selector) {
|
||||||
|
return this.closest(selector);
|
||||||
|
};
|
||||||
|
Node.prototype.createNew = function (tag, options) {
|
||||||
|
let el = document.createElement(tag);
|
||||||
|
el.classList.add(...VUtils.get(options.classes, []));
|
||||||
|
el.id = VUtils.get(options.id, '');
|
||||||
|
el.innerHTML = VUtils.get(options.content, "");
|
||||||
|
VUtils.mergeKeys(options.dataset, el.dataset);
|
||||||
|
if (VUtils.get(options.append, true) === true) {
|
||||||
|
this.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
Node.prototype.addDelegatedEventListener = function (type, aim, callback, err) {
|
||||||
|
if (!callback || !type || !aim)
|
||||||
|
return;
|
||||||
|
this.addMultiListener(type, (event) => {
|
||||||
|
let target = event.target;
|
||||||
|
if (event.detail instanceof HTMLElement) {
|
||||||
|
target = event.detail;
|
||||||
|
}
|
||||||
|
if (target instanceof HTMLElement) {
|
||||||
|
if (target.matches(aim)) {
|
||||||
|
VUtils.tryCatch([event, target], callback, err);
|
||||||
|
} else {
|
||||||
|
const parent = target.find(aim);
|
||||||
|
if (parent) {
|
||||||
|
VUtils.tryCatch([event, parent], callback, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Node.prototype.addMultiListener = function (listener, cb, options = {}) {
|
||||||
|
let splits = listener.split(" ");
|
||||||
|
for (let split of splits) {
|
||||||
|
this.addEventListener(split, cb, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VUtils.makePublic();
|
||||||
|
|
||||||
|
|
||||||
|
class VRipple {
|
||||||
|
constructor(options = {}) {
|
||||||
|
if (!VUtils.isUsed) {
|
||||||
|
throw "VRipply is only with Public VUtils usable!"
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
self.options = JSON.parse('{"classes":["btn-ripple__effect"],"target":"body","selector":".btn-ripple"}');
|
||||||
|
VUtils.mergeOptions(self.options, options);
|
||||||
|
if (self.options.selector.indexOf("#") > -1) {
|
||||||
|
throw "ID's are not allowed as selector!";
|
||||||
|
}
|
||||||
|
this.instanceCheck();
|
||||||
|
this.ripples = [];
|
||||||
|
requestAnimationFrame(this.initHandler.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceCheck() {
|
||||||
|
let opts = this.options;
|
||||||
|
const rawKey = [opts.target, opts.selector, opts.classes.join(".")].join(" ");
|
||||||
|
VRipple.instances = VRipple.instances || {};
|
||||||
|
VRipple.instances[rawKey] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
initHandler() {
|
||||||
|
let self = this;
|
||||||
|
let selector = self.options.selector;
|
||||||
|
let target = $(self.options.target);
|
||||||
|
target.addDelegatedEventListener('mousedown touchstart', selector, (e, el) => {
|
||||||
|
let pos = e.touches ? e.touches[0] : e;
|
||||||
|
let parent = el.parentNode;
|
||||||
|
let circle = el.createNew('span', self.options);
|
||||||
|
let bounds = parent.getBoundingClientRect();
|
||||||
|
let x = pos.clientX - bounds.left;
|
||||||
|
let y = pos.clientY - bounds.top;
|
||||||
|
circle.style.top = y + 'px';
|
||||||
|
circle.style.left = x + 'px';
|
||||||
|
circle._mouseDown = true;
|
||||||
|
circle._animationEnded = false;
|
||||||
|
self.ripples.push(circle);
|
||||||
|
});
|
||||||
|
document.body.addDelegatedEventListener('animationend', '.' + VUtils.get(self.options.classes, ''), self.rippleEnd.bind(self))
|
||||||
|
if (!document.body._vRippleInit) {
|
||||||
|
document.body.addMultiListener('mouseup touchend mouseleave rippleClose', e => {
|
||||||
|
let keys = Object.keys(VRipple.instances);
|
||||||
|
for (let key of keys) {
|
||||||
|
for (let ripple of VRipple.instances[key].ripples) {
|
||||||
|
self.rippleEnd.bind(VRipple.instances[key])(e, ripple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
document.body._vRippleInit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rippleEnd(ev, el) {
|
||||||
|
const parent = el.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
if (ev.type === 'animationend') {
|
||||||
|
el._animationEnded = true;
|
||||||
|
} else {
|
||||||
|
el._mouseDown = false;
|
||||||
|
}
|
||||||
|
if (!el._mouseDown && el._animationEnded) {
|
||||||
|
if (el.classList.contains('to-remove')) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
this.ripples.splice(this.ripples.indexOf(el), 1)
|
||||||
|
} else {
|
||||||
|
el.classList.add('to-remove');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rippler = new VRipple();
|
||||||
|
class FormHandler {
|
||||||
|
constructor(selector, parent, cb, err) {
|
||||||
|
this.cb = cb || console.log;
|
||||||
|
this.err = err || console.err;
|
||||||
|
$(parent).addDelegatedEventListener('submit', selector, this.handleEvent.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(e, el) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (el.checkValidity()) {
|
||||||
|
const url = el.action ?? '';
|
||||||
|
if (url === '') {
|
||||||
|
console.error("No URL Found on Form", el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(el.action, {
|
||||||
|
method: el.method.toUpperCase(),
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: new FormData(el),
|
||||||
|
redirect: 'manual'
|
||||||
|
}).then(res => {
|
||||||
|
if(!res.ok) {
|
||||||
|
throw new Error('Network response errored');
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}).then(this.cb).catch(this.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(function () {
|
||||||
|
const body = $('body');
|
||||||
|
body.addDelegatedEventListener('change input', 'input', (e, el) => {
|
||||||
|
let parent = el.parentNode;
|
||||||
|
if (el.value === "") {
|
||||||
|
parent.classList.remove('focus')
|
||||||
|
} else {
|
||||||
|
parent.classList.add('focus')
|
||||||
|
}
|
||||||
|
if (el.checkValidity()) {
|
||||||
|
parent.classList.add('valid');
|
||||||
|
parent.classList.remove('invalid');
|
||||||
|
} else {
|
||||||
|
parent.classList.remove('valid');
|
||||||
|
parent.classList.add('invalid');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if($('#login')) {
|
||||||
|
new FormHandler('form#login', 'body', e => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})()
|
|
@ -5,8 +5,11 @@ namespace Venom\Admin;
|
||||||
|
|
||||||
|
|
||||||
use Venom\Helper\URLHelper;
|
use Venom\Helper\URLHelper;
|
||||||
|
use Venom\Views\Asset;
|
||||||
use Venom\Views\RenderController;
|
use Venom\Views\RenderController;
|
||||||
use Venom\Views\VenomRenderer;
|
use Venom\Views\VenomRenderer;
|
||||||
|
use Venom\Models\User;
|
||||||
|
use \Venom\Security\Security;
|
||||||
|
|
||||||
class AdminController implements RenderController
|
class AdminController implements RenderController
|
||||||
|
|
||||||
|
@ -25,6 +28,15 @@ class AdminController implements RenderController
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
$this->tpl = 'async';
|
$this->tpl = 'async';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isLogin = Security::get()->hasRole(User::ADMIN_ROLE);
|
||||||
|
$renderer->addVar('isLoggedIn', $isLogin);
|
||||||
|
if (!$isLogin) {
|
||||||
|
Asset::get()->addCSS('login','login.css');
|
||||||
|
}
|
||||||
|
Asset::get()->addCSS('styles','style.css', 1);
|
||||||
|
Asset::get()->addJS('scripts', 'scripts.min.js', 1);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ class Asset
|
||||||
usort($this->jsFiles, function ($a, $b) {
|
usort($this->jsFiles, function ($a, $b) {
|
||||||
return $a['pos'] <=> $b['pos'];
|
return $a['pos'] <=> $b['pos'];
|
||||||
});
|
});
|
||||||
$theme = $this->getPath('/theme/' . Config::getInstance()->getRenderer()->assetDir . '/js/');
|
$theme = $this->getPath('/js/');
|
||||||
foreach ($this->jsFiles as $key => $file) {
|
foreach ($this->jsFiles as $key => $file) {
|
||||||
echo '<script src="' . $theme . $file['file'] . '" id="js-' . $key . '"></script>';
|
echo '<script src="' . $theme . $file['file'] . '" id="js-' . $key . '"></script>';
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,8 @@ class Asset
|
||||||
|
|
||||||
private function getPath($base): string
|
private function getPath($base): string
|
||||||
{
|
{
|
||||||
$preDir = $base;
|
$dir = Config::getInstance()->isAdmin() ? 'admin' : Config::getInstance()->getRenderer()->assetDir;
|
||||||
|
$preDir = '/theme/' . $dir . $base;
|
||||||
$config = Config::getInstance();
|
$config = Config::getInstance();
|
||||||
$baseUrl = Config::getInstance()->getBaseUrl();
|
$baseUrl = Config::getInstance()->getBaseUrl();
|
||||||
if ($baseUrl !== '' && $config->getRenderer()->useStaticUrl) {
|
if ($baseUrl !== '' && $config->getRenderer()->useStaticUrl) {
|
||||||
|
@ -84,7 +85,7 @@ class Asset
|
||||||
usort($this->cssFiles, function ($a, $b) {
|
usort($this->cssFiles, function ($a, $b) {
|
||||||
return $a['pos'] <=> $b['pos'];
|
return $a['pos'] <=> $b['pos'];
|
||||||
});
|
});
|
||||||
$theme = $this->getPath('/theme/' . Config::getInstance()->getRenderer()->assetDir . '/css/');
|
$theme = $this->getPath('/css/');
|
||||||
foreach ($this->cssFiles as $key => $file) {
|
foreach ($this->cssFiles as $key => $file) {
|
||||||
echo '<link rel="stylesheet" href="' . $theme . $file['file'] . '" id="css-' . $key . '">';
|
echo '<link rel="stylesheet" href="' . $theme . $file['file'] . '" id="css-' . $key . '">';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Venom\Models\User;
|
use Venom\Views\Asset;
|
||||||
use \Venom\Security\Security;
|
|
||||||
|
|
||||||
if (!Security::get()->hasRole(User::ADMIN_ROLE)) {
|
|
||||||
?>
|
?>
|
||||||
<form method="post" action="/admin/api/login">
|
<!doctype html>
|
||||||
<input type="text" name="USERNAME" placeholder="Username">
|
<html lang="en">
|
||||||
<input type="password" name="PASSWORD" placeholder="Password">
|
<head>
|
||||||
<input type="hidden" name="REDIRECT_TO" value="/admin/">
|
<meta charset="UTF-8">
|
||||||
<input type="submit" value="Login">
|
<meta name="viewport"
|
||||||
</form>
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Venom Admin Interface</title>
|
||||||
|
<?php Asset::get()->renderCSS(); ?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
<?php
|
<?php
|
||||||
echo 'Login!';
|
if (!$this->getVar('isLoggedIn')) {
|
||||||
|
$this->renderTemplate('login');
|
||||||
} else {
|
} else {
|
||||||
echo 'Admin Interface!';
|
echo 'Admin Interface!';
|
||||||
echo '<a href="/admin/api/login/logout">Ausloggen</a>';
|
echo '<a href="/admin/api/login/logout">Ausloggen</a>';
|
||||||
}
|
}
|
||||||
|
Asset::get()->renderJS();
|
||||||
|
?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
23
tpl/admin/login.php
Normal file
23
tpl/admin/login.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<main>
|
||||||
|
<div>
|
||||||
|
<p>- Admin Login -<br>Be Carefully!</p>
|
||||||
|
|
||||||
|
<form id="login" novalidate method="POST" action="/admin/api/login">
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="username" required>
|
||||||
|
<label for="name">Username</label>
|
||||||
|
<span class="error">Username is required</span>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="password" required type="password">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<span class="error">Password is required</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--primary">
|
||||||
|
<span class="btn-ripple"></span>
|
||||||
|
<span class="btn__content">Login</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a href="">Forgotten Password? [not active!]</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
Loading…
Reference in a new issue