diff --git a/public/theme/admin/css/login.css b/public/theme/admin/css/login.css new file mode 100644 index 0000000..92d45bb --- /dev/null +++ b/public/theme/admin/css/login.css @@ -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} \ No newline at end of file diff --git a/public/theme/admin/css/style.css b/public/theme/admin/css/style.css new file mode 100644 index 0000000..5dd4324 --- /dev/null +++ b/public/theme/admin/css/style.css @@ -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%}} \ No newline at end of file diff --git a/public/theme/admin/css/test.css b/public/theme/admin/css/test.css deleted file mode 100644 index cfed392..0000000 --- a/public/theme/admin/css/test.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/public/theme/admin/js/scripts.js b/public/theme/admin/js/scripts.js new file mode 100644 index 0000000..79e47cf --- /dev/null +++ b/public/theme/admin/js/scripts.js @@ -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); + }) + } +})() \ No newline at end of file diff --git a/public/theme/admin/js/test.js b/public/theme/admin/js/test.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/Venom/Admin/AdminController.php b/src/Venom/Admin/AdminController.php index 60ae195..80f37a4 100644 --- a/src/Venom/Admin/AdminController.php +++ b/src/Venom/Admin/AdminController.php @@ -5,8 +5,11 @@ namespace Venom\Admin; use Venom\Helper\URLHelper; +use Venom\Views\Asset; use Venom\Views\RenderController; use Venom\Views\VenomRenderer; +use Venom\Models\User; +use \Venom\Security\Security; class AdminController implements RenderController @@ -25,6 +28,15 @@ class AdminController implements RenderController http_response_code(404); $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; } diff --git a/src/Venom/Views/Asset.php b/src/Venom/Views/Asset.php index c4c4e03..7233737 100755 --- a/src/Venom/Views/Asset.php +++ b/src/Venom/Views/Asset.php @@ -62,7 +62,7 @@ class Asset usort($this->jsFiles, function ($a, $b) { return $a['pos'] <=> $b['pos']; }); - $theme = $this->getPath('/theme/' . Config::getInstance()->getRenderer()->assetDir . '/js/'); + $theme = $this->getPath('/js/'); foreach ($this->jsFiles as $key => $file) { echo ''; } @@ -70,7 +70,8 @@ class Asset private function getPath($base): string { - $preDir = $base; + $dir = Config::getInstance()->isAdmin() ? 'admin' : Config::getInstance()->getRenderer()->assetDir; + $preDir = '/theme/' . $dir . $base; $config = Config::getInstance(); $baseUrl = Config::getInstance()->getBaseUrl(); if ($baseUrl !== '' && $config->getRenderer()->useStaticUrl) { @@ -84,7 +85,7 @@ class Asset usort($this->cssFiles, function ($a, $b) { return $a['pos'] <=> $b['pos']; }); - $theme = $this->getPath('/theme/' . Config::getInstance()->getRenderer()->assetDir . '/css/'); + $theme = $this->getPath('/css/'); foreach ($this->cssFiles as $key => $file) { echo ''; } diff --git a/tpl/admin/base.php b/tpl/admin/base.php index d6eedb6..0d7bda0 100644 --- a/tpl/admin/base.php +++ b/tpl/admin/base.php @@ -1,19 +1,27 @@ hasRole(User::ADMIN_ROLE)) { - ?> -
- - - - -
- + + + + + + + Venom Admin Interface + renderCSS(); ?> + + +getVar('isLoggedIn')) { + $this->renderTemplate('login'); } else { echo 'Admin Interface!'; echo 'Ausloggen'; } +Asset::get()->renderJS(); +?> + + diff --git a/tpl/admin/login.php b/tpl/admin/login.php new file mode 100644 index 0000000..50f49a0 --- /dev/null +++ b/tpl/admin/login.php @@ -0,0 +1,23 @@ +
+
+

- Admin Login -
Be Carefully!

+ +
+
+ + + Username is required +
+
+ + + Password is required +
+ +
+ Forgotten Password? [not active!] +
+