diff options
| -rw-r--r-- | src/browser/src/components/Alert/Alert.js | 20 | ||||
| -rw-r--r-- | src/browser/src/components/Alert/Alert.scss | 54 | ||||
| -rw-r--r-- | src/browser/src/components/Alert/Alert.vue | 65 | ||||
| -rw-r--r-- | src/browser/src/components/FillLoader/FillLoader.scss | 347 | ||||
| -rw-r--r-- | src/browser/src/components/FillLoader/FillLoader.vue | 15 | ||||
| -rw-r--r-- | src/browser/src/components/Sidebar/Sidebar.vue | 76 | ||||
| -rw-r--r-- | src/browser/src/styles/_btn-states.scss | 51 | ||||
| -rw-r--r-- | src/browser/src/styles/_forms.scss | 4 | ||||
| -rw-r--r-- | src/browser/src/views/Login.vue | 48 |
9 files changed, 630 insertions, 50 deletions
diff --git a/src/browser/src/components/Alert/Alert.js b/src/browser/src/components/Alert/Alert.js new file mode 100644 index 0000000..b7647dd --- /dev/null +++ b/src/browser/src/components/Alert/Alert.js @@ -0,0 +1,20 @@ +// File#: _1_alert +// Usage: codyhouse.co/license +function initAlert() { + var alertClose = document.getElementsByClassName("js-alert__close-btn"); + if (alertClose.length > 0) { + for (var i = 0; i < alertClose.length; i++) { + (function(i) { + initAlertEvent(alertClose[i]); + })(i); + } + } + + function initAlertEvent(element) { + element.addEventListener("click", function(event) { + event.preventDefault(); + Util.removeClass(element.closest(".js-alert"), "alert--is-visible"); + }); + } +} +export default initAlert; diff --git a/src/browser/src/components/Alert/Alert.scss b/src/browser/src/components/Alert/Alert.scss new file mode 100644 index 0000000..1cdf3d2 --- /dev/null +++ b/src/browser/src/components/Alert/Alert.scss @@ -0,0 +1,54 @@ +/* -------------------------------- + +File#: _1_alert +Title: Alert +Descr: Feedback message +Usage: codyhouse.co/license + +-------------------------------- */ + +.alert { + padding: var(--space-xs) var(--space-sm); + background-color: alpha(var(--color-primary), 0.2); + border-radius: var(--radius-md); + color: var(--color-contrast-higher); + // hide element + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); +} + +.alert__link { + color: inherit; + text-decoration: underline; +} + +.alert__close-btn { + display: inline-block; // flex fallback + flex-shrink: 0; + margin-left: var(--space-sm); + + .icon { + display: block; + } +} + +// themes +.alert--success { + background-color: alpha(var(--color-success), 0.2); +} + +.alert--error { + background-color: alpha(var(--color-error), 0.2); +} + +.alert--warning { + background-color: alpha(var(--color-warning), 0.2); +} + +// toggle visibility +.alert--is-visible { + position: static; + clip: auto; + clip-path: none; +} diff --git a/src/browser/src/components/Alert/Alert.vue b/src/browser/src/components/Alert/Alert.vue new file mode 100644 index 0000000..ed00284 --- /dev/null +++ b/src/browser/src/components/Alert/Alert.vue @@ -0,0 +1,65 @@ +<template> + <div + class="alert js-alert" + v-bind:class="{ + 'alert--success': isSuccess, + 'alert--warning': isWarning, + 'alert--error': isError, + 'alert--is-visible': isVisible, + }" + role="alert" + > + <div class="flex items-center justify-between"> + <div class="flex items-center"> + <p> + <strong>{{ model.title }}:</strong> {{ model.message }} + </p> + </div> + + <button class="reset alert__close-btn js-alert__close-btn"> + <svg class="icon" viewBox="0 0 24 24"> + <title>Close alert</title> + <g + stroke-linecap="square" + stroke-linejoin="miter" + stroke-width="3" + stroke="currentColor" + fill="none" + stroke-miterlimit="10" + > + <line x1="19" y1="5" x2="5" y2="19"></line> + <line fill="none" x1="19" y1="19" x2="5" y2="5"></line> + </g> + </svg> + </button> + </div> + </div> +</template> + +<script> +import { reactive, toRefs, readonly } from "vue"; +import initAlert from "./Alert"; +export default { + setup(props) { + const model = reactive({ + title: props.title, + message: props.message, + type: props.type ?? "info", + isSuccess: model.type === "success", + isWarning: model.type === "warning", + isError: model.type === "error", + isInfo: model.type === "info", + isVisible: props.isVisible ?? false, + }); + + return { ...toRefs(model) }; + }, + mounted() { + initAlert(); + }, +}; +</script> + +<style lang="scss" scoped> +@import "Alert.scss"; +</style> diff --git a/src/browser/src/components/FillLoader/FillLoader.scss b/src/browser/src/components/FillLoader/FillLoader.scss new file mode 100644 index 0000000..5c6b0ca --- /dev/null +++ b/src/browser/src/components/FillLoader/FillLoader.scss @@ -0,0 +1,347 @@ +/* -------------------------------- + +File#: _1_fill-loader +Title: Loader +Descr: A collection of animated loaders with a filling effect +Usage: codyhouse.co/license + +-------------------------------- */ + +.fill-loader { + position: relative; + overflow: hidden; + display: inline-block; +} + +.fill-loader__fill { + position: absolute; +} + +@supports (animation-name: this) { + .fill-loader__label { + @include srHide; // show label only to screen readers if animations are supported + } +} + +// loader v1 + v2 +@supports (animation-name: this) { + .fill-loader--v1, + .fill-loader--v2 { + .fill-loader__base { + width: 64px; // loader width + height: 4px; // loader height + background-color: var(--color-contrast-low); + } + + .fill-loader__fill { + background-color: var(--color-primary); + top: 0; + left: 0; + height: 100%; + width: 100%; + will-change: transform; + } + } + + .fill-loader--v1 { + .fill-loader__fill { + animation: fill-loader-1 0.8s infinite var(--ease-in-out); + } + } + + .fill-loader--v2 { + .fill-loader__fill { + animation: fill-loader-2 0.8s infinite alternate var(--ease-in-out); + } + } +} + +@keyframes fill-loader-1 { + 0% { + transform-origin: 0 0; + transform: scaleX(0); + } + + 49% { + transform-origin: 0 0; + transform: scaleX(1); + } + + 51% { + transform: scaleX(1); + transform-origin: 100% 0; + } + + 100% { + transform: scaleX(0); + transform-origin: 100% 0; + } +} + +@keyframes fill-loader-2 { + 0% { + transform-origin: 0 0; + transform: scaleX(0.1); + } + + 49% { + transform-origin: 0 0; + transform: scaleX(1); + } + + 51% { + transform: scaleX(1); + transform-origin: 100% 0; + } + + 100% { + transform: scaleX(0.1); + transform-origin: 100% 0; + } +} + +// loader v3 +@supports (animation-name: this) { + .fill-loader--v3 { + .fill-loader__base { + width: 120px; // loader width + height: 10px; // loader height + background-color: var(--color-contrast-low); + } + + .fill-loader__fill { + top: 0; + left: 0; + height: 100%; + width: 100%; + transform: scaleX(0); + will-change: transform; + animation: fill-loader-1 1s infinite var(--ease-in-out); + } + + .fill-loader__fill--1st { + background-color: var(--color-contrast-medium); + } + + .fill-loader__fill--2nd { + background-color: var(--color-contrast-higher); + animation-delay: 0.1s; + } + + .fill-loader__fill--3rd { + background-color: var(--color-primary); + animation-delay: 0.2s; + } + } +} + +// loader v4 +@supports (animation-name: this) { + .fill-loader--v4 { + width: 90%; // loader width + max-width: 300px; + + .fill-loader__base { + height: 4px; // loader height + background-color: var(--color-contrast-low); + } + + .fill-loader__fill { + top: 0; + left: 0; + right: 0; + height: 100%; + background-color: var(--color-primary); + animation: fill-loader-4 1.6s infinite var(--ease-in-out); + will-change: left, right; + } + } +} + +@keyframes fill-loader-4 { + 0% { + left: 0; + right: 100%; + background-color: var(--color-primary); + } + + 10%, + 60% { + left: 0; + } + + 40%, + 90% { + right: 0; + } + + 50% { + left: 100%; + background-color: var(--color-primary); + } + + 51% { + left: 0; + right: 100%; + background-color: var(--color-accent); + } + + 100% { + left: 100%; + background-color: var(--color-accent); + } +} + +// loader v5 +@supports (animation-name: this) { + .fill-loader--v5 { + .fill-loader__base { + width: 48px; // loader width + height: 48px; // loader height + background-color: var(--color-contrast-low); + } + + .fill-loader__fill { + top: 0; + left: 0; + height: 100%; + width: 100%; + will-change: transform; + } + + .fill-loader__fill--1st { + background-color: var(--color-primary); + transform-origin: 0 50%; + animation: fill-loader-5-1st 2s infinite var(--ease-in-out); + } + + .fill-loader__fill--2nd { + background-color: var(--color-contrast-higher); + transform-origin: 50% 100%; + animation: fill-loader-5-2nd 2s infinite var(--ease-in-out); + } + + .fill-loader__fill--3rd { + background-color: var(--color-accent); + transform-origin: 100% 50%; + animation: fill-loader-5-3rd 2s infinite var(--ease-in-out); + } + + .fill-loader__fill--4th { + background-color: var(--color-contrast-low); + transform-origin: 50% 0%; + animation: fill-loader-5-4th 2s infinite var(--ease-in-out); + } + } +} + +@keyframes fill-loader-5-1st { + 0% { + transform: scaleX(0); + } + + 25%, + 100% { + transform: scaleX(1); + } +} + +@keyframes fill-loader-5-2nd { + 0%, + 25% { + transform: scaleY(0); + } + + 50%, + 100% { + transform: scaleY(1); + } +} + +@keyframes fill-loader-5-3rd { + 0%, + 50% { + transform: scaleX(0); + } + + 75%, + 100% { + transform: scaleX(1); + } +} + +@keyframes fill-loader-5-4th { + 0%, + 75% { + transform: scaleY(0); + } + + 100% { + transform: scaleY(1); + } +} + +// loader v6 +@supports (animation-name: this) { + .fill-loader--v6 { + .fill-loader__grid { + display: flex; + } + + .fill-loader__bar { + position: relative; + } + + .fill-loader__bar:nth-child(2) { + margin: 0 8px; + } + + .fill-loader__base { + width: 6px; + height: 30px; + background-color: var(--color-contrast-low); + } + + .fill-loader__fill { + top: 0; + left: 0; + height: 100%; + width: 100%; + will-change: transform; + transform: scaleY(0); + transform-origin: 50% 100%; + background-color: var(--color-primary); + animation: fill-loader-6 0.8s infinite; + } + + .fill-loader__fill--2nd { + animation-delay: 0.1s; + } + + .fill-loader__fill--3rd { + animation-delay: 0.2s; + } + } +} + +@keyframes fill-loader-6 { + 0% { + transform-origin: 0 100%; + transform: scaleY(0); + } + + 49% { + transform-origin: 0 100%; + transform: scaleY(1); + } + + 51% { + transform: scaleY(1); + transform-origin: 0 0; + } + + 100% { + transform: scaleY(0); + transform-origin: 0 0; + } +} diff --git a/src/browser/src/components/FillLoader/FillLoader.vue b/src/browser/src/components/FillLoader/FillLoader.vue new file mode 100644 index 0000000..73a986f --- /dev/null +++ b/src/browser/src/components/FillLoader/FillLoader.vue @@ -0,0 +1,15 @@ +<template> + <div class="fill-loader fill-loader--v3" role="alert"> + <p class="fill-loader__label">Content is loading...</p> + <div aria-hidden="true"> + <div class="fill-loader__base"></div> + <div class="fill-loader__fill fill-loader__fill--1st"></div> + <div class="fill-loader__fill fill-loader__fill--2nd"></div> + <div class="fill-loader__fill fill-loader__fill--3rd"></div> + </div> + </div> +</template> + +<style lang="scss" scoped> +@import "FillLoader.scss"; +</style> diff --git a/src/browser/src/components/Sidebar/Sidebar.vue b/src/browser/src/components/Sidebar/Sidebar.vue index e4e97b5..aa6efc3 100644 --- a/src/browser/src/components/Sidebar/Sidebar.vue +++ b/src/browser/src/components/Sidebar/Sidebar.vue @@ -1,62 +1,62 @@ <template> - <div - class="bg border-right" - :class="{ 'is-hidden': !isLoggedIn }" - style="width: 250px; height: 100vh;" - > - <nav class="sidenav sidenav--basic padding-y-sm text-sm@md"> - <ul class="sidenav__list margin-bottom-sm"> - <li class="sidenav__item"> - <router-link to="/" class="sidenav__link"> - <span class="sidenav__text">Hjem</span> - </router-link> - </li> + <div + class="bg border-right" + :class="{ 'is-hidden': !isLoggedIn }" + style="width: 250px; height: 100vh;" + > + <nav class="sidenav sidenav--basic padding-y-sm text-sm@md"> + <ul class="sidenav__list margin-bottom-sm"> + <li class="sidenav__item"> + <router-link to="/" class="sidenav__link"> + <span class="sidenav__text">Hjem</span> + </router-link> + </li> - <li class="sidenav__item"> - <router-link to="/transactions" class="sidenav__link"> - <span class="sidenav__text">Bevegelser</span> - </router-link> - </li> - </ul> + <li class="sidenav__item"> + <router-link to="/transactions" class="sidenav__link"> + <span class="sidenav__text">Bevegelser</span> + </router-link> + </li> + </ul> - <ul class="sidenav__list"> - <li class="sidenav__item"> - <router-link to="/settings" class="sidenav__link"> - <span class="sidenav__text">Innstillinger</span> - </router-link> - </li> + <ul class="sidenav__list"> + <li class="sidenav__item"> + <router-link to="/settings" class="sidenav__link"> + <span class="sidenav__text">Innstillinger</span> + </router-link> + </li> - <li class="sidenav__item"> - <router-link to="/account" class="sidenav__link"> - <span class="sidenav__text">Konto</span> - </router-link> - </li> - </ul> - </nav> - </div> + <li class="sidenav__item"> + <router-link to="/account" class="sidenav__link"> + <span class="sidenav__text">Konto</span> + </router-link> + </li> + </ul> + </nav> + </div> </template> <script> import "./Sidebar.js"; import { mapState } from "vuex"; export default { - name: "Sidebar", - computed: mapState(["isLoggedIn"]), + name: "Sidebar", + computed: mapState(["isLoggedIn"]), }; </script> -<style lang="scss"> +<style lang="scss" scoped> @import "Sidebar.scss"; .blurred { - filter: blur(30); + filter: blur(30); } nav { - user-select: none; + user-select: none; } .sidenav__text { - text-transform: uppercase; + text-transform: uppercase; } </style> diff --git a/src/browser/src/styles/_btn-states.scss b/src/browser/src/styles/_btn-states.scss new file mode 100644 index 0000000..bc1498f --- /dev/null +++ b/src/browser/src/styles/_btn-states.scss @@ -0,0 +1,51 @@ +/* -------------------------------- + +File#: _1_btn-states +Title: Buttons states +Descr: Multi-state button elements +Usage: codyhouse.co/license + +-------------------------------- */ + +.btn .btn__content-a { + display: inline-flex; +} + + +.btn .btn__content-b { + display: none; +} + +.btn__content-a, .btn__content-b { + align-items: center; +} + +.btn--state-b { + .btn__content-a { + display: none; + } + + .btn__content-b { + display: inline-block; // fallback + display: inline-flex; + } +} + +/* preserve button width when switching from state A to state B */ +.btn--preserve-width { + .btn__content-b { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + justify-content: center; + } + + &.btn--state-b .btn__content-a { + display: inline-block; // fallback + display: inline-flex; + visibility: hidden; + } +} + diff --git a/src/browser/src/styles/_forms.scss b/src/browser/src/styles/_forms.scss index f4fcd8c..0c7d745 100644 --- a/src/browser/src/styles/_forms.scss +++ b/src/browser/src/styles/_forms.scss @@ -32,6 +32,10 @@ } } +.form-control[disabled] { + filter: opacity(0.8); +} + .form-control[disabled], .form-control[readonly] { cursor: not-allowed; diff --git a/src/browser/src/views/Login.vue b/src/browser/src/views/Login.vue index 9beca02..f1105dc 100644 --- a/src/browser/src/views/Login.vue +++ b/src/browser/src/views/Login.vue @@ -1,13 +1,9 @@ <template> <div class="container max-width-xs padding-y-lg"> - <div v-if="isError"> - <h1 v-bind="error.title"></h1> - <p v-bind="error.message"></p> - </div> - <div v-if="isLoading" id="loading-message"> - <p>Logger inn...</p> - </div> <form class="login-form" @submit.prevent="submitForm()"> + <div v-if="isError"> + <Alert message="" title="" type="error" /> + </div> <div class="text-component text-center margin-bottom-sm"> <h1>dough</h1> </div> @@ -17,8 +13,9 @@ class="form-control width-100%" type="text" name="username" - v-model.trim="input.username" id="username" + :disabled="isLoading == true" + v-model.trim="input.username" /> </div> @@ -33,12 +30,31 @@ class="form-control width-100%" name="password" id="password" + :disabled="isLoading == true" v-model.trim="input.password" /> </div> <div class="margin-bottom-sm"> - <button class="btn btn--primary btn--md">Login</button> + <button class="btn btn--primary btn--md btn--preserve-width"> + <span v-if="isLoading" class="btn__content-b"> + <svg class="icon icon--is-spinning" aria-hidden="true" viewBox="0 0 16 16"> + <title>Loading</title> + <g stroke-width="1" fill="currentColor" stroke="currentColor"> + <path + d="M.5,8a7.5,7.5,0,1,1,1.91,5" + fill="none" + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + ></path> + </g> + </svg> + </span> + <span v-else class="btn__content-a"> + Login + </span> + </button> </div> <div class="text-center is-hidden"> @@ -49,12 +65,18 @@ </template> <script> +import Alert from "../components/Alert/Alert.vue"; +import FillLoader from "../components/FillLoader/FillLoader.vue"; import constants from "../constants"; import { reactive, toRefs } from "vue"; import store from "../store"; import router from "../router"; export default { + components: { + Alert, + FillLoader, + }, setup() { const model = reactive({ isError: false, @@ -73,7 +95,9 @@ export default { document.getElementById("username").required = true; document.getElementById("password").required = true; if (model.input.username && model.input.password) { - loginAsync(); + // displayError() + model.isLoading = true; + // loginAsync() } } @@ -86,8 +110,6 @@ export default { async function loginAsync() { try { - model.isLoading = true; - let response = await fetch(constants.API_ADDRESS + "/account/login", { method: "POST", credentials: "include", @@ -139,3 +161,5 @@ export default { }, }; </script> + +<style lang="scss"></style> |
