Merge pull request #524 from gradido/remove-base-input-example-login-vue

Remove base input example login vue
This commit is contained in:
Moriz Wahl 2021-06-14 16:30:17 +02:00 committed by GitHub
commit bdee28fde6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 398 additions and 3130 deletions

View File

@ -212,7 +212,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 21
min_coverage: 28
token: ${{ github.token }}
##############################################################################

View File

@ -1,17 +0,0 @@
//
// Circle badge
//
// General styles
.badge-circle {
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
width: 2rem;
height: 2rem;
font-size: .875rem;
}

View File

@ -1,42 +0,0 @@
//
// Dot badge
//
// General styles
.badge-dot {
padding-left: 0;
padding-right: 0;
background: transparent;
font-weight: $font-weight-normal;
font-size: $font-size-sm;
text-transform: none;
strong {
color: $gray-800;
}
i {
display: inline-block;
vertical-align: middle;
width: .375rem;
height: .375rem;
border-radius: 50%;
margin-right: .375rem;
}
&.badge-md {
i {
width: .5rem;
height: .5rem;
}
}
&.badge-lg {
i {
width: .625rem;
height: .625rem;
}
}
}

View File

@ -1,55 +0,0 @@
//
// Badge
//
// General styles
.badge {
text-transform: $badge-text-transfom;
a {
color: $white;
}
}
// Size variations
.badge-md {
padding: .65em 1em;
}
.badge-lg {
padding: .85em 1.375em;
}
// Multiple inline badges
.badge-inline {
margin-right: .625rem;
+ span {
top: 2px;
position: relative;
> a {
text-decoration: underline;
}
}
}
// Badge spacing inside a btn with some text
.btn {
.badge {
&:not(:first-child) {
margin-left: .5rem;
}
&:not(:last-child) {
margin-right: .5rem;
}
}
}

View File

@ -4,7 +4,6 @@
@import "custom/alert";
@import "custom/avatar";
@import "custom/badge";
@import "custom/buttons";
@import "custom/card";
@import "custom/chart";

View File

@ -1,45 +0,0 @@
<template>
<b-badge :variant="type" :pill="rounded" :size="size" :class="{ 'badge-circle': circle }">
<slot>
<i v-if="icon" :class="icon"></i>
</slot>
</b-badge>
</template>
<script>
export default {
name: 'badge',
props: {
tag: {
type: String,
default: 'span',
description: 'Html tag to use for the badge.',
},
rounded: {
type: Boolean,
default: false,
description: 'Whether badge is of pill type',
},
circle: {
type: Boolean,
default: false,
description: 'Whether badge is circle',
},
icon: {
type: String,
default: '',
description: 'Icon name. Will be overwritten by slot if slot is used',
},
type: {
type: String,
default: 'default',
description: 'Badge type (primary|info|danger|default|warning|success)',
},
size: {
type: String,
description: 'Badge size (md, lg)',
default: '',
},
},
}
</script>
<style></style>

View File

@ -1,75 +0,0 @@
<template>
<fade-transition>
<b-alert
v-model="visible"
:variant="type"
:class="[{ 'alert-dismissible': dismissible }]"
role="alert"
>
<slot v-if="!dismissible"></slot>
<template v-else>
<template v-if="icon || $slots.icon">
<slot name="icon">
<span class="alert-icon" data-notify="icon">
<i :class="icon"></i>
</span>
</slot>
</template>
<span class="alert-text"><slot></slot></span>
<slot name="dismiss-icon">
<button
type="button"
class="close"
data-dismiss="alert"
aria-label="Close"
@click="dismissAlert"
>
<span aria-hidden="true">×</span>
</button>
</slot>
</template>
</b-alert>
</fade-transition>
</template>
<script>
import { FadeTransition } from 'vue2-transitions'
export default {
name: 'base-alert',
components: {
FadeTransition,
},
created() {
// console.log('base-alert gesetzt in =>', this.$route.path)
},
props: {
type: {
type: String,
default: 'default',
description: 'Alert type',
},
dismissible: {
type: Boolean,
default: false,
description: 'Whether alert is dismissible (closeable)',
},
icon: {
type: String,
default: '',
description: 'Alert icon to display',
},
},
data() {
return {
visible: true,
}
},
methods: {
dismissAlert() {
this.visible = false
},
},
}
</script>

View File

@ -1,75 +0,0 @@
<template>
<b-button
:type="nativeType"
:disabled="disabled || loading"
@click="handleClick"
class="base-button"
:variant="!outline ? type : `outline-${type}`"
:size="size"
:block="block"
:class="[
{ 'rounded-circle': round },
{ 'btn-wd': wide },
{ 'btn-icon btn-fab': icon },
{ 'btn-link': link },
{ disabled: disabled },
]"
>
<slot name="loading">
<i v-if="loading" class="fas fa-spinner fa-spin"></i>
</slot>
<slot></slot>
</b-button>
</template>
<script>
export default {
name: 'base-button',
props: {
round: Boolean,
icon: Boolean,
block: Boolean,
loading: Boolean,
wide: Boolean,
disabled: Boolean,
type: {
type: String,
default: 'default',
description: 'Button type (primary|secondary|danger etc)',
},
nativeType: {
type: String,
default: 'button',
description: 'Button native type (e.g button, input etc)',
},
size: {
type: String,
default: '',
description: 'Button size (sm|lg)',
},
outline: {
type: Boolean,
description: 'Whether button is outlined (only border has color)',
},
link: {
type: Boolean,
description: 'Whether button is a link (no borders or background)',
},
},
methods: {
handleClick(evt) {
this.$emit('click', evt)
},
},
}
</script>
<style lang="scss">
.base-button {
display: inline-flex;
align-items: center;
justify-content: center;
i {
padding: 0 3px;
}
}
</style>

View File

@ -1,97 +0,0 @@
<template>
<component
:is="tag"
:class="[{ show: isOpen }, `drop${direction}`]"
@click="toggleDropDown"
v-click-outside="closeDropDown"
>
<slot name="title-container" :is-open="isOpen">
<component
:is="titleTag"
class="btn-rotate"
:class="[{ 'dropdown-toggle': hasToggle }, titleClasses]"
:aria-expanded="isOpen"
data-toggle="dropdown"
>
<slot name="title" :is-open="isOpen">
<i :class="icon"></i>
{{ title }}
</slot>
</component>
</slot>
<ul
class="dropdown-menu"
:class="[{ show: isOpen }, { 'dropdown-menu-right': menuOnRight }, menuClasses]"
>
<slot></slot>
</ul>
</component>
</template>
<script>
export default {
name: 'base-dropdown',
props: {
tag: {
type: String,
default: 'div',
description: 'Dropdown html tag (e.g div, ul etc)',
},
titleTag: {
type: String,
default: 'button',
description: 'Dropdown title (toggle) html tag',
},
title: {
type: String,
description: 'Dropdown title',
},
direction: {
type: String,
default: 'down', // up | down
description: 'Dropdown menu direction (up|down)',
},
icon: {
type: String,
description: 'Dropdown icon',
},
titleClasses: {
type: [String, Object, Array],
description: 'Title css classes',
},
menuClasses: {
type: [String, Object],
description: 'Menu css classes',
},
menuOnRight: {
type: Boolean,
description: 'Whether menu should appear on the right',
},
hasToggle: {
type: Boolean,
description: 'Whether dropdown has arrow icon shown',
default: true,
},
},
data() {
return {
isOpen: false,
}
},
methods: {
toggleDropDown() {
this.isOpen = !this.isOpen
this.$emit('change', this.isOpen)
},
closeDropDown() {
this.isOpen = false
this.$emit('change', false)
},
},
}
</script>
<style lang="scss" scoped>
.dropdown {
cursor: pointer;
user-select: none;
}
</style>

View File

@ -1,22 +0,0 @@
<template>
<div class="header" :class="{ [`bg-${type}`]: type }">
<b-container fluid>
<div class="header-body">
<slot></slot>
</div>
</b-container>
</div>
</template>
<script>
export default {
name: 'base-header',
props: {
type: {
type: String,
default: 'success',
description: 'Header background type',
},
},
}
</script>
<style></style>

View File

@ -1,67 +0,0 @@
<template>
<div>
<b-pagination
first-number
last-number
:per-page="perPage"
:size="size"
:value="value"
@change="(val) => $emit('change', val)"
:align="align"
:total-rows="total"
>
<template v-slot:prev-text>
<a class="page-link" aria-label="Previous">
<span aria-hidden="true">
<i class="fa fa-angle-left" aria-hidden="true"></i>
</span>
</a>
</template>
<template v-slot:next-text>
<a class="page-link" aria-label="Next">
<span aria-hidden="true">
<i class="fa fa-angle-right" aria-hidden="true"></i>
</span>
</a>
</template>
</b-pagination>
</div>
</template>
<script>
export default {
name: 'base-pagination',
props: {
pageCount: {
type: Number,
default: 0,
description: 'Pagination page count. This should be specified in combination with perPage',
},
perPage: {
type: Number,
default: 10,
description: 'Pagination per page. Should be specified with total or pageCount',
},
total: {
type: Number,
default: 0,
description:
'Can be specified instead of pageCount. The page count in this case will be total/perPage',
},
value: {
type: Number,
default: 1,
description: 'Pagination value',
},
size: {
type: String,
default: '',
description: 'Pagination size',
},
align: {
type: String,
default: '',
description: 'Pagination alignment (e.g center|start|end)',
},
},
}
</script>

View File

@ -1,79 +0,0 @@
<template>
<div class="wrapper">
<div :class="`progress-${type}`" v-if="showLabel">
<div class="progress-label">
<slot name="label">
<span>{{ label }}</span>
</slot>
</div>
<div class="progress-percentage">
<slot>
<span>{{ value }}%</span>
</slot>
</div>
</div>
<b-progress :size="size" :class="[progressClasses]" :style="`height: ${height}px`">
<b-progress-bar :class="computedClasses" :value="value"></b-progress-bar>
</b-progress>
</div>
</template>
<script>
export default {
name: 'base-progress',
props: {
striped: {
type: Boolean,
description: 'Whether progress is striped',
},
animated: {
type: Boolean,
description: 'Whether progress is animated (works only with `striped` prop together)',
},
label: {
type: String,
description: 'Progress label (shown on the left above progress)',
},
height: {
type: Number,
default: 3,
description: 'Progress line height',
},
type: {
type: String,
default: 'default',
description: 'Progress type (e.g danger, primary etc)',
},
showLabel: {
type: Boolean,
default: false,
},
progressClasses: {
type: [Array, String],
default: '',
description: 'Progress css classes',
},
size: {
type: String,
default: '',
},
value: {
type: Number,
default: 0,
validator: (value) => {
return value >= 0 && value <= 100
},
description: 'Progress value',
},
},
computed: {
computedClasses() {
return [
{ 'progress-bar-striped': this.striped },
{ 'progress-bar-animated': this.animated },
{ [`bg-${this.type}`]: this.type },
]
},
},
}
</script>
<style></style>

View File

@ -1,92 +0,0 @@
<template>
<div class="slider" :disabled="disabled"></div>
</template>
<script>
import noUiSlider from 'nouislider'
export default {
name: 'base-slider',
props: {
value: {
type: [String, Array, Number],
description: 'slider value',
},
disabled: {
type: Boolean,
default: false,
description: 'whether the slider is disabled',
},
start: {
type: [Number, Array],
default: 0,
description:
'[noUi Slider start](https://refreshless.com/nouislider/slider-options/#section-start)',
},
connect: {
type: [Boolean, Array],
default: () => [true, false],
description:
'[noUi Slider connect](https://refreshless.com/nouislider/slider-options/#section-connect)',
},
range: {
type: Object,
default: () => {
return {
min: 0,
max: 100,
}
},
description:
'[noUi Slider range](https://refreshless.com/nouislider/slider-values/#section-range)',
},
options: {
type: Object,
default: () => {
return {}
},
description: '[noUi Slider options](https://refreshless.com/nouislider/slider-options/)',
},
},
data() {
return {
slider: null,
}
},
methods: {
createSlider() {
noUiSlider.create(this.$el, {
start: this.value || this.start,
connect: Array.isArray(this.value) ? true : this.connect,
range: this.range,
...this.options,
})
const slider = this.$el.noUiSlider
slider.on('slide', () => {
const value = slider.get()
if (value !== this.value) {
this.$emit('input', value)
}
})
},
},
mounted() {
this.createSlider()
},
watch: {
value(newValue, oldValue) {
const slider = this.$el.noUiSlider
const sliderValue = slider.get()
if (newValue !== oldValue && sliderValue !== newValue) {
if (Array.isArray(sliderValue) && Array.isArray(newValue)) {
if (oldValue.length === newValue.length && oldValue.every((v, i) => v === newValue[i])) {
slider.set(newValue)
}
} else {
slider.set(newValue)
}
}
},
},
}
</script>
<style></style>

View File

@ -1,66 +0,0 @@
<template>
<table class="table tablesorter" :class="tableClass">
<thead :class="theadClasses">
<tr>
<slot name="columns" :columns="columns">
<th v-for="column in columns" :key="column">{{ column }}</th>
</slot>
</tr>
</thead>
<tbody :class="tbodyClasses">
<tr v-for="(item, index) in data" :key="index">
<slot :row="item" :index="index">
<td v-for="(column, index) in columns" :key="index">
{{ itemValue(item, column) }}
</td>
</slot>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'base-table',
props: {
columns: {
type: Array,
default: () => [],
description: 'Table columns',
},
data: {
type: Array,
default: () => [],
description: 'Table data',
},
type: {
type: String, // striped | hover
default: '',
description: 'Whether table is striped or hover type',
},
theadClasses: {
type: String,
default: '',
description: '<thead> css classes',
},
tbodyClasses: {
type: String,
default: '',
description: '<tbody> css classes',
},
},
computed: {
tableClass() {
return this.type && `table-${this.type}`
},
},
methods: {
hasValue(item, column) {
return item[column.toLowerCase()] !== 'undefined'
},
itemValue(item, column) {
return item[column.toLowerCase()]
},
},
}
</script>
<style></style>

View File

@ -1,25 +0,0 @@
<template>
<nav aria-label="breadcrumb">
<b-breadcrumb :class="[{ [`bg-${type}`]: type }, listClasses]">
<slot></slot>
</b-breadcrumb>
</nav>
</template>
<script>
export default {
name: 'breadcrumb',
props: {
type: {
type: String,
default: '',
description: 'Breadcrumb background type',
},
listClasses: {
type: [String, Object],
default: '',
description: 'Breadcrumb list classes',
},
},
}
</script>
<style></style>

View File

@ -1,18 +0,0 @@
<template>
<b-breadcrumb-item :active="active">
<slot></slot>
</b-breadcrumb-item>
</template>
<script>
export default {
name: 'breadcrumb-item',
props: {
active: {
type: Boolean,
default: false,
description: 'Whether breadcrumb item is active',
},
},
}
</script>
<style></style>

View File

@ -1,40 +0,0 @@
<template>
<bread-crumb list-classes="breadcrumb-links breadcrumb-dark">
<bread-crumb-item>
<router-link to="/overview">
<i class="fas fa-home"></i>
</router-link>
</bread-crumb-item>
<bread-crumb-item
v-for="(route, index) in $route.matched.slice()"
:key="route.name"
:active="index === $route.matched.length - 1"
style="display: inline-block"
>
<router-link :to="{ name: route.name }" v-if="index < $route.matched.length - 1">
{{ route.name }}
</router-link>
<span v-else>{{ route.name }}</span>
</bread-crumb-item>
</bread-crumb>
</template>
<script>
import BreadCrumb from './Breadcrumb'
import BreadCrumbItem from './BreadcrumbItem'
export default {
name: 'route-breadcrumb',
components: {
BreadCrumb,
BreadCrumbItem,
},
methods: {
getBreadName(route) {
return route.name
},
},
}
</script>
<style scoped></style>

View File

@ -1,38 +0,0 @@
<template>
<div class="btn-group-toggle" data-toggle="buttons">
<label class="btn" :class="[{ active: value }, buttonClasses]">
<input v-model="model" type="checkbox" checked="" autocomplete="off" />
<slot></slot>
</label>
</div>
</template>
<script>
export default {
name: 'button-checkbox',
props: {
value: {
type: Boolean,
description: 'Checked value',
},
buttonClasses: {
type: [String, Object],
description: 'Inner button css classes',
},
},
model: {
prop: 'value',
event: 'change',
},
computed: {
model: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
},
},
},
}
</script>
<style></style>

View File

@ -1,55 +0,0 @@
<template>
<div class="btn-group-toggle" data-toggle="buttons">
<label
v-for="(option, index) in options"
:key="index"
class="btn"
:class="[{ active: value === option.value }, buttonClasses]"
>
<input
:value="option.value"
v-model="model"
type="radio"
id="option1"
autocomplete="off"
checked=""
/>
{{ option.label }}
</label>
</div>
</template>
<script>
export default {
name: 'button-radio-group',
props: {
options: {
type: Array,
description: 'Radio options. Should be an array of objects {value: "", label: ""}',
default: () => [],
},
value: {
type: String,
description: 'Radio value',
},
buttonClasses: {
type: [String, Object],
description: 'Inner button css classes',
},
},
model: {
prop: 'value',
event: 'change',
},
computed: {
model: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
},
},
},
}
</script>
<style></style>

View File

@ -1,71 +0,0 @@
<template>
<b-card
no-body
:class="[
{ 'card-lift--hover': hover },
{ shadow: shadow },
{ [`shadow-${shadowSize}`]: shadowSize },
{ [`bg-gradient-${gradient}`]: gradient },
{ [`bg-${type}`]: type },
]"
>
<slot name="image"></slot>
<b-card-header :class="headerClasses" v-if="$slots.header">
<slot name="header"></slot>
</b-card-header>
<b-card-body :class="bodyClasses" v-if="!noBody">
<slot></slot>
</b-card-body>
<slot v-if="noBody"></slot>
<b-card-footer :class="footerClasses" v-if="$slots.footer">
<slot name="footer"></slot>
</b-card-footer>
</b-card>
</template>
<script>
export default {
name: 'card',
props: {
type: {
type: String,
description: 'Card type',
},
gradient: {
type: String,
description: 'Card background gradient type (warning,danger etc)',
},
hover: {
type: Boolean,
description: 'Whether card should move on hover',
},
shadow: {
type: Boolean,
description: 'Whether card has shadow',
},
shadowSize: {
type: String,
description: 'Card shadow size',
},
noBody: {
type: Boolean,
default: false,
description: 'Whether card should have wrapper body class',
},
bodyClasses: {
type: [String, Object, Array],
description: 'Card body css classes',
},
headerClasses: {
type: [String, Object, Array],
description: 'Card header css classes',
},
footerClasses: {
type: [String, Object, Array],
description: 'Card footer css classes',
},
},
}
</script>
<style></style>

View File

@ -1,58 +0,0 @@
<template>
<card class="card-stats" :show-footer-line="true">
<b-row>
<b-col>
<slot>
<h5 class="card-title text-uppercase text-muted mb-0" v-if="title">
{{ title }}
</h5>
<span class="h2 font-weight-bold mb-0" v-if="subTitle">
{{ subTitle }}
</span>
</slot>
</b-col>
<b-col cols="auto" v-if="$slots.icon || icon">
<slot name="icon">
<div
class="icon icon-shape text-white rounded-circle shadow"
:class="[`bg-${type}`, iconClasses]"
>
<i :class="icon"></i>
</div>
</slot>
</b-col>
<b-col cols="auto" v-if="$slots.img || img">
<slot name="img">
<img :src="img" width="80" />
</slot>
</b-col>
</b-row>
<p class="mt-3 mb-0 text-sm">
<slot name="footer"></slot>
</p>
</card>
</template>
<script>
import Card from './Card.vue'
export default {
name: 'stats-card',
components: {
Card,
},
props: {
type: {
type: String,
default: 'primary',
},
icon: String,
img: String,
title: String,
subTitle: String,
iconClasses: [String, Array],
},
}
</script>
<style></style>

View File

@ -1,30 +0,0 @@
import { Bar, mixins } from 'vue-chartjs'
import globalOptionsMixin from '@/components/Charts/globalOptionsMixin'
export default {
name: 'bar-chart',
extends: Bar,
mixins: [mixins.reactiveProp, globalOptionsMixin],
props: {
extraOptions: {
type: Object,
default: () => ({}),
},
},
data() {
return {
ctx: null,
}
},
mounted() {
this.$watch(
'chartData',
(newVal, oldVal) => {
if (!oldVal) {
this.renderChart(this.chartData, this.extraOptions)
}
},
{ immediate: true },
)
},
}

View File

@ -1,29 +0,0 @@
import { Line, mixins } from 'vue-chartjs'
import globalOptionsMixin from '@/components/Charts/globalOptionsMixin'
export default {
name: 'line-chart',
extends: Line,
mixins: [mixins.reactiveProp, globalOptionsMixin],
props: {
extraOptions: {
type: Object,
default: () => ({}),
},
},
data() {
return {
ctx: null,
}
},
mounted() {
this.$watch(
'chartData',
(newVal, oldVal) => {
if (!oldVal) {
this.renderChart(this.chartData, this.extraOptions)
}
},
{ immediate: true },
)
},
}

View File

@ -1,234 +0,0 @@
import { parseOptions } from '@/components/Charts/optionHelpers'
import Chart from 'chart.js'
export const Charts = {
mode: 'light', // (themeMode) ? themeMode : 'light';
fonts: {
base: 'Open Sans',
},
colors: {
gray: {
100: '#f6f9fc',
200: '#e9ecef',
300: '#dee2e6',
400: '#ced4da',
500: '#adb5bd',
600: '#8898aa',
700: '#525f7f',
800: '#32325d',
900: '#212529',
},
theme: {
default: '#172b4d',
primary: '#5e72e4',
secondary: '#f4f5f7',
info: '#11cdef',
success: '#2dce89',
danger: '#f5365c',
warning: '#fb6340',
},
black: '#12263F',
white: '#FFFFFF',
transparent: 'transparent',
},
}
function chartOptions() {
const { colors, mode, fonts } = Charts
// Options
const options = {
defaults: {
global: {
responsive: true,
maintainAspectRatio: false,
defaultColor: mode === 'dark' ? colors.gray[700] : colors.gray[600],
defaultFontColor: mode === 'dark' ? colors.gray[700] : colors.gray[600],
defaultFontFamily: fonts.base,
defaultFontSize: 13,
layout: {
padding: 0,
},
legend: {
display: false,
position: 'bottom',
labels: {
usePointStyle: true,
padding: 16,
},
},
elements: {
point: {
radius: 0,
backgroundColor: colors.theme.primary,
},
line: {
tension: 0.4,
borderWidth: 4,
borderColor: colors.theme.primary,
backgroundColor: colors.transparent,
borderCapStyle: 'rounded',
},
rectangle: {
backgroundColor: colors.theme.warning,
},
arc: {
backgroundColor: colors.theme.primary,
borderColor: mode === 'dark' ? colors.gray[800] : colors.white,
borderWidth: 4,
},
},
tooltips: {
enabled: true,
mode: 'index',
intersect: false,
},
},
pie: {
tooltips: {
mode: 'point',
},
},
doughnut: {
tooltips: {
mode: 'point',
},
cutoutPercentage: 83,
legendCallback: function (chart) {
const data = chart.data
let content = ''
data.labels.forEach(function (label, index) {
const bgColor = data.datasets[0].backgroundColor[index]
content += '<span class="chart-legend-item">'
content +=
'<i class="chart-legend-indicator" style="background-color: ' + bgColor + '"></i>'
content += label
content += '</span>'
})
return content
},
},
},
}
// yAxes
Chart.scaleService.updateScaleDefaults('linear', {
gridLines: {
borderDash: [2],
borderDashOffset: [2],
color: mode === 'dark' ? colors.gray[900] : colors.gray[200],
drawBorder: false,
drawTicks: true,
zeroLineWidth: 1,
zeroLineColor: mode === 'dark' ? colors.gray[900] : colors.gray[200],
zeroLineBorderDash: [2],
zeroLineBorderDashOffset: [2],
},
ticks: {
beginAtZero: true,
padding: 10,
callback: function (value) {
if (!(value % 10)) {
return value
}
},
},
})
// xAxes
Chart.scaleService.updateScaleDefaults('category', {
gridLines: {
drawBorder: false,
drawOnChartArea: false,
drawTicks: false,
lineWidth: 1,
zeroLineWidth: 1,
},
ticks: {
padding: 20,
},
maxBarThickness: 10,
})
return options
}
let initialized = false
export function initGlobalOptions() {
if (initialized) {
return
}
parseOptions(Chart, chartOptions())
initialized = true
}
export const basicOptions = {
maintainAspectRatio: false,
legend: {
display: false,
},
responsive: true,
}
export const blueChartOptions = {
scales: {
yAxes: [
{
gridLines: {
color: Charts.colors.gray[700],
zeroLineColor: Charts.colors.gray[700],
},
},
],
},
}
export const lineChartOptionsBlue = {
...basicOptions,
tooltips: {
backgroundColor: '#f5f5f5',
titleFontColor: '#333',
bodyFontColor: '#666',
bodySpacing: 4,
xPadding: 12,
mode: 'nearest',
intersect: 0,
position: 'nearest',
},
responsive: true,
scales: {
yAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: 'rgba(29,140,248,0.0)',
zeroLineColor: 'transparent',
},
ticks: {
suggestedMin: 60,
suggestedMax: 125,
padding: 20,
fontColor: '#9e9e9e',
},
},
],
xAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: 'rgba(29,140,248,0.1)',
zeroLineColor: 'transparent',
},
ticks: {
padding: 20,
fontColor: '#9e9e9e',
},
},
],
},
}

View File

@ -1,7 +0,0 @@
import { initGlobalOptions } from '@/components/Charts/config'
import './roundedCornersExtension'
export default {
mounted() {
initGlobalOptions()
},
}

View File

@ -1,10 +0,0 @@
// Parse global options
export function parseOptions(parent, options) {
for (const item in options) {
if (typeof options[item] !== 'object') {
parent[item] = options[item]
} else {
parseOptions(parent[item], options[item])
}
}
}

View File

@ -1,126 +0,0 @@
//
// Chart extension for making the bars rounded
// Code from: https://codepen.io/jedtrow/full/ygRYgo
//
import Chart from 'chart.js'
Chart.elements.Rectangle.prototype.draw = function () {
const ctx = this._chart.ctx
const vm = this._view
let left, right, top, bottom, signX, signY, borderSkipped
let borderWidth = vm.borderWidth
// Set Radius Here
// If radius is large enough to cause drawing errors a max radius is imposed
const cornerRadius = 6
if (!vm.horizontal) {
// bar
left = vm.x - vm.width / 2
right = vm.x + vm.width / 2
top = vm.y
bottom = vm.base
signX = 1
signY = bottom > top ? 1 : -1
borderSkipped = vm.borderSkipped || 'bottom'
} else {
// horizontal bar
left = vm.base
right = vm.x
top = vm.y - vm.height / 2
bottom = vm.y + vm.height / 2
signX = right > left ? 1 : -1
signY = 1
borderSkipped = vm.borderSkipped || 'left'
}
// Canvas doesn't allow us to stroke inside the width so we can
// adjust the sizes to fit if we're setting a stroke on the line
if (borderWidth) {
// borderWidth shold be less than bar width and bar height.
const barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom))
borderWidth = borderWidth > barSize ? barSize : borderWidth
const halfStroke = borderWidth / 2
// Adjust borderWidth when bar top position is near vm.base(zero).
const borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0)
const borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0)
const borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0)
const borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0)
// not become a vertical line?
if (borderLeft !== borderRight) {
top = borderTop
bottom = borderBottom
}
// not become a horizontal line?
if (borderTop !== borderBottom) {
left = borderLeft
right = borderRight
}
}
ctx.beginPath()
ctx.fillStyle = vm.backgroundColor
ctx.strokeStyle = vm.borderColor
ctx.lineWidth = borderWidth
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
const corners = [
[left, bottom],
[left, top],
[right, top],
[right, bottom],
]
// Find first (starting) corner with fallback to 'bottom'
const borders = ['bottom', 'left', 'top', 'right']
let startCorner = borders.indexOf(borderSkipped, 0)
if (startCorner === -1) {
startCorner = 0
}
function cornerAt(index) {
return corners[(startCorner + index) % 4]
}
// Draw rectangle from 'startCorner'
let corner = cornerAt(0)
ctx.moveTo(corner[0], corner[1])
for (let i = 1; i < 4; i++) {
corner = cornerAt(i)
let nextCornerId = i + 1
if (nextCornerId === 4) {
nextCornerId = 0
}
const width = corners[2][0] - corners[1][0]
const height = corners[0][1] - corners[1][1]
const x = corners[1][0]
const y = corners[1][1]
let radius = cornerRadius
// Fix radius being too large
if (radius > height / 2) {
radius = height / 2
}
if (radius > width / 2) {
radius = width / 2
}
ctx.moveTo(x + radius, y)
ctx.lineTo(x + width - radius, y)
ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
ctx.lineTo(x + width, y + height - radius)
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
ctx.lineTo(x + radius, y + height)
ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
ctx.lineTo(x, y + radius)
ctx.quadraticCurveTo(x, y, x + radius, y)
}
ctx.fill()
if (borderWidth) {
ctx.stroke()
}
}

View File

@ -1,28 +0,0 @@
import { mount } from '@vue/test-utils'
import CloseButton from './CloseButton'
const localVue = global.localVue
describe('CloseButton', () => {
let wrapper
const propsData = {
target: 'Target',
expanded: false,
}
const Wrapper = () => {
return mount(CloseButton, { localVue, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('emmits click event', () => {
wrapper.find('.navbar-toggler').trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
})
})

View File

@ -1,36 +0,0 @@
<template>
<button
type="button"
class="navbar-toggler"
data-toggle="collapse"
@click="handleClick"
:data-target="`#${target}`"
:aria-controls="target"
:aria-expanded="expanded"
aria-label="Toggle navigation"
>
<span></span>
<span></span>
</button>
</template>
<script>
export default {
name: 'close-button',
props: {
target: {
type: [String, Number],
description: 'Close button target element',
},
expanded: {
type: Boolean,
description: 'Whether button is expanded (aria-expanded attribute)',
},
},
methods: {
handleClick(evt) {
this.$emit('click', evt)
},
},
}
</script>
<style></style>

View File

@ -1,78 +0,0 @@
<template>
<div
class="custom-control custom-checkbox"
:class="[{ disabled: disabled }, { [`custom-checkbox-${type}`]: type }, inlineClass]"
>
<input
:id="cbId"
class="custom-control-input"
:class="inputClasses"
type="checkbox"
:disabled="disabled"
v-model="model"
/>
<label :for="cbId" class="custom-control-label">
<slot>
<span v-if="inline">&nbsp;</span>
</slot>
</label>
</div>
</template>
<script>
export default {
name: 'base-checkbox',
model: {
prop: 'checked',
},
props: {
checked: {
type: [Array, Boolean],
description: 'Whether checkbox is checked',
},
disabled: {
type: Boolean,
description: 'Whether checkbox is disabled',
},
inline: {
type: Boolean,
description: 'Whether checkbox is inline',
},
inputClasses: {
type: [String, Object, Array],
description: 'Checkbox input classes',
},
type: {
type: String,
description: 'Checkbox type (e.g info, danger etc)',
},
},
data() {
return {
cbId: '',
touched: false,
}
},
computed: {
model: {
get() {
return this.checked
},
set(check) {
if (!this.touched) {
this.touched = true
}
this.$emit('input', check)
},
},
inlineClass() {
if (this.inline) {
return `form-check-inline`
}
return ''
},
},
created() {
this.cbId = Math.random().toString(16).slice(2)
},
}
</script>

View File

@ -1,187 +0,0 @@
<template>
<validation-provider
:rules="rules"
:name="name"
v-bind="$attrs"
v-slot="{ errors, valid, invalid, validated }"
>
<b-form-group>
<slot name="label">
<label v-if="label" :class="labelClasses">
{{ label }}
</label>
</slot>
<div
:class="[
{ 'input-group': hasIcon },
{ focused: focused },
{ 'input-group-alternative': alternative },
{ 'has-label': label || $slots.label },
inputGroupClasses,
]"
>
<div v-if="prependIcon || $slots.prepend" class="input-group-prepend">
<span class="input-group-text">
<slot name="prepend">
<i :class="prependIcon"></i>
</slot>
</span>
</div>
<slot v-bind="slotData">
<input
:value="value"
:type="type"
v-on="listeners"
v-bind="$attrs"
:valid="valid"
:required="required"
class="form-control"
:class="[
{ 'is-valid': valid && validated && successMessage },
{ 'is-invalid': invalid && validated },
inputClasses,
]"
/>
</slot>
<div v-if="appendIcon || $slots.append" class="input-group-append">
<span class="input-group-text">
<slot name="append">
<i :class="appendIcon"></i>
</slot>
</span>
</div>
<slot name="infoBlock"></slot>
</div>
<slot name="success">
<div class="valid-feedback" v-if="valid && validated && successMessage">
{{ successMessage }}
</div>
</slot>
<slot name="error">
<div v-if="errors[0]" class="invalid-feedback" style="display: block">
{{ errors[0] }}
</div>
</slot>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
inheritAttrs: false,
name: 'base-input',
props: {
required: {
type: Boolean,
description: 'Whether input is required (adds an asterix *)',
},
group: {
type: Boolean,
description: 'Whether input is an input group (manual override in special cases)',
},
alternative: {
type: Boolean,
description: 'Whether input is of alternative layout',
},
label: {
type: String,
description: 'Input label (text before input)',
},
error: {
type: String,
description: 'Input error (below input)',
},
successMessage: {
type: String,
description: 'Input success message',
default: '',
},
labelClasses: {
type: String,
description: 'Input label css classes',
default: 'form-control-label',
},
inputClasses: {
type: String,
description: 'Input css classes',
},
inputGroupClasses: {
type: String,
description: 'Input group css classes',
},
value: {
type: [String, Number],
description: 'Input value',
},
type: {
type: String,
description: 'Input type',
default: 'text',
},
appendIcon: {
type: String,
description: 'Append icon (right)',
},
prependIcon: {
type: String,
description: 'Prepend icon (left)',
},
rules: {
type: [String, Array, Object],
description: 'Vee validate validation rules',
default: '',
},
name: {
type: String,
description: 'Input name (used for validation)',
default: '',
},
},
data() {
return {
focused: false,
}
},
computed: {
listeners() {
return {
...this.$listeners,
input: this.updateValue,
focus: this.onFocus,
blur: this.onBlur,
}
},
slotData() {
return {
focused: this.focused,
error: this.error,
...this.listeners,
}
},
hasIcon() {
const { append, prepend } = this.$slots
return (
append !== undefined ||
prepend !== undefined ||
this.appendIcon !== undefined ||
this.prependIcon !== undefined ||
this.group
)
},
},
methods: {
updateValue(evt) {
const value = evt.target.value
this.$emit('input', value)
},
onFocus(evt) {
this.focused = true
this.$emit('focus', evt)
},
onBlur(evt) {
this.focused = false
this.$emit('blur', evt)
},
},
}
</script>
<style></style>

View File

@ -1,64 +0,0 @@
<template>
<div class="custom-control custom-radio" :class="[inlineClass, { disabled: disabled }]">
<input
:id="cbId"
class="custom-control-input"
type="radio"
:disabled="disabled"
:value="name"
v-model="model"
/>
<label :for="cbId" class="custom-control-label">
<slot>
<span v-if="inline">&nbsp;</span>
</slot>
</label>
</div>
</template>
<script>
export default {
name: 'base-radio',
props: {
name: {
type: [String, Number],
description: 'Radio label',
},
disabled: {
type: Boolean,
description: 'Whether radio is disabled',
},
value: {
type: [String, Boolean],
description: 'Radio value',
},
inline: {
type: Boolean,
description: 'Whether radio is inline',
},
},
data() {
return {
cbId: '',
}
},
computed: {
model: {
get() {
return this.value
},
set(value) {
this.$emit('input', value)
},
},
inlineClass() {
if (this.inline) {
return `form-check-inline`
}
return ''
},
},
created() {
this.cbId = Math.random().toString(16).slice(2)
},
}
</script>

View File

@ -1,127 +0,0 @@
<template>
<slide-y-up-transition :duration="animationDuration">
<b-modal
class="modal fade"
ref="app-modal"
:size="size"
:hide-header="!$slots.header"
:modal-class="[{ 'modal-mini': type === 'mini' }, ...modalClasses]"
@mousedown.self="closeModal"
tabindex="-1"
role="dialog"
centered
@close="closeModal"
@hide="closeModal"
:header-class="headerClasses"
:footer-class="footerClasses"
:content-class="[gradient ? `bg-gradient-${gradient}` : '', ...modalContentClasses]"
:body-class="bodyClasses"
:aria-hidden="!show"
>
<template v-slot:modal-header>
<slot name="header"></slot>
<slot name="close-button">
<button
type="button"
class="close"
v-if="showClose"
@click="closeModal"
data-dismiss="modal"
aria-label="Close"
>
<span :aria-hidden="!show">×</span>
</button>
</slot>
</template>
<slot />
<template v-slot:modal-footer>
<slot name="footer"></slot>
</template>
</b-modal>
</slide-y-up-transition>
</template>
<script>
import { SlideYUpTransition } from 'vue2-transitions'
export default {
name: 'modal',
components: {
SlideYUpTransition,
},
props: {
show: Boolean,
showClose: {
type: Boolean,
default: true,
},
type: {
type: String,
default: '',
validator(value) {
const acceptedValues = ['', 'notice', 'mini']
return acceptedValues.indexOf(value) !== -1
},
description: 'Modal type (notice|mini|"") ',
},
modalClasses: {
type: [Object, String],
description: 'Modal dialog css classes',
},
size: {
type: String,
description: 'Modal size',
validator(value) {
const acceptedValues = ['', 'sm', 'lg']
return acceptedValues.indexOf(value) !== -1
},
},
modalContentClasses: {
type: [Object, String],
description: 'Modal dialog content css classes',
},
gradient: {
type: String,
description: 'Modal gradient type (danger, primary etc)',
},
headerClasses: {
type: [Object, String],
description: 'Modal Header css classes',
},
bodyClasses: {
type: [Object, String],
description: 'Modal Body css classes',
},
footerClasses: {
type: [Object, String],
description: 'Modal Footer css classes',
},
animationDuration: {
type: Number,
default: 500,
description: 'Modal transition duration',
},
},
methods: {
closeModal() {
this.$emit('update:show', false)
this.$emit('close')
},
},
watch: {
show(val) {
if (val) {
this.$refs['app-modal'].show()
} else {
this.$refs['app-modal'].hide()
}
},
},
}
</script>
<style>
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6) !important;
}
</style>

View File

@ -1,80 +0,0 @@
<template>
<!--Notice modal-->
<modal
:show.sync="$store.state.modals"
modal-classes="modal-danger"
modal-content-classes="bg-gradient-danger"
>
<h6 slot="header" class="modal-title">Your attention is required</h6>
<div class="py-3 text-center">
<form ref="form" @submit.stop.prevent="handleSubmit">
<b-form-group
label="Name"
label-for="name-input"
invalid-feedback="Name is required"
:state="nameState"
>
<b-form-input id="name-input" v-model="name" :state="nameState" required></b-form-input>
</b-form-group>
</form>
</div>
<template slot="footer">
<base-button type="white">Ok</base-button>
<base-button type="link" class="ml-auto" @click="$store.state.modals = false">
abbrechen
</base-button>
</template>
</modal>
</template>
<script>
export default {
name: 'modal',
data() {
return {
name: '',
nameState: null,
submittedNames: [],
}
},
/* Modal */
checkFormValidity() {
const valid = this.$refs.form.checkValidity()
this.nameState = valid
return valid
},
resetModal() {
this.name = ''
this.nameState = null
},
handleOk(bvModalEvt) {
// Prevent modal from closing
bvModalEvt.preventDefault()
// Trigger submit handler
this.handleSubmit()
},
handleSubmit() {
// Exit when the form isn't valid
if (!this.checkFormValidity()) {
return
}
// Push the name to submitted names
this.submittedNames.push(this.name)
this.$store.state.modals = false
this.$store.commit('loginAsAdmin')
this.$router.push('/AdminOverview')
// Hide the modal manually
this.$nextTick(() => {
this.$bvModal.hide('modal-prevent-closing')
})
},
}
</script>
<style>
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6) !important;
}
</style>

View File

@ -1,120 +0,0 @@
<template>
<b-navbar toggleable :class="classes">
<div :class="containerClasses">
<slot name="brand"></slot>
<slot name="toggle-button">
<button
class="navbar-toggler collapsed"
v-if="hasMenu"
type="button"
@click="toggleMenu"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-bar navbar-kebab"></span>
<span class="navbar-toggler-bar navbar-kebab"></span>
<span class="navbar-toggler-bar navbar-kebab"></span>
</button>
</slot>
<b-navbar-toggle target="nav-text-collapse" @click.stop="toggleMenu"></b-navbar-toggle>
<b-collapse
is-nav
id="nav-text-collapse"
class="navbar-custom-collapse collapse"
:class="menuClasses"
:visible="show"
v-click-outside="closeMenu"
>
<slot :close-menu="closeMenu"></slot>
</b-collapse>
</div>
</b-navbar>
</template>
<script>
export default {
name: 'base-nav',
props: {
show: {
type: Boolean,
default: false,
description:
'Whether navbar menu is shown (valid for viewports < specified by `expand` prop)',
},
transparent: {
type: Boolean,
default: false,
description: 'Whether navbar is transparent',
},
expand: {
type: String,
default: 'lg',
description: 'Breakpoint where nav should expand',
},
menuClasses: {
type: [String, Object, Array],
default: '',
description: 'Navbar menu (items) classes. Can be used to align menu items to the right/left',
},
containerClasses: {
type: [String, Object, Array],
default: 'container',
description:
'Container classes. Can be used to control container classes (contains both navbar brand and menu items)',
},
type: {
type: String,
default: '',
validator(value) {
return [
'',
'dark',
'success',
'danger',
'warning',
'white',
'primary',
'light',
'info',
'vue',
].includes(value)
},
description: 'Navbar color type',
},
},
model: {
prop: 'show',
event: 'change',
},
computed: {
classes() {
const color = `bg-${this.type}`
const classes = [
{ 'navbar-transparent': this.transparent },
{ [`navbar-expand-${this.expand}`]: this.expand },
]
if (this.position) {
classes.push(`navbar-${this.position}`)
}
if (!this.transparent) {
classes.push(color)
}
return classes
},
hasMenu() {
return this.$slots.default
},
},
methods: {
toggleMenu() {
this.$emit('change', !this.show)
},
closeMenu() {
this.$emit('change', false)
},
},
}
</script>
<style></style>

View File

@ -1,194 +0,0 @@
<template>
<div
@click="tryClose"
data-notify="container"
class="alert alert-notify alert-dismissible"
:class="[{ 'alert-with-icon': icon }, verticalAlign, horizontalAlign, alertType]"
role="alert"
:style="customPosition"
data-notify-position="top-center"
>
<template v-if="icon || $slots.icon">
<slot name="icon">
<span class="alert-icon" data-notify="icon">
<i :class="icon"></i>
</span>
</slot>
</template>
<span class="alert-text">
<span v-if="title" class="title">
<b>
{{ title }}
<br />
</b>
</span>
<span v-if="message" v-html="message"></span>
<content-render v-if="!message && component" :component="component"></content-render>
</span>
<slot name="dismiss-icon">
<button type="button" class="close" data-dismiss="alert" aria-label="Close" @click="close">
<span aria-hidden="true">×</span>
</button>
</slot>
</div>
</template>
<script>
export default {
name: 'notification',
components: {
contentRender: {
props: ['component'],
render: function (createElement) {
return createElement(this.component)
},
},
},
props: {
message: String,
title: {
type: String,
description: 'Notification title',
},
icon: {
type: String,
description: 'Notification icon',
},
verticalAlign: {
type: String,
default: 'top',
validator: (value) => {
const acceptedValues = ['top', 'bottom']
return acceptedValues.indexOf(value) !== -1
},
description: 'Vertical alignment of notification (top|bottom)',
},
horizontalAlign: {
type: String,
default: 'right',
validator: (value) => {
const acceptedValues = ['left', 'center', 'right']
return acceptedValues.indexOf(value) !== -1
},
description: 'Horizontal alignment of notification (left|center|right)',
},
type: {
type: String,
default: 'info',
validator: (value) => {
const acceptedValues = ['default', 'info', 'primary', 'danger', 'warning', 'success']
return acceptedValues.indexOf(value) !== -1
},
description:
'Notification type of notification (default|info|primary|danger|warning|success)',
},
timeout: {
type: Number,
default: 5000,
validator: (value) => {
return value >= 0
},
description: 'Notification timeout (closes after X milliseconds). Default is 5000 (5s)',
},
timestamp: {
type: Date,
default: () => new Date(),
description:
'Notification timestamp (used internally to handle notification removal correctly)',
},
component: {
type: [Object, Function],
description: 'Custom content component. Cane be a `.vue` component or render function',
},
showClose: {
type: Boolean,
default: true,
description: 'Whether to show close button',
},
closeOnClick: {
type: Boolean,
default: true,
description: "Whether to close notification when clicking it' body",
},
clickHandler: {
type: Function,
description: 'Custom notification click handler',
},
},
data() {
return {
elmHeight: 0,
}
},
computed: {
hasIcon() {
return this.icon && this.icon.length > 0
},
alertType() {
return `alert-${this.type}`
},
customPosition() {
const initialMargin = 20
const alertHeight = this.elmHeight + 10
let sameAlertsCount = this.$notifications.state.filter((alert) => {
return (
alert.horizontalAlign === this.horizontalAlign &&
alert.verticalAlign === this.verticalAlign &&
alert.timestamp <= this.timestamp
)
}).length
if (this.$notifications.settings.overlap) {
sameAlertsCount = 1
}
const pixels = (sameAlertsCount - 1) * alertHeight + initialMargin
const styles = {}
if (this.verticalAlign === 'top') {
styles.top = `${pixels}px`
} else {
styles.bottom = `${pixels}px`
}
return styles
},
},
methods: {
close() {
this.$emit('close', this.timestamp)
},
tryClose(evt) {
if (this.clickHandler) {
this.clickHandler(evt, this)
}
if (this.closeOnClick) {
this.close()
}
},
},
mounted() {
this.elmHeight = this.$el.clientHeight
if (this.timeout) {
setTimeout(this.close, this.timeout)
}
},
}
</script>
<style lang="scss">
.notifications .alert {
position: fixed;
z-index: 10000;
&[data-notify='container'] {
max-width: 500px;
}
&.center {
margin: 0 auto;
}
&.left {
left: 20px;
}
&.right {
right: 20px;
}
}
</style>

View File

@ -1,52 +0,0 @@
<template>
<div class="notifications">
<slide-y-up-transition :duration="transitionDuration" group mode="out-in">
<notification
v-for="notification in notifications"
v-bind="notification"
:clickHandler="notification.onClick"
:key="notification.timestamp.getTime()"
@close="removeNotification"
></notification>
</slide-y-up-transition>
</div>
</template>
<script>
import Notification from './Notification.vue'
import { SlideYUpTransition } from 'vue2-transitions'
export default {
components: {
SlideYUpTransition,
Notification,
},
props: {
transitionDuration: {
type: Number,
default: 200,
},
overlap: {
type: Boolean,
default: false,
},
},
data() {
return {
notifications: this.$notifications.state,
}
},
methods: {
removeNotification(timestamp) {
this.$notifications.removeNotification(timestamp)
},
},
created() {
this.$notifications.settings.overlap = this.overlap
},
watch: {
overlap: function (newVal) {
this.$notifications.settings.overlap = newVal
},
},
}
</script>

View File

@ -1,66 +0,0 @@
import Notifications from './Notifications.vue'
const NotificationStore = {
state: [], // here the notifications will be added
settings: {
overlap: false,
verticalAlign: 'top',
horizontalAlign: 'right',
type: 'info',
timeout: 5000,
closeOnClick: true,
showClose: true,
},
setOptions(options) {
this.settings = Object.assign(this.settings, options)
},
removeNotification(timestamp) {
const indexToDelete = this.state.findIndex((n) => n.timestamp === timestamp)
if (indexToDelete !== -1) {
this.state.splice(indexToDelete, 1)
}
},
addNotification(notification) {
if (typeof notification === 'string' || notification instanceof String) {
notification = { message: notification }
}
notification.timestamp = new Date()
notification.timestamp.setMilliseconds(
notification.timestamp.getMilliseconds() + this.state.length,
)
notification = Object.assign({}, this.settings, notification)
this.state.push(notification)
},
notify(notification) {
if (Array.isArray(notification)) {
notification.forEach((notificationInstance) => {
this.addNotification(notificationInstance)
})
} else {
this.addNotification(notification)
}
},
}
const NotificationsPlugin = {
install(Vue, options) {
const app = new Vue({
data: {
notificationStore: NotificationStore,
},
methods: {
notify(notification) {
this.notificationStore.notify(notification)
},
},
})
Vue.prototype.$notify = app.notify
Vue.prototype.$notifications = app.notificationStore
Vue.component('Notifications', Notifications)
if (options) {
NotificationStore.setOptions(options)
}
},
}
export default NotificationsPlugin

View File

@ -1,81 +0,0 @@
<template>
<vue-bootstrap-typeahead v-model="query" :data="users" @change="getUser" />
</template>
<script>
import VueBootstrapTypeahead from 'vue-bootstrap-typeahead'
// Global registration
// Vue.component('vue-bootstrap-typeahead', VueBootstrapTypeahead)
// OR
// Local registration
export default {
name: 'SearchUser',
components: {
VueBootstrapTypeahead,
},
data() {
return {
user: '',
users: [
'Bob',
'Alice',
'Bernd',
'Dario',
'Alex',
'Pauls',
'Ulf',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illnois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
],
}
},
methods: {
getUser() {
alert(this.data.user)
},
},
}
</script>

View File

@ -41,6 +41,19 @@ describe('SideBar', () => {
expect(wrapper.find('#sidenav-main').exists()).toBeTruthy()
})
describe('navbar button', () => {
it('has a navbar button', () => {
expect(wrapper.find('button.navbar-toggler').exists()).toBeTruthy()
})
it('calls showSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')
wrapper.find('button.navbar-toggler').trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalledWith(true)
})
})
describe('balance', () => {
it('shows em-dash as balance while loading', () => {
expect(wrapper.find('div.row.text-center').text()).toBe('— GDD')
@ -55,19 +68,6 @@ describe('SideBar', () => {
})
})
describe('navbar button', () => {
it('has a navbar button', () => {
expect(wrapper.find('button.navbar-toggler').exists()).toBeTruthy()
})
it('calls showSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')
wrapper.find('button.navbar-toggler').trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalledWith(true)
})
})
describe('close siedbar', () => {
it('calls closeSidebar when clicked', async () => {
const spy = jest.spyOn(wrapper.vm.$sidebar, 'displaySidebar')

View File

@ -14,13 +14,11 @@
</b-row>
<slot name="mobile-right">
<ul class="nav align-items-center d-md-none">
<a slot="title-container" class="nav-link" role="button">
<div class="media align-items-center">
<span class="avatar avatar-sm">
<vue-qrcode :value="$store.state.email" type="image/png"></vue-qrcode>
</span>
</div>
</a>
<div class="media align-items-center">
<span class="avatar avatar-sm">
<vue-qrcode :value="$store.state.email" type="image/png"></vue-qrcode>
</span>
</div>
</ul>
</slot>
<slot></slot>
@ -71,15 +69,15 @@
</template>
<script>
import NavbarToggleButton from '@/components/NavbarToggleButton'
import VueQrcode from 'vue-qrcode'
import LanguageSwitch from '@/components/LanguageSwitch.vue'
import VueQrcode from 'vue-qrcode'
export default {
name: 'sidebar',
components: {
NavbarToggleButton,
VueQrcode,
LanguageSwitch,
VueQrcode,
},
props: {
logo: {

View File

@ -10,12 +10,10 @@ const SidebarStore = {
},
toggleMinimize() {
document.body.classList.toggle('sidebar-mini')
// we simulate the window Resize so the charts will get updated in realtime.
const simulateWindowResize = setInterval(() => {
window.dispatchEvent(new Event('resize'))
}, 180)
// we stop the simulation of Window Resize after the animations are completed
setTimeout(() => {
clearInterval(simulateWindowResize)
}, 1000)

View File

@ -1,33 +0,0 @@
<template>
<div
class="tab-pane"
v-show="active"
:id="id || title"
:class="{ active: active }"
:aria-expanded="active"
>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'tab-pane',
props: ['title', 'id'],
inject: ['addTab', 'removeTab'],
data() {
return {
active: false,
}
},
mounted() {
this.addTab(this)
},
destroyed() {
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
this.removeTab(this)
},
}
</script>
<style></style>

View File

@ -1,155 +0,0 @@
<template>
<div>
<div
:class="[
{ 'col-md-4': vertical && !tabNavWrapperClasses },
{ 'col-12': centered && !tabNavWrapperClasses },
tabNavWrapperClasses,
]"
>
<b-nav
class="nav-pills"
role="tablist"
:class="[
`nav-pills-${type}`,
{ 'flex-column': vertical },
{ 'justify-content-center': centered },
tabNavClasses,
]"
>
<b-nav-item
v-for="tab in tabs"
class="active"
data-toggle="tab"
role="tablist"
:active="tab.active"
:key="tab.id"
:href="`#${tab.id}`"
@click.prevent="activateTab(tab)"
:aria-expanded="tab.active"
>
<tab-item-content :tab="tab"></tab-item-content>
</b-nav-item>
</b-nav>
</div>
<div
class="tab-content"
:class="[
{ 'tab-space': !vertical },
{ 'col-md-8': vertical && !tabContentClasses },
tabContentClasses,
]"
>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'tabs',
components: {
TabItemContent: {
props: ['tab'],
render(h) {
return h('div', [this.tab.$slots.title || this.tab.title])
},
},
},
provide() {
return {
addTab: this.addTab,
removeTab: this.removeTab,
}
},
props: {
type: {
type: String,
default: 'primary',
validator: (value) => {
const acceptedValues = ['primary', 'info', 'success', 'warning', 'danger']
return acceptedValues.indexOf(value) !== -1
},
},
activeTab: {
type: String,
default: '',
description: 'Active tab name',
},
tabNavWrapperClasses: {
type: [String, Object],
default: '',
description: 'ul wrapper css classes',
},
tabNavClasses: {
type: [String, Object],
default: '',
description: 'ul css classes',
},
tabContentClasses: {
type: [String, Object],
default: '',
description: 'tab content css classes',
},
vertical: Boolean,
centered: Boolean,
value: String,
},
data() {
return {
tabs: [],
}
},
methods: {
findAndActivateTab(title) {
const tabToActivate = this.tabs.find((t) => t.title === title)
if (tabToActivate) {
this.activateTab(tabToActivate)
}
},
activateTab(tab) {
if (this.handleClick) {
this.handleClick(tab)
}
this.deactivateTabs()
tab.active = true
},
deactivateTabs() {
this.tabs.forEach((tab) => {
tab.active = false
})
},
addTab(tab) {
const index = this.$slots.default.indexOf(tab.$vnode)
if (!this.activeTab && index === 0) {
tab.active = true
}
if (this.activeTab === tab.name) {
tab.active = true
}
this.tabs.splice(index, 0, tab)
},
removeTab(tab) {
const tabs = this.tabs
const index = tabs.indexOf(tab)
if (index > -1) {
tabs.splice(index, 1)
}
},
},
mounted() {
this.$nextTick(() => {
if (this.value) {
this.findAndActivateTab(this.value)
}
})
},
watch: {
value(newVal) {
this.findAndActivateTab(newVal)
},
},
}
</script>
<style scoped></style>

View File

@ -1,50 +1,8 @@
import BaseCheckbox from './Inputs/BaseCheckbox.vue'
import BaseAlert from './BaseAlert.vue'
import BaseRadio from './Inputs/BaseRadio.vue'
import BaseInput from './Inputs/BaseInput.vue'
import Badge from './Badge'
import BaseProgress from './BaseProgress.vue'
import BaseButton from './BaseButton.vue'
import BaseDropdown from './BaseDropdown.vue'
import BaseTable from './BaseTable.vue'
import Card from './Cards/Card.vue'
import StatsCard from './Cards/StatsCard.vue'
import BaseNav from './Navbar/BaseNav'
import NavbarToggleButton from './Navbar/NavbarToggleButton'
import TabPane from './Tabs/Tab.vue'
import Tabs from './Tabs/Tabs.vue'
import Collapse from './Collapse/Collapse.vue'
import CollapseItem from './Collapse/CollapseItem.vue'
import Modal from './Modal.vue'
import BaseSlider from './BaseSlider.vue'
import BasePagination from './BasePagination.vue'
import SidebarPlugin from './SidebarPlugin'
export {
BaseCheckbox,
Badge,
BaseAlert,
BaseProgress,
BasePagination,
BaseRadio,
BaseInput,
Card,
StatsCard,
BaseTable,
BaseDropdown,
SidebarPlugin,
BaseNav,
NavbarToggleButton,
TabPane,
Tabs,
Modal,
BaseSlider,
BaseButton,
Collapse,
CollapseItem,
}
export { SidebarPlugin, NavbarToggleButton, Collapse, CollapseItem }

View File

@ -34,7 +34,7 @@
"email":"E-Mail",
"email_repeat":"eMail wiederholen",
"password":"Passwort",
"password_repeat":"Passwort wiederholen",
"passwordRepeat":"Passwort wiederholen",
"password_old":"altes Passwort",
"password_new":"neues Passwort",
"password_new_repeat":"neues Passwort wiederholen",
@ -106,7 +106,6 @@
},
"profil": {
"activity": {
"chart":"Gemeinschaftsstunden Chart",
"new":"Neue Gemeinschaftsstunden eintragen",
"list":"Meine Gemeinschaftsstunden Liste"
},

View File

@ -34,7 +34,7 @@
"email":"Email",
"email_repeat":"Repeat Email",
"password":"Password",
"password_repeat":"Repeat password",
"passwordRepeat":"Repeat password",
"password_old":"Old password",
"password_new":"New password",
"password_new_repeat":"Repeat new password",
@ -107,7 +107,6 @@
"profil": {
"transactions":"transactions",
"activity": {
"chart":"Community Hours Chart",
"new":"Register new community hours",
"list":"My Community Hours List"
},

View File

@ -1,14 +1,7 @@
// Polyfills for js features used in the Dashboard but not supported in some browsers (mainly IE)
import '@/polyfills'
// Notifications plugin. Used on Notifications page
import Notifications from '@/components/NotificationPlugin'
// Validation plugin used to validate forms
import { configure, extend } from 'vee-validate'
// A plugin file where you could register global components used across the app
import GlobalComponents from './globalComponents'
// A plugin file where you could register global directives
import GlobalDirectives from './globalDirectives'
// Sidebar on the right. Used as a local plugin in DashboardLayout.vue
import SideBar from '@/components/SidebarPlugin'
import PortalVue from 'portal-vue'
@ -27,16 +20,12 @@ import { messages } from 'vee-validate/dist/locale/en.json'
import VueQrcodeReader from 'vue-qrcode-reader'
import VueQrcode from 'vue-qrcode'
import VueFlatPickr from 'vue-flatpickr-component'
import VueGoodTablePlugin from 'vue-good-table'
// import the styles
import 'vue-good-table/dist/vue-good-table.css'
import FlatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import VueMoment from 'vue-moment'
import Loading from 'vue-loading-overlay'
// import the styles
import 'vue-loading-overlay/dist/vue-loading.css'
Object.keys(rules).forEach((rule) => {
@ -45,21 +34,20 @@ Object.keys(rules).forEach((rule) => {
message: messages[rule], // assign message
})
})
export default {
install(Vue) {
Vue.use(GlobalComponents)
Vue.use(GlobalDirectives)
Vue.use(SideBar)
Vue.use(Notifications)
Vue.use(PortalVue)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
Vue.use(VueBootstrapToasts)
Vue.use(VueGoodTablePlugin)
Vue.use(VueMoment)
Vue.use(VueQrcodeReader)
Vue.use(VueQrcode)
Vue.use(VueFlatPickr)
Vue.use(FlatPickr)
Vue.use(Loading)
configure({
classes: {

View File

@ -1,38 +1,7 @@
import BaseInput from '@/components/Inputs/BaseInput.vue'
import BaseDropdown from '@/components/BaseDropdown.vue'
import Card from '@/components/Cards/Card.vue'
import Modal from '@/components/Modal.vue'
import StatsCard from '@/components/Cards/StatsCard.vue'
import BaseButton from '@/components/BaseButton.vue'
import Badge from '@/components/Badge.vue'
import BaseCheckbox from '@/components/Inputs/BaseCheckbox.vue'
import BaseRadio from '@/components/Inputs/BaseRadio'
import BaseProgress from '@/components/BaseProgress'
import BasePagination from '@/components/BasePagination'
import BaseAlert from '@/components/BaseAlert'
import BaseNav from '@/components/Navbar/BaseNav'
import BaseHeader from '@/components/BaseHeader'
import { ValidationProvider, ValidationObserver } from 'vee-validate'
/**
* You can register global components here and use them as a plugin in your main Vue instance
*/
const GlobalComponents = {
install(Vue) {
Vue.component(Badge.name, Badge)
Vue.component(BaseAlert.name, BaseAlert)
Vue.component(BaseButton.name, BaseButton)
Vue.component(BaseCheckbox.name, BaseCheckbox)
Vue.component(BaseHeader.name, BaseHeader)
Vue.component(BaseInput.name, BaseInput)
Vue.component(BaseDropdown.name, BaseDropdown)
Vue.component(BaseNav.name, BaseNav)
Vue.component(BasePagination.name, BasePagination)
Vue.component(BaseProgress.name, BaseProgress)
Vue.component(BaseRadio.name, BaseRadio)
Vue.component(Card.name, Card)
Vue.component(Modal.name, Modal)
Vue.component(StatsCard.name, StatsCard)
Vue.component('validation-provider', ValidationProvider)
Vue.component('validation-observer', ValidationObserver)
},

View File

@ -58,7 +58,6 @@ export const store = new Vuex.Store({
sessionId: null,
email: '',
language: null,
modals: false,
firstName: '',
lastName: '',
username: '',

View File

@ -24,7 +24,25 @@
</template>
</side-bar>
<div class="main-content">
<dashboard-navbar :type="$route.meta.navbarType"></dashboard-navbar>
<div class="d-none d-md-block">
<b-navbar>
<b-navbar-nav class="ml-auto">
<b-nav-item>
<b-media no-body class="align-items-center">
<span class="pb-2 text-lg font-weight-bold">
{{ $store.state.email }}
</span>
<b-media-body class="ml-2">
<span class="avatar">
<vue-qrcode :value="$store.state.email" type="image/png"></vue-qrcode>
</span>
</b-media-body>
</b-media>
</b-nav-item>
</b-navbar-nav>
</b-navbar>
</div>
<div @click="$sidebar.displaySidebar(false)">
<fade-transition :duration="200" origin="center top" mode="out-in">
<!-- your content here -->
@ -48,11 +66,11 @@ import PerfectScrollbar from 'perfect-scrollbar'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
import loginAPI from '../../apis/loginAPI'
import DashboardNavbar from './DashboardNavbar.vue'
import ContentFooter from './ContentFooter.vue'
// import DashboardContent from './Content.vue';
import { FadeTransition } from 'vue2-transitions'
import communityAPI from '../../apis/communityAPI'
import VueQrcode from 'vue-qrcode'
function hasElement(className) {
return document.getElementsByClassName(className).length > 0
@ -72,9 +90,8 @@ function initScrollbar(className) {
export default {
components: {
DashboardNavbar,
ContentFooter,
// DashboardContent,
VueQrcode,
FadeTransition,
},
data() {
@ -132,4 +149,9 @@ export default {
},
}
</script>
<style lang="scss"></style>
<style lang="scss">
.xxx {
position: relative;
right: 0px;
}
</style>

View File

@ -1,48 +0,0 @@
<template>
<base-nav
container-classes="container-fluid"
class="navbar-expand"
:class="{ 'navbar-dark': type === 'default' }"
>
<!-- Navbar links -->
<b-navbar-nav class="align-items-center ml-md-auto">
<!-- This item dont have <b-nav-item> because item have data-action/data-target on tag <a>, wich we cant add -->
<li class="nav-item d-sm-none"></li>
</b-navbar-nav>
<b-navbar-nav class="align-items-center ml-auto ml-md-0">
<div class="pr-1" slot="title-container ">
<b-media no-body class="align-items-center">
<span class="pb-2 text-lg font-weight-bold">
{{ $store.state.email }}
</span>
<b-media-body class="ml-2">
<span class="avatar">
<vue-qrcode :value="$store.state.email" type="image/png"></vue-qrcode>
</span>
</b-media-body>
</b-media>
</div>
</b-navbar-nav>
</base-nav>
</template>
<script>
import { BaseNav } from '@/components'
import VueQrcode from 'vue-qrcode'
export default {
components: {
BaseNav,
VueQrcode,
},
props: {
type: {
type: String,
},
},
}
</script>
<style>
.pointer {
cursor: pointer;
}
</style>

View File

@ -4,32 +4,32 @@
<b-tab :title="names.thisMonth" active>
<b-row>
<b-col lg="3">
<base-input :label="$t('communitys.form.hours')">
<b-input :label="$t('communitys.form.hours')">
<b-form-input
type="number"
size="lg"
placeholder="23"
style="font-size: xx-large; padding-left: 5px"
/>
</base-input>
<base-input :label="$t('communitys.form.date_period')">
</b-input>
<b-input :label="$t('communitys.form.date_period')">
<flat-pickr
class="form-control"
v-model="date"
:config="config"
style="font-size: 0.5em; padding-left: 5px"
></flat-pickr>
</base-input>
</b-input>
</b-col>
<b-col lg="9">
<base-input :label="$t('communitys.form.hours_report')">
<b-input :label="$t('communitys.form.hours_report')">
<textarea
class="form-control"
rows="5"
@focus="textFocus"
style="font-size: x-large; padding-left: 20px"
></textarea>
</base-input>
</b-input>
</b-col>
</b-row>
<b-row>
@ -57,12 +57,8 @@
</template>
<script>
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
export default {
name: 'GDDAddWork2',
components: { flatPickr },
data() {
return {
date: null,
@ -147,32 +143,32 @@ export default {
newWorkForm() {
this.formular = `
<b-col lg="3">
<base-input label="Stunden">
<b-input label="Stunden">
<b-form-input
type="number"
size="lg"
placeholder="0"
style="font-size: xx-large; padding-left: 20px"
/>
</base-input>
<base-input label="Datum / Zeitraum">
</b-input>
<b-input label="Datum / Zeitraum">
<flat-pickr
class="form-control"
v-model="date"
:config="config"
style="font-size: xx-large; padding-left: 20px"
></flat-pickr>
</base-input>
</b-input>
</b-col>
<b-col lg="9">
<base-input label="Arbeitsreport">
<b-input label="Arbeitsreport">
<textarea
class="form-control"
rows="5"
@focus="textFocus"
style="font-size: x-large; padding-left: 20px"
></textarea>
</base-input>
</b-input>
</b-col>
`

View File

@ -17,19 +17,31 @@
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="p-4">
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<base-input
alternative
class="mb-3"
prepend-icon="ni ni-email-83"
:placeholder="$t('form.email')"
<validation-provider
name="Email"
:rules="{ required: true, email: true }"
v-model="form.email"
></base-input>
v-slot="validationContext"
>
<b-form-group class="mb-3" label="Email" label-for="input-reset-pwd">
<b-form-input
id="input-reset-pwd"
name="input-reset-pwd"
v-model="form.email"
placeholder="Email"
:state="getValidationState(validationContext)"
aria-describedby="reset-pwd--live-feedback"
></b-form-input>
<b-form-invalid-feedback id="reset-pwd--live-feedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<div class="text-center">
<b-button type="submit" outline variant="secondary" class="mt-4">
<b-button type="submit" variant="primary">
{{ $t('site.password.reset_now') }}
</b-button>
</div>
@ -60,6 +72,9 @@ export default {
},
created() {},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
async onSubmit() {
await loginAPI.sendEmail(this.form.email)
// always give success to avoid email spying

View File

@ -22,34 +22,63 @@
<div class="text-center text-muted mb-4">
<small>{{ $t('login') }}</small>
</div>
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<base-input
alternative
class="mb-3"
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
<validation-provider
name="Email"
:rules="{ required: true, email: true }"
prepend-icon="ni ni-email-83"
placeholder="Email"
v-model="model.email"
></base-input>
v-slot="validationContext"
>
<b-form-group class="mb-3" label="Email" label-for="login-email">
<b-form-input
id="login-email"
name="example-input-1"
v-model="form.email"
placeholder="Email"
:state="getValidationState(validationContext)"
aria-describedby="login-email-live-feedback"
></b-form-input>
<b-input-group>
<b-form-input
class="mb-0"
v-model="model.password"
name="Password"
:type="passwordVisible ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
:placeholder="$t('form.password')"
></b-form-input>
<b-form-invalid-feedback id="login-email-live-feedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
<validation-provider
:name="$t('form.password')"
:rules="{ required: true }"
v-slot="validationContext"
>
<b-form-group
class="mb-5"
id="example-input-group-1"
:label="$t('form.password')"
label-for="example-input-1"
>
<b-input-group>
<b-form-input
id="input-pwd"
name="input-pwd"
v-model="form.password"
:placeholder="$t('form.password')"
:type="passwordVisible ? 'text' : 'password'"
:state="getValidationState(validationContext)"
aria-describedby="input-2-live-feedback"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback id="input-2-live-feedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<b-alert v-show="loginfail" show dismissible variant="warning">
<span class="alert-text bv-example-row">
@ -62,12 +91,8 @@
</b-row>
</span>
</b-alert>
<!-- <b-form-checkbox v-model="model.rememberMe">{{ $t('site.login.remember')}}</b-form-checkbox> -->
<div class="text-center" ref="submitButton">
<base-button type="secondary" native-type="submit" class="my-4">
{{ $t('site.login.signin') }}
</base-button>
<div class="text-center">
<b-button type="submit" variant="primary">{{ $t('login') }}</b-button>
</div>
</b-form>
</validation-observer>
@ -98,7 +123,7 @@ export default {
name: 'login',
data() {
return {
model: {
form: {
email: '',
password: '',
// rememberMe: false
@ -109,6 +134,10 @@ export default {
}
},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible
},
@ -118,7 +147,7 @@ export default {
const loader = this.$loading.show({
container: this.$refs.submitButton,
})
const result = await loginAPI.login(this.model.email, this.model.password)
const result = await loginAPI.login(this.form.email, this.form.password)
if (result.success) {
this.$store.dispatch('login', {
sessionId: result.result.data.session_id,

View File

@ -53,16 +53,27 @@ describe('Register', () => {
expect(wrapper.find('form').exists()).toBeTruthy()
})
it('has 3 text input fields', () => {
expect(wrapper.findAll('input[type="text"]').length).toBe(3)
it('has firstname input fields', () => {
expect(wrapper.find('#registerFirstname').exists()).toBeTruthy()
})
it('has lastname input fields', () => {
expect(wrapper.find('#registerLastname').exists()).toBeTruthy()
})
it('has 2 password input fields', () => {
expect(wrapper.findAll('input[type="password"]').length).toBe(2)
it('has email input fields', () => {
expect(wrapper.find('#registerEmail').exists()).toBeTruthy()
})
it('has password input fields', () => {
expect(wrapper.find('#registerPassword').exists()).toBeTruthy()
})
it('has password repeat input fields', () => {
expect(wrapper.find('#registerPasswordRepeat').exists()).toBeTruthy()
})
it('has 1 checkbox input fields', () => {
expect(wrapper.findAll('input[type="checkbox"]').length).toBe(1)
expect(wrapper.find('#registerCheckbox').exists()).toBeTruthy()
})
it('has no submit button when not completely filled', () => {
@ -70,9 +81,9 @@ describe('Register', () => {
})
it('shows a warning when no valid Email is entered', async () => {
wrapper.findAll('input[type="text"]').at(2).setValue('no_valid@Email')
wrapper.find('#registerEmail').setValue('no_valid@Email')
await flushPromises()
await expect(wrapper.find('.invalid-feedback').text()).toEqual(
await expect(wrapper.find('#registerEmailLiveFeedback').text()).toEqual(
'The Email field must be a valid email',
)
})

View File

@ -23,65 +23,137 @@
<div class="text-center text-muted mb-4">
<small>{{ $t('signup') }}</small>
</div>
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<base-input
:label="$t('form.firstname')"
alternative
class="mb-3"
name="firstname"
:rules="{ required: true, min: 3 }"
v-model="model.firstname"
></base-input>
<base-input
:label="$t('form.lastname')"
alternative
class="mb-3"
name="lastname"
:rules="{ required: true, min: 2 }"
v-model="model.lastname"
></base-input>
<base-input
:label="$t('form.email')"
alternative
class="mb-3"
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<validation-provider
:name="$t('form.firstname')"
:rules="{ required: true, min: 3 }"
v-slot="validationContext"
>
<b-form-group
class="mb-3"
:label="$t('form.firstname')"
label-for="registerFirstname"
>
<b-form-input
id="registerFirstname"
:name="$t('form.firstname')"
v-model="form.firstname"
:placeholder="$t('form.firstname')"
:state="getValidationState(validationContext)"
aria-describedby="registerFirstnameLiveFeedback"
></b-form-input>
<b-form-invalid-feedback id="registerFirstnameLiveFeedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<validation-provider
:name="$t('form.lastname')"
:rules="{ required: true, min: 2 }"
v-slot="validationContext"
>
<b-form-group
class="mb-3"
:label="$t('form.lastname')"
label-for="registerLastname"
>
<b-form-input
id="registerLastname"
:name="$t('form.lastname')"
v-model="form.lastname"
:placeholder="$t('form.lastname')"
:state="getValidationState(validationContext)"
aria-describedby="registerLastnameLiveFeedback"
></b-form-input>
<b-form-invalid-feedback id="registerLastnameLiveFeedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<validation-provider
name="Email"
:rules="{ required: true, email: true }"
v-model="model.email"
></base-input>
v-slot="validationContext"
>
<b-form-group class="mb-3" label="Email" label-for="registerEmail">
<b-form-input
id="registerEmail"
name="Email"
v-model="form.email"
placeholder="Email"
:state="getValidationState(validationContext)"
aria-describedby="registerEmailLiveFeedback"
></b-form-input>
<b-form-invalid-feedback id="registerEmailLiveFeedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<hr />
<b-form-group :label="$t('form.password')">
<validation-provider
:name="$t('form.password')"
:rules="{ required: true }"
v-slot="validationContext"
>
<b-form-group
class="mb-5"
:label="$t('form.password')"
label-for="registerPassword"
>
<b-input-group>
<b-form-input
id="registerPassword"
:name="$t('form.password')"
v-model="form.password"
:placeholder="$t('form.password')"
:type="passwordVisible ? 'text' : 'password'"
:state="getValidationState(validationContext)"
aria-describedby="registerPasswordLiveFeedback"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback id="registerPasswordLiveFeedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<b-form-group
class="mb-5"
:label="$t('form.passwordRepeat')"
label-for="registerPasswordRepeat"
>
<b-input-group>
<b-form-input
class="mb-0"
v-model="password"
name="password"
:class="{ valid: passwordValidation.valid }"
:type="passwordVisible ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
:placeholder="$t('form.password')"
id="registerPasswordRepeat"
:name="$t('form.passwordRepeat')"
v-model.lazy="form.passwordRepeat"
:placeholder="$t('form.passwordRepeat')"
:type="passwordVisibleRepeat ? 'text' : 'password'"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
<b-button variant="outline-primary" @click="togglePasswordRepeatVisibility">
<b-icon :icon="passwordVisibleRepeat ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<base-input
:label="$t('form.password_repeat')"
type="password"
name="password-repeat"
:placeholder="$t('form.password_repeat')"
prepend-icon="ni ni-lock-circle-open"
v-model.lazy="checkPassword"
:class="{ valid: passwordValidation.valid }"
/>
<transition name="hint" appear>
<div v-if="passwordValidation.errors.length > 0 && !submitted" class="hints">
<ul>
@ -99,14 +171,13 @@
</transition>
<b-row class="my-4">
<b-col cols="12">
<base-input
:rules="{ required: { allowFalse: false } }"
name="Privacy Policy"
<b-form-checkbox
id="registerCheckbox"
v-model="form.agree"
:name="$t('site.signup.agree')"
>
<b-form-checkbox v-model="model.agree">
<span class="text-muted" v-html="$t('site.signup.agree')"></span>
</b-form-checkbox>
</base-input>
<span class="text-muted" v-html="$t('site.signup.agree')"></span>
</b-form-checkbox>
</b-col>
</b-row>
<b-alert
@ -131,12 +202,13 @@
passwordValidation.valid &&
namesFilled &&
emailFilled &&
model.agree
form.agree
"
>
<b-button type="submit" variant="secondary" class="mt-4">
{{ $t('signup') }}
</b-button>
<div class="text-center">
<b-button class="ml-2" @click="resetForm()">{{ $t('form.reset') }}</b-button>
<b-button type="submit" variant="primary">{{ $t('signup') }}</b-button>
</div>
</div>
</b-form>
</validation-observer>
@ -157,41 +229,61 @@ export default {
name: 'register',
data() {
return {
model: {
form: {
firstname: '',
lastname: '',
email: '',
agree: false,
password: '',
passwordRepeat: '',
},
password: '',
checkPassword: '',
passwordVisible: false,
passwordVisibleRepeat: false,
submitted: false,
showError: false,
messageError: '',
}
},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
resetForm() {
this.form = {
firstname: '',
lastname: '',
email: '',
password: '',
passwordRepeat: '',
}
this.$nextTick(() => {
this.$refs.observer.reset()
})
},
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible
},
togglePasswordRepeatVisibility() {
this.passwordVisibleRepeat = !this.passwordVisibleRepeat
},
async onSubmit() {
const result = await loginAPI.create(
this.model.email,
this.model.firstname,
this.model.lastname,
this.password,
this.form.email,
this.form.firstname,
this.form.lastname,
this.form.password,
)
if (result.success) {
this.$store.dispatch('login', {
sessionId: result.result.data.session_id,
email: this.model.email,
email: this.form.email,
})
this.model.email = ''
this.model.firstname = ''
this.model.lastname = ''
this.form.email = ''
this.form.firstname = ''
this.form.lastname = ''
this.password = ''
this.passwordVisibleRepeat = ''
this.$router.push('/thx/register')
} else {
this.showError = true
@ -201,29 +293,29 @@ export default {
closeAlert() {
this.showError = false
this.messageError = ''
this.model.email = ''
this.model.firstname = ''
this.model.lastname = ''
this.password = ''
this.form.email = ''
this.form.firstname = ''
this.form.lastname = ''
this.form.password = ''
},
},
computed: {
samePasswords() {
return this.password === this.checkPassword
return this.form.password === this.form.passwordRepeat
},
passwordsFilled() {
return this.password !== '' && this.checkPassword !== ''
return this.form.password !== '' && this.form.passwordRepeat !== ''
},
namesFilled() {
return (
this.model.firstname !== '' &&
this.model.firstname.length > 2 &&
this.model.lastname !== '' &&
this.model.lastname.length > 1
this.form.firstname !== '' &&
this.form.firstname.length > 2 &&
this.form.lastname !== '' &&
this.form.lastname.length > 1
)
},
emailFilled() {
return this.model.email !== ''
return this.form.email !== ''
},
rules() {
return [
@ -236,7 +328,7 @@ export default {
passwordValidation() {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.password)) {
if (!condition.regex.test(this.form.password)) {
errors.push(condition.message)
}
}

View File

@ -20,38 +20,63 @@
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="p-4">
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<validation-observer ref="observer" v-slot="{ handleSubmit }">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<b-form-group :label="$t('form.password')">
<validation-provider
:name="$t('form.password')"
:rules="{ required: true }"
v-slot="validationContext"
>
<b-form-group
class="mb-5"
:label="$t('form.password')"
label-for="resetPassword"
>
<b-input-group>
<b-form-input
id="resetPassword"
:name="$t('form.password')"
v-model="form.password"
:placeholder="$t('form.password')"
:type="passwordVisible ? 'text' : 'password'"
:state="getValidationState(validationContext)"
aria-describedby="resetPasswordLiveFeedback"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
<b-form-invalid-feedback id="resetPasswordLiveFeedback">
{{ validationContext.errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
<b-form-group
class="mb-5"
:label="$t('form.passwordRepeat')"
label-for="resetPasswordRepeat"
>
<b-input-group>
<b-form-input
class="mb-0"
v-model="password"
name="password"
:class="{ valid: passwordValidation.valid }"
:type="passwordVisible ? 'text' : 'password'"
prepend-icon="ni ni-lock-circle-open"
:placeholder="$t('form.password')"
id="resetPasswordRepeat"
:name="$t('form.passwordRepeat')"
v-model.lazy="form.passwordRepeat"
:placeholder="$t('form.passwordRepeat')"
:type="passwordVisibleRepeat ? 'text' : 'password'"
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
<b-button variant="outline-primary" @click="togglePasswordRepeatVisibility">
<b-icon :icon="passwordVisibleRepeat ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<base-input
:label="$t('form.password_repeat')"
type="password"
name="password-repeat"
:placeholder="$t('form.password_repeat')"
prepend-icon="ni ni-lock-circle-open"
v-model.lazy="checkPassword"
:class="{ valid: passwordValidation.valid }"
/>
<transition name="hint" appear>
<div v-if="passwordValidation.errors.length > 0 && !submitted" class="hints">
<ul>
@ -90,8 +115,11 @@ export default {
name: 'reset',
data() {
return {
form: {
password: '',
passwordRepeat: '',
},
password: '',
checkPassword: '',
passwordVisible: false,
submitted: false,
authenticated: false,
@ -100,13 +128,16 @@ export default {
}
},
methods: {
getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null
},
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible
},
async onSubmit() {
const result = await loginAPI.changePassword(this.sessionId, this.email, this.password)
const result = await loginAPI.changePassword(this.sessionId, this.email, this.form.password)
if (result.success) {
this.password = ''
this.form.password = ''
/*
this.$store.dispatch('login', {
sessionId: result.result.data.session_id,
@ -132,10 +163,10 @@ export default {
},
computed: {
samePasswords() {
return this.password === this.checkPassword
return this.form.password === this.form.passwordRepeat
},
passwordsFilled() {
return this.password !== '' && this.checkPassword !== ''
return this.form.password !== '' && this.form.passwordRepeat !== ''
},
rules() {
return [
@ -148,7 +179,7 @@ export default {
passwordValidation() {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.password)) {
if (!condition.regex.test(this.form.password)) {
errors.push(condition.message)
}
}

View File

@ -5,16 +5,10 @@ import { ValidationProvider, ValidationObserver, extend } from 'vee-validate'
import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json'
import BaseInput from '@/components/Inputs/BaseInput.vue'
import BaseButton from '@/components/BaseButton.vue'
import RegeneratorRuntime from 'regenerator-runtime'
import Notifications from '@/components/NotificationPlugin'
import SideBar from '@/components/SidebarPlugin'
import VueRouter from 'vue-router'
import BaseDropdown from '@/components/BaseDropdown.vue'
import VueQrcode from 'vue-qrcode'
import BaseHeader from '@/components/BaseHeader'
import StatsCard from '@/components/Cards/StatsCard.vue'
import VueMoment from 'vue-moment'
@ -34,18 +28,11 @@ global.localVue.use(BootstrapVue)
global.localVue.use(Vuex)
global.localVue.use(IconsPlugin)
global.localVue.use(RegeneratorRuntime)
global.localVue.use(Notifications)
global.localVue.use(SideBar)
global.localVue.use(VueRouter)
global.localVue.use(VueQrcode)
global.localVue.use(VueMoment)
global.localVue.component(BaseInput.name, BaseInput)
global.localVue.component('validation-provider', ValidationProvider)
global.localVue.component('validation-observer', ValidationObserver)
global.localVue.component(BaseButton.name, BaseButton)
global.localVue.component(BaseDropdown.name, BaseDropdown)
global.localVue.component(BaseHeader.name, BaseHeader)
global.localVue.component(StatsCard.name, StatsCard)
global.localVue.directive('click-outside', clickOutside)
global.localVue.directive('focus', focus)