aboutsummaryrefslogtreecommitdiffstats
path: root/src/browser
diff options
context:
space:
mode:
Diffstat (limited to 'src/browser')
-rw-r--r--src/browser/src/components/Alert/Alert.js20
-rw-r--r--src/browser/src/components/Alert/Alert.scss54
-rw-r--r--src/browser/src/components/Alert/Alert.vue65
-rw-r--r--src/browser/src/components/FillLoader/FillLoader.scss347
-rw-r--r--src/browser/src/components/FillLoader/FillLoader.vue15
-rw-r--r--src/browser/src/components/Sidebar/Sidebar.vue76
-rw-r--r--src/browser/src/styles/_btn-states.scss51
-rw-r--r--src/browser/src/styles/_forms.scss4
-rw-r--r--src/browser/src/views/Login.vue48
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>