@@ -0,0 +1,3 @@ | |||
> 1% | |||
last 2 versions | |||
not dead |
@@ -0,0 +1,5 @@ | |||
[*.{js,jsx,ts,tsx,vue}] | |||
indent_style = space | |||
indent_size = 2 | |||
trim_trailing_whitespace = true | |||
insert_final_newline = true |
@@ -0,0 +1,20 @@ | |||
module.exports = { | |||
root: true, | |||
env: { | |||
node: true | |||
}, | |||
extends: [ | |||
'plugin:vue/essential', | |||
'@vue/standard', | |||
'@vue/typescript/recommended' | |||
], | |||
parserOptions: { | |||
ecmaVersion: 2020 | |||
}, | |||
rules: { | |||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', | |||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', | |||
"@typescript-eslint/ban-ts-comment": "off", | |||
"@typescript-eslint/ban-ts-ignore": "off" | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
.DS_Store | |||
node_modules | |||
/dist | |||
# local env files | |||
.env.local | |||
.env.*.local | |||
# Log files | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
pnpm-debug.log* | |||
# Editor directories and files | |||
.idea | |||
.vscode | |||
*.suo | |||
*.ntvs* | |||
*.njsproj | |||
*.sln | |||
*.sw? |
@@ -0,0 +1,24 @@ | |||
# client | |||
## Project setup | |||
``` | |||
npm install | |||
``` | |||
### Compiles and hot-reloads for development | |||
``` | |||
npm run serve | |||
``` | |||
### Compiles and minifies for production | |||
``` | |||
npm run build | |||
``` | |||
### Lints and fixes files | |||
``` | |||
npm run lint | |||
``` | |||
### Customize configuration | |||
See [Configuration Reference](https://cli.vuejs.org/config/). |
@@ -0,0 +1,5 @@ | |||
module.exports = { | |||
presets: [ | |||
'@vue/cli-plugin-babel/preset' | |||
] | |||
} |
@@ -0,0 +1,55 @@ | |||
{ | |||
"name": "client", | |||
"version": "0.1.0", | |||
"private": true, | |||
"scripts": { | |||
"serve": "vue-cli-service serve", | |||
"build": "vue-cli-service build", | |||
"lint": "vue-cli-service lint" | |||
}, | |||
"dependencies": { | |||
"@fortawesome/fontawesome-pro": "^5.15.3", | |||
"@mdi/font": "^5.9.55", | |||
"@vue/composition-api": "1.0.0-rc.8", | |||
"apollo-cache-inmemory": "^1.6.6", | |||
"apollo-client": "^2.6.10", | |||
"core-js": "^3.6.5", | |||
"js-cookie": "^2.2.1", | |||
"lodash": "^4.17.21", | |||
"subscriptions-transport-ws": "^0.9.18", | |||
"uuid": "^3.4.0", | |||
"vue": "^2.6.11", | |||
"vue-apollo": "^3.0.7", | |||
"vue-class-component": "^7.2.3", | |||
"vue-cookies": "^1.7.4", | |||
"vue-property-decorator": "^9.1.2", | |||
"vue-router": "^3.2.0", | |||
"vuetify": "^2.4.0", | |||
"vuex": "^3.4.0" | |||
}, | |||
"devDependencies": { | |||
"@typescript-eslint/eslint-plugin": "^4.18.0", | |||
"@typescript-eslint/parser": "^4.18.0", | |||
"@vue/cli-plugin-babel": "~4.5.0", | |||
"@vue/cli-plugin-eslint": "~4.5.0", | |||
"@vue/cli-plugin-router": "~4.5.0", | |||
"@vue/cli-plugin-typescript": "~4.5.0", | |||
"@vue/cli-plugin-vuex": "~4.5.0", | |||
"@vue/cli-service": "~4.5.0", | |||
"@vue/eslint-config-standard": "^5.1.2", | |||
"@vue/eslint-config-typescript": "^7.0.0", | |||
"eslint": "^6.7.2", | |||
"eslint-plugin-import": "^2.20.2", | |||
"eslint-plugin-node": "^11.1.0", | |||
"eslint-plugin-promise": "^4.2.1", | |||
"eslint-plugin-standard": "^4.0.0", | |||
"eslint-plugin-vue": "^6.2.2", | |||
"node-sass": "^4.12.0", | |||
"sass": "^1.32.0", | |||
"sass-loader": "^10.0.0", | |||
"typescript": "~4.1.5", | |||
"vue-cli-plugin-vuetify": "~2.4.0", | |||
"vue-template-compiler": "^2.6.11", | |||
"vuetify-loader": "^1.7.0" | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
<!DOCTYPE html> | |||
<html lang="de"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> | |||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> | |||
<title><%= htmlWebpackPlugin.options.title %></title> | |||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> | |||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous"> | |||
</head> | |||
<body> | |||
<noscript> | |||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> | |||
</noscript> | |||
<div id="app"></div> | |||
<!-- built files will be auto injected --> | |||
</body> | |||
</html> |
@@ -0,0 +1,130 @@ | |||
<template> | |||
<div> | |||
<router-view /> | |||
<div | |||
v-if="!connected" | |||
id="loader" | |||
> | |||
<div> | |||
<v-progress-circular | |||
indeterminate | |||
:size="64" | |||
:width="8" | |||
color="rgb(255, 4, 29)" | |||
/> | |||
<p>Verbinde zu Server...</p> | |||
</div> | |||
</div> | |||
<confirm ref="confirm" /> | |||
<v-snackbar | |||
v-model="error_visible" | |||
class="error_snack" | |||
:timeout="5000" | |||
top | |||
> | |||
{{ error_text }} | |||
<template #action="{ attrs }"> | |||
<v-btn | |||
dark | |||
text | |||
v-bind="attrs" | |||
@click="error_visible = false" | |||
> | |||
Schließen | |||
</v-btn> | |||
</template> | |||
</v-snackbar> | |||
</div> | |||
</template> | |||
<script lang="ts"> | |||
import Vue from 'vue' | |||
import { useGraphQL } from '@/plugins/graphql' | |||
import { mapState } from 'vuex' | |||
export default Vue.extend({ | |||
name: 'App', | |||
setup (props, context) { | |||
return { | |||
...useGraphQL(context) | |||
} | |||
}, | |||
computed: { | |||
...mapState(['profile']), | |||
error_text () { | |||
return this.$store.state.snackbar.text | |||
}, | |||
error_visible: { | |||
get () { | |||
return this.$store.state.snackbar.visible | |||
}, | |||
set (v) { | |||
if (!v) this.$store.commit('CLOSE_SNACKBAR') | |||
} | |||
} | |||
}, | |||
mounted () { | |||
this.login('a.kimmig@dhg-rw.de', 'Passwort') | |||
} | |||
}) | |||
</script> | |||
<style scoped> | |||
#loader { | |||
position: fixed; | |||
top: 0; | |||
bottom: 0; | |||
left: 0; | |||
right: 0; | |||
width: 100vw; | |||
height: 100vh; | |||
z-index: 9999; | |||
background: rgba(255, 255, 255, 0.8); | |||
text-align: center; | |||
} | |||
#loader > div { | |||
height: 100vh; | |||
width: 100vw; | |||
display: table-cell; | |||
vertical-align: middle; | |||
text-align: center; | |||
} | |||
#loader > div > p { | |||
font-family: "Roboto", sans-serif; | |||
margin-top: 16px; | |||
} | |||
</style> | |||
<style> | |||
.changed { | |||
color: rgb(255, 4, 29); | |||
} | |||
.changefade { | |||
color: inherit; | |||
transition: color 0.8s; | |||
} | |||
.trhover:hover { | |||
cursor: pointer; | |||
} | |||
.center_switch > div > div > div { | |||
margin: auto !important; | |||
} | |||
.error_snack .v-snack__wrapper { | |||
font-family: "Roboto", sans-serif; | |||
background-color: red !important; | |||
} | |||
.row-clickable tbody tr:hover { | |||
cursor: pointer; | |||
} | |||
</style> |
@@ -0,0 +1 @@ | |||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg> |
@@ -0,0 +1,107 @@ | |||
<template> | |||
<v-dialog | |||
v-model="dialog" | |||
:max-width="options.width" | |||
:style="{ zIndex: options.zIndex }" | |||
@keydown.esc="cancel" | |||
> | |||
<v-card> | |||
<v-toolbar | |||
dark | |||
:color="options.color" | |||
dense | |||
flat | |||
> | |||
<v-toolbar-title class="white--text"> | |||
{{ title }} | |||
</v-toolbar-title> | |||
</v-toolbar> | |||
<v-card-text | |||
v-show="!!message" | |||
class="pa-4" | |||
> | |||
{{ message }} | |||
</v-card-text> | |||
<v-card-actions class="pt-0"> | |||
<v-spacer /> | |||
<v-btn | |||
color="primary darken-1" | |||
@click.native="agree" | |||
> | |||
Ja | |||
</v-btn> | |||
<v-btn | |||
color="grey" | |||
@click.native="cancel" | |||
> | |||
Abbrechen | |||
</v-btn> | |||
</v-card-actions> | |||
</v-card> | |||
</v-dialog> | |||
</template> | |||
<script> | |||
/** | |||
* Vuetify Confirm Dialog component | |||
* | |||
* Insert component where you want to use it: | |||
* <confirm ref="confirm"></confirm> | |||
* | |||
* Call it: | |||
* this.$refs.confirm.open('Delete', 'Are you sure?', { color: 'red' }).then((confirm) => {}) | |||
* Or use await: | |||
* if (await this.$refs.confirm.open('Delete', 'Are you sure?', { color: 'red' })) { | |||
* // yes | |||
* } | |||
* else { | |||
* // cancel | |||
* } | |||
* | |||
* Alternatively you can place it in main App component and access it globally via this.$root.$confirm | |||
* <template> | |||
* <v-app> | |||
* ... | |||
* <confirm ref="confirm"></confirm> | |||
* </v-app> | |||
* </template> | |||
* | |||
* mounted() { | |||
* this.$root.$confirm = this.$refs.confirm.open | |||
* } | |||
*/ | |||
export default { | |||
data: () => ({ | |||
dialog: false, | |||
resolve: null, | |||
reject: null, | |||
message: null, | |||
title: null, | |||
options: { | |||
color: 'primary', | |||
width: 290, | |||
zIndex: 200 | |||
} | |||
}), | |||
methods: { | |||
open (title, message, options) { | |||
this.dialog = true | |||
this.title = title | |||
this.message = message | |||
this.options = Object.assign(this.options, options) | |||
return new Promise((resolve, reject) => { | |||
this.resolve = resolve | |||
this.reject = reject | |||
}) | |||
}, | |||
agree () { | |||
this.resolve(true) | |||
this.dialog = false | |||
}, | |||
cancel () { | |||
this.resolve(false) | |||
this.dialog = false | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,90 @@ | |||
<template> | |||
<v-menu | |||
v-model="show" | |||
:close-on-content-click="false" | |||
:nudge-right="40" | |||
transition="scale-transition" | |||
offset-y | |||
min-width="290px" | |||
> | |||
<template #activator="{ on }"> | |||
<v-text-field | |||
v-model="dateFormatted" | |||
:label="label" | |||
:clearable="clearable" | |||
prepend-icon="mdi-calendar" | |||
readonly | |||
v-on="on" | |||
/> | |||
</template> | |||
<v-date-picker | |||
v-model="date" | |||
:first-day-of-week="1" | |||
@input="show = false" | |||
/> | |||
</v-menu> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DateSelector', | |||
props: { | |||
value: { | |||
type: String, | |||
default: '' | |||
}, | |||
label: { | |||
type: String, | |||
default: '' | |||
}, | |||
clearable: { | |||
type: Boolean, | |||
required: false, | |||
default: false | |||
} | |||
}, | |||
data: () => ({ | |||
show: false | |||
}), | |||
computed: { | |||
dateFormatted: { | |||
get () { | |||
return this.formatDate(this.value) | |||
}, | |||
set (val) { | |||
this.$emit('input', this.parseDate(val)) | |||
} | |||
}, | |||
date: { | |||
get () { | |||
return this.value | |||
}, | |||
set (val) { | |||
this.$emit('input', val) | |||
} | |||
} | |||
}, | |||
methods: { | |||
formatDate (date) { | |||
if (!date) return null | |||
const [year, month, day] = date.split('-') | |||
return `${day}.${month}.${year}` | |||
}, | |||
parseDate (date) { | |||
if (!date) return null | |||
const [day, month, year] = date.split('/') | |||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,139 @@ | |||
<template> | |||
<v-row> | |||
<v-col cols="6"> | |||
<v-menu | |||
v-model="showdate" | |||
:close-on-content-click="false" | |||
:nudge-right="40" | |||
transition="scale-transition" | |||
offset-y | |||
min-width="290px" | |||
> | |||
<template #activator="{ on }"> | |||
<v-text-field | |||
:value="dateFormatted" | |||
:label="`${label}-Datum`" | |||
:disabled="disabled" | |||
prepend-icon="far fa-calendar-day" | |||
readonly | |||
v-on="on" | |||
/> | |||
</template> | |||
<v-date-picker | |||
v-model="date" | |||
:first-day-of-week="1" | |||
@input="showdate = false" | |||
/> | |||
</v-menu> | |||
</v-col> | |||
<v-col cols="6"> | |||
<v-menu | |||
v-model="showtime" | |||
:close-on-content-click="false" | |||
:nudge-right="40" | |||
transition="scale-transition" | |||
offset-y | |||
min-width="290px" | |||
> | |||
<template #activator="{ on }"> | |||
<v-text-field | |||
:value="timeFormatted" | |||
:label="`${label}-Uhrzeit`" | |||
:disabled="disabled" | |||
prepend-icon="far fa-clock" | |||
readonly | |||
v-on="on" | |||
/> | |||
</template> | |||
<v-time-picker | |||
v-model="time" | |||
format="24hr" | |||
:allowed-minutes="m => m % 5 === 0" | |||
@input="showtime = false" | |||
/> | |||
</v-menu> | |||
</v-col> | |||
</v-row> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DateSelector', | |||
props: { | |||
value: { | |||
type: String, | |||
default: '' | |||
}, | |||
label: { | |||
type: String, | |||
default: '' | |||
}, | |||
disabled: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
data: () => ({ | |||
showdate: false, | |||
showtime: false | |||
}), | |||
computed: { | |||
_date () { | |||
return (this.value?.split(' ') || [])[0] || '' | |||
}, | |||
_time () { | |||
return (this.value?.split(' ') || [])[1] || '' | |||
}, | |||
dateFormatted () { | |||
return this.formatDate(this._date) | |||
}, | |||
timeFormatted () { | |||
return this.formatTime(this._time) | |||
}, | |||
date: { | |||
get () { | |||
return this._date | |||
}, | |||
set (val) { | |||
this.$emit('input', `${val} ${this._time}`) | |||
} | |||
}, | |||
time: { | |||
get () { | |||
return this._time | |||
}, | |||
set (val) { | |||
this.$emit('input', `${this._date} ${val}:00`) | |||
} | |||
} | |||
}, | |||
methods: { | |||
formatDate (date) { | |||
if (!date) return null | |||
const [year, month, day] = date.split('-') | |||
return `${day}.${month}.${year}` | |||
}, | |||
formatTime (time) { | |||
if (!time) return null | |||
const [hour, minute] = time.split(':') | |||
return `${hour}:${minute}` | |||
}, | |||
parseDate (date) { | |||
if (!date) return null | |||
const [day, month, year] = date.split('/') | |||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,20 @@ | |||
<template> | |||
<div> | |||
T2: {{ get('pub_person','b77038e7-9f17-4ef2-951c-958aeba8a5b5') }} | |||
</div> | |||
</template> | |||
<script> | |||
import { getData } from '../plugins/useSDB' | |||
export default { | |||
name: 'Demo', | |||
setup () { | |||
return { ...getData() } | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,9 @@ | |||
<script> | |||
import { VCard } from 'vuetify/lib' | |||
export default { | |||
name: 'Card', | |||
extends: VCard | |||
} | |||
</script> |
@@ -0,0 +1,77 @@ | |||
<template> | |||
<v-edit-dialog | |||
:return-value.sync="wert" | |||
large | |||
persistent | |||
:save-text="savebutton" | |||
:cancel-text="cancelbutton" | |||
@open="open" | |||
@save="save" | |||
@close="close" | |||
> | |||
<div style="min-width:40px;"> | |||
{{ value }} | |||
</div> | |||
<template #input> | |||
<v-text-field | |||
v-model="wert" | |||
:label="label" | |||
autofocus | |||
/> | |||
</template> | |||
</v-edit-dialog> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'EditDialog', | |||
props: { | |||
value: { | |||
type: [String, Number], | |||
required: false, | |||
default: '' | |||
}, | |||
label: { | |||
type: String, | |||
required: false, | |||
default: '' | |||
}, | |||
savebutton: { | |||
type: String, | |||
required: false, | |||
default: 'Übernehmen' | |||
}, | |||
cancelbutton: { | |||
type: String, | |||
required: false, | |||
default: 'Abbrechen' | |||
} | |||
}, | |||
data: () => ({ | |||
wert: null | |||
}), | |||
methods: { | |||
open () { | |||
if (this.value === null || this.value === undefined) { | |||
this.wert = '' | |||
} else { | |||
this.wert = `${this.value}` | |||
} | |||
}, | |||
close () { | |||
this.wert = null | |||
}, | |||
save () { | |||
this.$emit('input', this.wert) | |||
this.wert = null | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,76 @@ | |||
<template> | |||
<v-edit-dialog | |||
:return-value.sync="wert" | |||
large | |||
persistent | |||
:save-text="savebutton" | |||
cancel-text="Abbrechen" | |||
@open="open" | |||
@save="save" | |||
@close="close" | |||
> | |||
<div>{{ value | dateformat(dateformat) }}</div> | |||
<template #input> | |||
<date-selector | |||
v-model="wert" | |||
:label="label" | |||
:clearable="clearable" | |||
/> | |||
</template> | |||
</v-edit-dialog> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'EditDialog', | |||
props: { | |||
value: { | |||
type: String, | |||
required: false, | |||
default: undefined | |||
}, | |||
label: { | |||
type: String, | |||
required: false, | |||
default: '' | |||
}, | |||
savebutton: { | |||
type: String, | |||
required: false, | |||
default: 'Übernehmen' | |||
}, | |||
dateformat: { | |||
type: String, | |||
required: false, | |||
default: null | |||
}, | |||
clearable: { | |||
type: Boolean, | |||
required: false, | |||
default: false | |||
} | |||
}, | |||
data: () => ({ | |||
wert: null | |||
}), | |||
methods: { | |||
open () { | |||
this.wert = this.value | |||
}, | |||
close () { | |||
this.wert = null | |||
}, | |||
save () { | |||
this.$emit('input', this.wert) | |||
this.wert = null | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,82 @@ | |||
<template> | |||
<v-edit-dialog | |||
:return-value.sync="wert" | |||
large | |||
persistent | |||
:save-text="savebutton" | |||
cancel-text="Abbrechen" | |||
@open="open" | |||
@save="save" | |||
@close="close" | |||
> | |||
<div>{{ text }}</div> | |||
<template #input> | |||
<v-select | |||
v-model="wert" | |||
:label="label" | |||
:items="myitems" | |||
autofocus | |||
clearable | |||
/> | |||
</template> | |||
</v-edit-dialog> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'EditDialogSelect', | |||
props: { | |||
value: { | |||
type: String, | |||
required: false, | |||
default: undefined | |||
}, | |||
label: { | |||
type: String, | |||
required: false, | |||
default: '' | |||
}, | |||
savebutton: { | |||
type: String, | |||
required: false, | |||
default: 'Übernehmen' | |||
}, | |||
items: { | |||
type: undefined, | |||
required: true | |||
} | |||
}, | |||
data: () => ({ | |||
wert: null | |||
}), | |||
computed: { | |||
text () { | |||
return this.myitems?.find(e => e.value === this.value)?.text | |||
}, | |||
myitems () { | |||
if (Array.isArray(this.items)) return this.items | |||
return [] | |||
} | |||
}, | |||
methods: { | |||
open () { | |||
this.wert = this.value | |||
}, | |||
close () { | |||
this.wert = null | |||
}, | |||
save () { | |||
this.$emit('input', this.wert) | |||
this.wert = null | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,69 @@ | |||
<template> | |||
<v-list-item | |||
:href="href" | |||
:rel="href && href !== '#' ? 'noopener' : undefined" | |||
:target="href && href !== '#' ? '_blank' : undefined" | |||
:to="item.to" | |||
:active-class="`primary ${!isDark ? 'black' : 'white'}--text`" | |||
> | |||
<v-list-item-icon v-if="item.icon"> | |||
<v-icon v-text="item.icon" /> | |||
</v-list-item-icon> | |||
<v-list-item-icon | |||
v-else-if="text" | |||
class="v-list-item__icon--text" | |||
v-text="computedText" | |||
/> | |||
<v-list-item-content v-if="item.title || item.subtitle"> | |||
<v-list-item-title v-text="item.title" /> | |||
<v-list-item-subtitle v-text="item.subtitle" /> | |||
</v-list-item-content> | |||
</v-list-item> | |||
</template> | |||
<script> | |||
import Themeable from 'vuetify/lib/mixins/themeable' | |||
export default { | |||
name: 'Item', | |||
mixins: [Themeable], | |||
props: { | |||
item: { | |||
type: Object, | |||
default: () => ({ | |||
href: undefined, | |||
icon: undefined, | |||
subtitle: undefined, | |||
title: undefined, | |||
to: undefined | |||
}) | |||
}, | |||
text: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
computed: { | |||
computedText () { | |||
if (!this.item || !this.item.title) return '' | |||
let text = '' | |||
this.item.title.split(' ').forEach(val => { | |||
text += val.substring(0, 1) | |||
}) | |||
return text | |||
}, | |||
href () { | |||
return this.item.href || (!this.item.to ? '#' : undefined) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,142 @@ | |||
<template> | |||
<v-list-group | |||
:group="group" | |||
:prepend-icon="item.icon" | |||
:sub-group="subGroup" | |||
append-icon="mdi-menu-down" | |||
:color="barColor !== 'rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.7)' ? 'white' : 'grey darken-1'" | |||
> | |||
<template #activator> | |||
<v-list-item-icon | |||
v-if="text" | |||
class="v-list-item__icon--text" | |||
v-text="computedText" | |||
/> | |||
<v-list-item-avatar | |||
v-else-if="item.avatar" | |||
class="align-self-center" | |||
color="grey" | |||
> | |||
<v-img src="https://demos.creative-tim.com/material-dashboard-pro/assets/img/faces/avatar.jpg" /> | |||
</v-list-item-avatar> | |||
<v-list-item-content> | |||
<v-list-item-title v-text="item.title" /> | |||
<v-tooltip bottom> | |||
<template #activator="{ on }"> | |||
<v-list-item-subtitle | |||
v-on="on" | |||
v-text="item.subtitle" | |||
/> | |||
</template> | |||
<span>{{ item.subtitle }}</span> | |||
</v-tooltip> | |||
</v-list-item-content> | |||
</template> | |||
<template v-for="(child, i) in children"> | |||
<base-item-sub-group | |||
v-if="child.children" | |||
:key="`sub-group-${i}`" | |||
:item="buildChild(child)" | |||
/> | |||
<base-item | |||
v-else | |||
:key="`item-${i}`" | |||
:item="child" | |||
text | |||
/> | |||
</template> | |||
</v-list-group> | |||
</template> | |||
<script> | |||
// Utilities | |||
import kebabCase from 'lodash/kebabCase' | |||
import { mapState } from 'vuex' | |||
export default { | |||
name: 'ItemGroup', | |||
inheritAttrs: false, | |||
props: { | |||
item: { | |||
type: Object, | |||
default: () => ({ | |||
avatar: undefined, | |||
group: undefined, | |||
title: undefined, | |||
children: [] | |||
}) | |||
}, | |||
subGroup: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
text: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
computed: { | |||
...mapState(['barColor']), | |||
children () { | |||
return this.item.children.map(item => ({ | |||
...item, | |||
to: !item.to ? undefined : `${this.item.group}/${item.to}` | |||
})) | |||
}, | |||
computedText () { | |||
if (!this.item || !this.item.title) return '' | |||
let text = '' | |||
this.item.title.split(' ').forEach(val => { | |||
text += val.substring(0, 1) | |||
}) | |||
return text | |||
}, | |||
group () { | |||
return this.genGroup(this.item.children) | |||
} | |||
}, | |||
methods: { | |||
genGroup (children, parentgroup) { | |||
if (!parentgroup) { | |||
parentgroup = this.item.group | |||
} | |||
return children | |||
.filter(item => item.to || item.group) | |||
.map(item => { | |||
let group = `${parentgroup}/${kebabCase(item.to)}` | |||
if (item.children) { | |||
group = `${group}|${this.genGroup(item.children, `${parentgroup}/${item.group}`)}` | |||
} | |||
return group | |||
}).join('|') | |||
}, | |||
buildChild (child) { | |||
return { ...child, group: `${this.item.group}/${child.group}` } | |||
} | |||
} | |||
} | |||
</script> | |||
<style> | |||
.v-list-group__activator p { | |||
margin-bottom: 0; | |||
} | |||
.v-list-group .v-list-group__items { | |||
padding-left: 3px; | |||
border-left: 1px dashed red; | |||
} | |||
</style> |
@@ -0,0 +1,24 @@ | |||
<template> | |||
<base-item-group | |||
:item="item" | |||
:text="!item.icon" | |||
/> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'ItemSubGroup', | |||
props: { | |||
item: { | |||
type: Object, | |||
default: () => ({ | |||
avatar: undefined, | |||
group: undefined, | |||
title: undefined, | |||
children: [] | |||
}) | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,60 @@ | |||
<template> | |||
<v-alert | |||
v-bind="$attrs" | |||
class="v-alert--material" | |||
dark | |||
v-on="$listeners" | |||
> | |||
<template | |||
v-if="$attrs.icon" | |||
#prepend | |||
> | |||
<v-icon | |||
class="v-alert__icon elevation-6 white" | |||
light | |||
:color="$attrs.color" | |||
> | |||
{{ $attrs.icon }} | |||
</v-icon> | |||
</template> | |||
<slot /> | |||
<template | |||
v-if="$attrs.dismissible" | |||
#close="{ toggle }" | |||
> | |||
<v-btn | |||
:aria-label="$vuetify.lang.t('$vuetify.close')" | |||
color | |||
icon | |||
small | |||
@click="toggle" | |||
> | |||
<v-icon> | |||
$vuetify.icons.cancel | |||
</v-icon> | |||
</v-btn> | |||
</template> | |||
</v-alert> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'MaterialAlert' | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-alert--material | |||
margin-top: 32px | |||
.v-alert__icon | |||
top: -36px | |||
.v-alert__dismissible | |||
align-self: flex-start | |||
margin: 0 !important | |||
padding: 0 !important | |||
</style> |
@@ -0,0 +1,176 @@ | |||
<template> | |||
<v-card | |||
v-bind="$attrs" | |||
:class="classes" | |||
class="v-card--material pa-3" | |||
> | |||
<div class="d-flex grow flex-wrap"> | |||
<v-avatar | |||
v-if="avatar" | |||
size="128" | |||
class="mx-auto v-card--material__avatar elevation-6" | |||
color="grey" | |||
> | |||
<v-img :src="avatar" /> | |||
</v-avatar> | |||
<v-sheet | |||
v-else | |||
:class="{ | |||
'pa-7': !$slots.image | |||
}" | |||
:color="color" | |||
:max-height="icon ? 90 : undefined" | |||
:width="inline || icon ? 'auto' : '100%'" | |||
elevation="6" | |||
class="text-start v-card--material__heading mb-n6" | |||
dark | |||
> | |||
<slot | |||
v-if="$slots.heading" | |||
name="heading" | |||
/> | |||
<slot | |||
v-else-if="$slots.image" | |||
name="image" | |||
/> | |||
<div | |||
v-else-if="title && !icon" | |||
class="display-1 font-weight-light" | |||
v-text="title" | |||
/> | |||
<v-icon | |||
v-else-if="icon" | |||
size="32" | |||
v-text="icon" | |||
/> | |||
<div | |||
v-if="text" | |||
class="headline font-weight-thin" | |||
v-text="text" | |||
/> | |||
</v-sheet> | |||
<div | |||
v-if="$slots['after-heading']" | |||
class="ml-6" | |||
> | |||
<slot name="after-heading" /> | |||
</div> | |||
<v-col | |||
v-if="hoverReveal" | |||
cols="12" | |||
class="text-center py-0 mt-n12" | |||
> | |||
<slot name="reveal-actions" /> | |||
</v-col> | |||
<div | |||
v-else-if="icon && title" | |||
class="ml-4" | |||
> | |||
<div | |||
class="card-title font-weight-light" | |||
v-text="title" | |||
/> | |||
<div | |||
class="card-subtitle font-weight-light font-italic grey--text" | |||
v-text="subTitle" | |||
/> | |||
</div> | |||
</div> | |||
<slot /> | |||
<template v-if="$slots.actions"> | |||
<v-divider class="mt-2" /> | |||
<v-card-actions class="pb-0"> | |||
<slot name="actions" /> | |||
</v-card-actions> | |||
</template> | |||
</v-card> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'MaterialCard', | |||
props: { | |||
avatar: { | |||
type: String, | |||
default: '' | |||
}, | |||
color: { | |||
type: String, | |||
default: 'success' | |||
}, | |||
hoverReveal: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
icon: { | |||
type: String, | |||
default: undefined | |||
}, | |||
image: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
inline: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
text: { | |||
type: String, | |||
default: '' | |||
}, | |||
title: { | |||
type: String, | |||
default: '' | |||
}, | |||
subTitle: { | |||
type: String, | |||
default: '' | |||
} | |||
}, | |||
computed: { | |||
classes () { | |||
return { | |||
'v-card--material--has-heading': this.hasHeading, | |||
'v-card--material--hover-reveal': this.hoverReveal | |||
} | |||
}, | |||
hasHeading () { | |||
return Boolean(this.$slots.heading || this.title || this.icon) | |||
}, | |||
hasAltHeading () { | |||
return Boolean(this.$slots.heading || (this.title && this.icon)) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-card--material | |||
&__avatar | |||
position: relative | |||
top: -64px | |||
margin-bottom: -32px | |||
&__heading | |||
position: relative | |||
top: -40px | |||
transition: .3s ease | |||
z-index: 1 | |||
&.v-card--material--hover-reveal:hover | |||
.v-card--material__heading | |||
transform: translateY(-40px) | |||
</style> |
@@ -0,0 +1,95 @@ | |||
<template> | |||
<base-material-card | |||
class="v-card--material-chart" | |||
v-bind="$attrs" | |||
v-on="$listeners" | |||
> | |||
<template #heading> | |||
<chartist | |||
:data="data" | |||
:event-handlers="eventHandlers" | |||
:options="options" | |||
:ratio="ratio" | |||
:responsive-options="responsiveOptions" | |||
:type="type" | |||
style="max-height: 150px;" | |||
/> | |||
</template> | |||
<slot | |||
slot="reveal-actions" | |||
name="reveal-actions" | |||
/> | |||
<slot /> | |||
<slot | |||
slot="actions" | |||
name="actions" | |||
/> | |||
</base-material-card> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'MaterialChartCard', | |||
inheritAttrs: false, | |||
props: { | |||
data: { | |||
type: Object, | |||
default: () => ({}) | |||
}, | |||
eventHandlers: { | |||
type: Array, | |||
default: () => ([]) | |||
}, | |||
options: { | |||
type: Object, | |||
default: () => ({}) | |||
}, | |||
ratio: { | |||
type: String, | |||
default: undefined | |||
}, | |||
responsiveOptions: { | |||
type: Array, | |||
default: () => ([]) | |||
}, | |||
type: { | |||
type: String, | |||
required: true, | |||
validator: v => ['Bar', 'Line', 'Pie'].includes(v) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-card--material-chart | |||
p | |||
color: #999 | |||
.v-card--material__heading | |||
max-height: 185px | |||
.ct-label | |||
color: inherit | |||
opacity: .7 | |||
font-size: 0.975rem | |||
font-weight: 100 | |||
.ct-grid | |||
stroke: rgba(255, 255, 255, 0.2) | |||
.ct-series-a .ct-point, | |||
.ct-series-a .ct-line, | |||
.ct-series-a .ct-bar, | |||
.ct-series-a .ct-slice-donut | |||
stroke: rgba(255,255,255,.8) | |||
.ct-series-a .ct-slice-pie, | |||
.ct-series-a .ct-area | |||
fill: rgba(255,255,255,.4) | |||
</style> |
@@ -0,0 +1,164 @@ | |||
<template> | |||
<v-dialog | |||
v-model="show" | |||
persistent | |||
scrollable | |||
styles="overflow:visible;" | |||
content-class="v-dialog--material" | |||
@keydown.esc="$emit('esc')" | |||
> | |||
<base-material-card | |||
:avatar="avatar" | |||
:color="color" | |||
:icon="icon" | |||
:image="image" | |||
:text="text" | |||
:title="title" | |||
:sub-title="subTitle" | |||
> | |||
<v-card-text> | |||
<slot /> | |||
</v-card-text> | |||
<template #actions> | |||
<v-spacer /> | |||
<template v-for="(a,i) in actions"> | |||
<v-btn | |||
v-if="a === 'ok'" | |||
:key="`${a}-${i}`" | |||
@click="$emit(a)" | |||
> | |||
OK | |||
</v-btn> | |||
<v-btn | |||
v-if="a === 'cancel'" | |||
:key="`${a}-${i}`" | |||
@click="$emit('close')" | |||
> | |||
Abbrechen | |||
</v-btn> | |||
<v-btn | |||
v-if="a === 'close'" | |||
:key="`${a}-${i}`" | |||
@click="$emit('close')" | |||
> | |||
Schließen | |||
</v-btn> | |||
<v-btn | |||
v-if="a === 'save'" | |||
:key="`${a}-${i}`" | |||
@click="$emit(a)" | |||
> | |||
Speichern | |||
</v-btn> | |||
<v-btn | |||
v-if="a === 'del'" | |||
:key="`${a}-${i}`" | |||
@click="$emit(a)" | |||
> | |||
Löschen | |||
</v-btn> | |||
</template> | |||
</template> | |||
</base-material-card> | |||
</v-dialog> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'MaterialCard', | |||
inheritAttrs: false, | |||
props: { | |||
value: { | |||
type: Boolean, | |||
required: true, | |||
default: false | |||
}, | |||
avatar: { | |||
type: String, | |||
default: '' | |||
}, | |||
color: { | |||
type: String, | |||
default: 'success' | |||
}, | |||
hoverReveal: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
icon: { | |||
type: String, | |||
default: undefined | |||
}, | |||
image: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
inline: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
text: { | |||
type: String, | |||
default: '' | |||
}, | |||
title: { | |||
type: String, | |||
default: '' | |||
}, | |||
subTitle: { | |||
type: String, | |||
default: '' | |||
}, | |||
actions: { | |||
type: Array, | |||
default: () => [] | |||
} | |||
}, | |||
computed: { | |||
show: { | |||
get () { | |||
return this.value | |||
}, | |||
set (val) { | |||
this.$emit('input', val) | |||
} | |||
}, | |||
classes () { | |||
return { | |||
'v-card--material--has-heading': this.hasHeading, | |||
'v-card--material--hover-reveal': this.hoverReveal | |||
} | |||
}, | |||
hasHeading () { | |||
return Boolean(this.$slots.heading || this.title || this.icon) | |||
}, | |||
hasAltHeading () { | |||
return Boolean(this.$slots.heading || (this.title && this.icon)) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-card--material | |||
&__avatar | |||
position: relative | |||
top: -64px | |||
margin-bottom: -32px | |||
&__heading | |||
position: relative | |||
top: -40px | |||
transition: .3s ease | |||
z-index: 1 | |||
&.v-card--material--hover-reveal:hover | |||
.v-card--material__heading | |||
transform: translateY(-40px) | |||
.v-dialog--material | |||
overflow-y: visible !important | |||
</style> |
@@ -0,0 +1,70 @@ | |||
<template> | |||
<v-menu | |||
v-model="value" | |||
:transition="transition" | |||
offset-y | |||
v-bind="$attrs" | |||
> | |||
<template #activator="{ attrs, on }"> | |||
<v-btn | |||
:color="color" | |||
default | |||
min-width="200" | |||
rounded | |||
v-bind="attrs" | |||
v-on="on" | |||
> | |||
<slot /> | |||
<v-icon> | |||
mdi-{{ value ? 'menu-up' : 'menu-down' }} | |||
</v-icon> | |||
</v-btn> | |||
</template> | |||
<v-sheet> | |||
<v-list dense> | |||
<v-list-item | |||
v-for="(item, i) in items" | |||
:key="i" | |||
@click="$(`click:action-${item.id}`)" | |||
> | |||
<v-list-item-content> | |||
<v-list-item-title v-text="item.text" /> | |||
</v-list-item-content> | |||
</v-list-item> | |||
</v-list> | |||
</v-sheet> | |||
</v-menu> | |||
</template> | |||
<script> | |||
// Mixins | |||
import Proxyable from 'vuetify/lib/mixins/proxyable' | |||
export default { | |||
name: 'MaterialDropdown', | |||
mixins: [Proxyable], | |||
props: { | |||
color: { | |||
type: String, | |||
default: 'primary' | |||
}, | |||
items: { | |||
type: Array, | |||
default: () => ([ | |||
{ | |||
id: undefined, | |||
text: undefined | |||
} | |||
]) | |||
}, | |||
transition: { | |||
type: String, | |||
default: 'scale-transition' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,66 @@ | |||
<template> | |||
<v-snackbar | |||
:class="classes" | |||
:value="value" | |||
v-bind="{ | |||
...$attrs, | |||
...$props, | |||
'color': 'transparent' | |||
}" | |||
@change="$emit('change', $event)" | |||
> | |||
<base-material-alert | |||
:color="color" | |||
:dismissible="dismissible" | |||
:type="type" | |||
class="ma-0" | |||
dark | |||
> | |||
<slot /> | |||
</base-material-alert> | |||
</v-snackbar> | |||
</template> | |||
<script> | |||
// Components | |||
import { VSnackbar } from 'vuetify/lib' | |||
export default { | |||
name: 'BaseMaterialSnackbar', | |||
extends: VSnackbar, | |||
props: { | |||
dismissible: { | |||
type: Boolean, | |||
default: true | |||
}, | |||
type: { | |||
type: String, | |||
default: '' | |||
} | |||
}, | |||
computed: { | |||
classes () { | |||
return { | |||
...VSnackbar.options.computed.classes.call(this), | |||
'v-snackbar--material': true | |||
} | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-snackbar--material | |||
margin-top: 32px | |||
margin-bottom: 32px | |||
.v-alert--material, | |||
.v-snack__wrapper | |||
border-radius: 4px | |||
.v-snack__content | |||
overflow: visible | |||
padding: 0 | |||
</style> |
@@ -0,0 +1,113 @@ | |||
<template> | |||
<base-material-card | |||
:icon="icon" | |||
class="v-card--material-stats" | |||
v-bind="$attrs" | |||
v-on="$listeners" | |||
> | |||
<template #after-heading> | |||
<div class="ml-auto text-right"> | |||
<div | |||
class="body-3 grey--text font-weight-light" | |||
v-text="title" | |||
/> | |||
<h3 class="display-2 font-weight-light text--primary"> | |||
{{ value }} <small>{{ smallValue }}</small> | |||
</h3> | |||
</div> | |||
</template> | |||
<v-col | |||
cols="12" | |||
class="px-0" | |||
> | |||
<v-divider /> | |||
</v-col> | |||
<v-icon | |||
:color="subIconColor" | |||
size="16" | |||
class="ml-2 mr-1" | |||
> | |||
{{ subIcon }} | |||
</v-icon> | |||
<span | |||
:class="subTextColor" | |||
class="caption grey--text font-weight-light" | |||
v-text="subText" | |||
/> | |||
</base-material-card> | |||
</template> | |||
<script> | |||
import Card from './Card' | |||
export default { | |||
name: 'MaterialStatsCard', | |||
inheritAttrs: false, | |||
props: { | |||
...Card.props, | |||
icon: { | |||
type: String, | |||
required: true | |||
}, | |||
subIcon: { | |||
type: String, | |||
default: undefined | |||
}, | |||
subIconColor: { | |||
type: String, | |||
default: undefined | |||
}, | |||
subTextColor: { | |||
type: String, | |||
default: undefined | |||
}, | |||
subText: { | |||
type: String, | |||
default: undefined | |||
}, | |||
title: { | |||
type: String, | |||
default: undefined | |||
}, | |||
value: { | |||
type: String, | |||
default: undefined | |||
}, | |||
smallValue: { | |||
type: String, | |||
default: undefined | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-card--material-stats | |||
display: flex | |||
flex-wrap: wrap | |||
position: relative | |||
> div:first-child | |||
justify-content: space-between | |||
.v-card | |||
border-radius: 4px | |||
flex: 0 1 auto | |||
.v-card__text | |||
display: inline-block | |||
flex: 1 0 calc(100% - 120px) | |||
position: absolute | |||
top: 0 | |||
right: 0 | |||
width: 100% | |||
.v-card__actions | |||
flex: 1 0 100% | |||
</style> |
@@ -0,0 +1,43 @@ | |||
<template> | |||
<v-tabs | |||
v-model="internalValue" | |||
:active-class="`${color} ${$vuetify.theme.dark ? 'black' : 'white'}--text`" | |||
class="v-tabs--pill" | |||
hide-slider | |||
v-bind="$attrs" | |||
> | |||
<slot /> | |||
<slot name="items" /> | |||
</v-tabs> | |||
</template> | |||
<script> | |||
// Mixins | |||
import Proxyable from 'vuetify/lib/mixins/proxyable' | |||
export default { | |||
name: 'MaterialTabs', | |||
mixins: [Proxyable], | |||
props: { | |||
color: { | |||
type: String, | |||
default: 'primary' | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-tabs--pill | |||
.v-tab, | |||
.v-tab:before | |||
border-radius: 24px | |||
&.v-tabs--icons-and-text | |||
.v-tab, | |||
.v-tab:before | |||
border-radius: 4px | |||
</style> |
@@ -0,0 +1,76 @@ | |||
<template> | |||
<v-card class="text-center v-card--testimony"> | |||
<div class="pt-6"> | |||
<v-icon | |||
color="black" | |||
x-large | |||
> | |||
mdi-format-quote-close | |||
</v-icon> | |||
</div> | |||
<v-card-text | |||
class="display-1 font-weight-light font-italic mb-3" | |||
v-text="blurb" | |||
/> | |||
<div | |||
class="display-2 font-weight-light mb-2" | |||
v-text="author" | |||
/> | |||
<div | |||
class="body-2 text-uppercase grey--text" | |||
v-text="handle" | |||
/> | |||
<v-avatar | |||
class="elevation-12" | |||
color="grey" | |||
size="100" | |||
> | |||
<v-img | |||
:alt="`${author} Testimonial`" | |||
:src="avatar" | |||
/> | |||
</v-avatar> | |||
<div /> | |||
</v-card> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'BaseMaterialTestimony', | |||
props: { | |||
author: { | |||
type: String, | |||
default: '' | |||
}, | |||
avatar: { | |||
type: String, | |||
default: 'https://demos.creative-tim.com/material-dashboard-pro/assets/img/faces/card-profile1-square.jpg' | |||
}, | |||
blurb: { | |||
type: String, | |||
default: '' | |||
}, | |||
handle: { | |||
type: String, | |||
default: '' | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-card--testimony | |||
padding-bottom: 72px | |||
margin-bottom: 64px | |||
.v-avatar | |||
position: absolute | |||
left: calc(50% - 64px) | |||
top: calc(100% - 64px) | |||
</style> |
@@ -0,0 +1,110 @@ | |||
<template> | |||
<v-card | |||
class="v-card--wizard" | |||
elevation="12" | |||
max-width="700" | |||
> | |||
<v-card-title class="justify-center display-2 font-weight-light pt-5"> | |||
Build your profile | |||
</v-card-title> | |||
<div class="text-center display-1 grey--text font-weight-light mb-6"> | |||
This information will let us know more about you. | |||
</div> | |||
<v-tabs | |||
ref="tabs" | |||
v-model="internalValue" | |||
background-color="green lighten-5" | |||
color="white" | |||
grow | |||
slider-size="50" | |||
> | |||
<v-tabs-slider | |||
class="mt-1" | |||
color="success" | |||
/> | |||
<v-tab | |||
v-for="(item, i) in items" | |||
:key="i" | |||
:ripple="false" | |||
:disabled="!availableSteps.includes(i)" | |||
> | |||
{{ item }} | |||
</v-tab> | |||
</v-tabs> | |||
<div class="my-6" /> | |||
<v-card-text> | |||
<v-tabs-items v-model="internalValue"> | |||
<slot /> | |||
</v-tabs-items> | |||
</v-card-text> | |||
<v-card-actions class="pb-4 pa-4"> | |||
<v-btn | |||
:disabled="internalValue === 0" | |||
class="white--text" | |||
color="grey darken-2" | |||
min-width="125" | |||
@click="$emit('click-prev')" | |||
> | |||
Previous | |||
</v-btn> | |||
<v-spacer /> | |||
<v-btn | |||
:disabled="!availableSteps.includes(internalValue + 1)" | |||
color="success" | |||
min-width="100" | |||
@click="$emit('click-next')" | |||
> | |||
{{ internalValue === items.length - 1 ? 'Finish' : 'Next' }} | |||
</v-btn> | |||
</v-card-actions> | |||
</v-card> | |||
</template> | |||
<script> | |||
// Mixins | |||
import Proxyable from 'vuetify/lib/mixins/proxyable' | |||
export default { | |||
name: 'BaseMaterialWizard', | |||
mixins: [Proxyable], | |||
props: { | |||
availableSteps: { | |||
type: Array, | |||
default: () => ([]) | |||
}, | |||
items: { | |||
type: Array, | |||
default: () => ([]) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
.v-card--wizard | |||
overflow: visible | |||
.v-tabs-bar | |||
height: 56px | |||
padding: 0 8px | |||
.v-tabs-slider-wrapper | |||
overflow: visible | |||
.v-tabs-slider | |||
border-radius: 4px | |||
.v-tabs-slider-wrapper | |||
contain: initial | |||
z-index: 0 | |||
</style> |
@@ -0,0 +1,455 @@ | |||
<template> | |||
<div | |||
class="number-input" | |||
:class="{ | |||
'number-input--inline': inline, | |||
'number-input--center': center, | |||
'number-input--controls': controls, | |||
[`number-input--${size}`]: size, | |||
}" | |||
v-on="listeners" | |||
> | |||
<button | |||
v-if="controls" | |||
class="number-input__button number-input__button--minus" | |||
type="button" | |||
tabindex="-1" | |||
:disabled="disabled || readonly || !decreasable" | |||
@click="decrease" | |||
/> | |||
<input | |||
:ref="myref" | |||
class="number-input__input" | |||
v-bind="attrs" | |||
:name="name" | |||
:value="formatted" | |||
:min="min" | |||
:max="max" | |||
:step="step" | |||
:readonly="readonly || !inputtable" | |||
:disabled="disabled || (!decreasable && !increasable)" | |||
:placeholder="placeholder" | |||
:tabindex="tabindex" | |||
:autofocus="autofocus" | |||
autocomplete="off" | |||
@change="change" | |||
@paste="paste" | |||
@keydown.down="decrease" | |||
@keydown.up="increase" | |||
> | |||
<button | |||
v-if="controls" | |||
class="number-input__button number-input__button--plus" | |||
type="button" | |||
tabindex="-1" | |||
:disabled="disabled || readonly || !increasable" | |||
@click="increase" | |||
/> | |||
</div> | |||
</template> | |||
<script> | |||
const isNaN = Number.isNaN || window.isNaN | |||
const REGEXP_NUMBER = /^-?(?:\d+|\d+\.\d+|\.\d+)(?:[eE][-+]?\d+)?$/ | |||
const REGEXP_DECIMALS = /\.\d*(?:0|9){10}\d*$/ | |||
const normalizeDecimalNumber = (value, times = 100) => ( | |||
REGEXP_DECIMALS.test(value) ? (Math.round(value * times) / times) : value | |||
) | |||
export default { | |||
name: 'NumberInput', | |||
model: { | |||
event: 'change' | |||
}, | |||
props: { | |||
attrs: { | |||
type: Object, | |||
default: undefined | |||
}, | |||
center: Boolean, | |||
controls: Boolean, | |||
disabled: Boolean, | |||
inputtable: { | |||
type: Boolean, | |||
default: true | |||
}, | |||
inline: Boolean, | |||
max: { | |||
type: Number, | |||
default: Infinity | |||
}, | |||
min: { | |||
type: Number, | |||
default: -Infinity | |||
}, | |||
name: { | |||
type: String, | |||
default: undefined | |||
}, | |||
placeholder: { | |||
type: String, | |||
default: undefined | |||
}, | |||
readonly: Boolean, | |||
rounded: Boolean, | |||
size: { | |||
type: String, | |||
default: undefined | |||
}, | |||
step: { | |||
type: Number, | |||
default: 1 | |||
}, | |||
value: { | |||
type: Number, | |||
default: NaN | |||
}, | |||
decimals: { | |||
type: Number, | |||
default: NaN | |||
}, | |||
tabindex: { | |||
type: Number, | |||
default: undefined | |||
}, | |||
autofocus: Boolean, | |||
myref: { | |||
type: String, | |||
default: 'input' | |||
} | |||
}, | |||
data () { | |||
return { | |||
currentValue: NaN | |||
} | |||
}, | |||
computed: { | |||
/** | |||
* Indicate if the value is increasable. | |||
* @returns {boolean} Return `true` if it is decreasable, else `false`. | |||
*/ | |||
increasable () { | |||
const num = this.currentValue | |||
return isNaN(num) || num < this.max | |||
}, | |||
/** | |||
* Indicate if the value is decreasable. | |||
* @returns {boolean} Return `true` if it is decreasable, else `false`. | |||
*/ | |||
decreasable () { | |||
const num = this.currentValue | |||
return isNaN(num) || num > this.min | |||
}, | |||
/** | |||
* Filter listeners | |||
* @returns {Object} Return filtered listeners. | |||
*/ | |||
listeners () { | |||
const listeners = { ...this.$listeners } | |||
delete listeners.change | |||
return listeners | |||
}, | |||
formatted () { | |||
return isNaN(this.currentValue) ? '' : (isNaN(this.decimals) ? this.currentValue : parseFloat(this.currentValue).toFixed(this.decimals)) | |||
} | |||
}, | |||
watch: { | |||
value: { | |||
immediate: true, | |||
handler (newValue, oldValue) { | |||
if ( | |||
// Avoid triggering change event when created | |||
!(isNaN(newValue) && typeof oldValue === 'undefined') && | |||
// Avoid infinite loop | |||
newValue !== this.currentValue | |||
) { | |||
this.setValue(newValue) | |||
} | |||
} | |||
} | |||
}, | |||
methods: { | |||
/** | |||
* Change event handler. | |||
* @param {string} value - The new value. | |||
*/ | |||
change (event) { | |||
this.setValue(Math.min(this.max, Math.max(this.min, parseFloat(event.target.value.replace(',', '.'))))) | |||
}, | |||
/** | |||
* Paste event handler. | |||
* @param {Event} event - Event object. | |||
*/ | |||
paste (event) { | |||
const clipboardData = event.clipboardData || window.clipboardData | |||
if (clipboardData && !REGEXP_NUMBER.test(clipboardData.getData('text'))) { | |||
event.preventDefault() | |||
} | |||
}, | |||
/** | |||
* Decrease the value. | |||
*/ | |||
decrease () { | |||
if (this.decreasable) { | |||
let { currentValue } = this | |||
if (isNaN(currentValue)) { | |||
currentValue = 0 | |||
} | |||
this.setValue(Math.min(this.max, Math.max( | |||
this.min, | |||
normalizeDecimalNumber(currentValue - this.step) | |||
))) | |||
} | |||
}, | |||
/** | |||
* Increase the value. | |||
*/ | |||
increase () { | |||
if (this.increasable) { | |||
let { currentValue } = this | |||
if (isNaN(currentValue)) { | |||
currentValue = 0 | |||
} | |||
this.setValue(Math.min(this.max, Math.max( | |||
this.min, | |||
normalizeDecimalNumber(currentValue + this.step) | |||
))) | |||
} | |||
}, | |||
/** | |||
* Set new value and dispatch change event. | |||
* @param {number} value - The new value to set. | |||
*/ | |||
setValue (value) { | |||
const oldValue = this.currentValue | |||
let newValue = this.rounded ? Math.round(value) : value | |||
if (this.min <= this.max) { | |||
newValue = Math.min(this.max, Math.max(this.min, newValue)) | |||
} | |||
this.currentValue = newValue | |||
if (newValue === oldValue) { | |||
// Force to override the number in the input box (#13). | |||
this.$refs[this.ref].value = newValue | |||
} | |||
this.$emit('change', newValue, oldValue) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.number-input { | |||
display: block; | |||
font-size: 0; | |||
max-width: 100%; | |||
overflow: hidden; | |||
position: relative; | |||
&__button { | |||
background-color: #fff; | |||
border: 0; | |||
border-radius: 0.25rem; | |||
bottom: 1px; | |||
position: absolute; | |||
top: 1px; | |||
width: 2.5rem; | |||
z-index: 1; | |||
&:focus { | |||
outline: none; | |||
} | |||
&:hover { | |||
&::before, | |||
&::after { | |||
background-color: #0074d9; | |||
} | |||
} | |||
&:disabled { | |||
opacity: 0.65; | |||
&::before, | |||
&::after { | |||
background-color: #ddd; | |||
} | |||
} | |||
&::before, | |||
&::after { | |||
background-color: #111; | |||
content: ""; | |||
left: 50%; | |||
position: absolute; | |||
top: 50%; | |||
transform: translate(-50%, -50%); | |||
transition: background-color 0.15s; | |||
} | |||
&::before { | |||
height: 1px; | |||
width: 50%; | |||
} | |||
&::after { | |||
height: 50%; | |||
width: 1px; | |||
} | |||
&--minus { | |||
border-bottom-right-radius: 0; | |||
border-right: 1px solid #ddd; | |||
border-top-right-radius: 0; | |||
left: 1px; | |||
&::after { | |||
visibility: hidden; | |||
} | |||
} | |||
&--plus { | |||
border-bottom-left-radius: 0; | |||
border-left: 1px solid #ddd; | |||
border-top-left-radius: 0; | |||
right: 1px; | |||
} | |||
} | |||
&__input { | |||
-moz-appearance: textfield; | |||
background-color: #fff; | |||
border: 1px solid #ddd; | |||
border-radius: 0.25rem; | |||
display: block; | |||
font-size: 1rem; | |||
line-height: 1.5; | |||
max-width: 100%; | |||
min-height: 1.5rem; | |||
min-width: 3rem; | |||
padding: 0.4375rem 0.875rem; | |||
transition: border-color 0.15s; | |||
width: 100%; | |||
&::-webkit-outer-spin-button, | |||
&::-webkit-inner-spin-button { | |||
-webkit-appearance: none; | |||
} | |||
&:focus { | |||
border-color: #0074d9; | |||
outline: none; | |||
} | |||
&:disabled, | |||
&[readonly] { | |||
background-color: #f8f8f8; | |||
} | |||
} | |||
&--inline { | |||
display: inline-block; | |||
& > input { | |||
display: inline-block; | |||
width: 12.5rem; | |||
} | |||
} | |||
&--center { | |||
& > input { | |||
text-align: center; | |||
} | |||
} | |||
&--controls { | |||
& > input { | |||
padding-left: 3.375rem; | |||
padding-right: 3.375rem; | |||
} | |||
} | |||
&--small { | |||
& > input { | |||
border-radius: 0.1875rem; | |||
font-size: 0.875rem; | |||
padding: 0.25rem 0.5rem; | |||
} | |||
&.number-input--inline > input { | |||
width: 10rem; | |||
} | |||
&.number-input--controls > button { | |||
width: 2rem; | |||
} | |||
&.number-input--controls > input { | |||
padding-left: 2.5rem; | |||
padding-right: 2.5rem; | |||
} | |||
} | |||
&--large { | |||
& > input { | |||
border-radius: 0.3125rem; | |||
font-size: 1.25rem; | |||
padding: 0.5rem 1rem; | |||
} | |||
&.number-input--inline > input { | |||
width: 15rem; | |||
} | |||
&.number-input--controls > button { | |||
width: 3rem; | |||
} | |||
&.number-input--controls > input { | |||
padding-left: 4rem; | |||
padding-right: 4rem; | |||
} | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,34 @@ | |||
<template> | |||
<div class="display-2 font-weight-light col col-12 text-left text--primary pa-0 mb-8"> | |||
<h5 class="font-weight-light"> | |||
{{ subheading }} | |||
<template v-if="text"> | |||
<span | |||
class="subtitle-1" | |||
v-text="text" | |||
/> | |||
</template> | |||
</h5> | |||
<div class="pt-2"> | |||
<slot /> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'Subheading', | |||
props: { | |||
subheading: { | |||
type: String, | |||
default: '' | |||
}, | |||
text: { | |||
type: String, | |||
default: '' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,40 @@ | |||
<template> | |||
<section class="mb-12 text-center"> | |||
<h1 | |||
class="font-weight-light mb-2 headline" | |||
v-text="`Vuetify ${heading}`" | |||
/> | |||
<span | |||
class="font-weight-light subtitle-1" | |||
> | |||
Please checkout the | |||
<a | |||
:href="`https://vuetifyjs.com/${link}`" | |||
rel="noopener" | |||
target="_blank" | |||
class="secondary--text" | |||
style="text-decoration:none;" | |||
> | |||
full documentation | |||
</a> | |||
</span> | |||
</section> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'VComponent', | |||
props: { | |||
heading: { | |||
type: String, | |||
default: '' | |||
}, | |||
link: { | |||
type: String, | |||
default: '' | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,17 @@ | |||
<template> | |||
<v-container> | |||
<v-card-title class="red--text"> | |||
403: Kein Zugriff | |||
</v-card-title> | |||
</v-container> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'E403' | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,27 @@ | |||
import Vue from 'vue' | |||
import App from './App.vue' | |||
import router from './plugins/router' | |||
import store from './plugins/store' | |||
import vuetify from './plugins/vuetify' | |||
import '@mdi/font/css/materialdesignicons.css' | |||
import '@fortawesome/fontawesome-pro/css/all.css' | |||
import './plugins/cookies' | |||
import './plugins/compositionAPI' | |||
import './plugins/base' | |||
Vue.component('E403', () => import('./components/e403.vue')) | |||
Vue.component('DateSelector', () => import('./components/DateSelector.vue')) | |||
Vue.component('DateTimeSelector', () => import('./components/DateTimeSelector.vue')) | |||
Vue.component('Confirm', () => import('./components/Confirm.vue')) | |||
Vue.config.productionTip = false | |||
new Vue({ | |||
router, | |||
store, | |||
// @ts-ignore | |||
vuetify, | |||
render: h => h(App) | |||
}).$mount('#app') |
@@ -0,0 +1,19 @@ | |||
import Vue from 'vue' | |||
// @ts-ignore | |||
import upperFirst from 'lodash/upperFirst' | |||
// @ts-ignore | |||
import camelCase from 'lodash/camelCase' | |||
const requireComponent = require.context( | |||
'@/components/base', true, /\.vue$/ | |||
) | |||
requireComponent.keys().forEach(fileName => { | |||
const componentConfig = requireComponent(fileName) | |||
const componentName = upperFirst( | |||
camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')) | |||
) | |||
Vue.component(`Base${componentName}`, componentConfig.default || componentConfig) | |||
}) |
@@ -0,0 +1,4 @@ | |||
import Vue from 'vue' | |||
import VueCompositionAPI from '@vue/composition-api' | |||
Vue.use(VueCompositionAPI) |
@@ -0,0 +1,4 @@ | |||
import Vue from 'vue' | |||
import VueCookies from 'vue-cookies' | |||
Vue.use(VueCookies) |
@@ -0,0 +1,82 @@ | |||
// @ts-ignore | |||
import { v4 as uuid } from 'uuid' | |||
import { SubscriptionClient } from 'subscriptions-transport-ws' | |||
import ApolloClient from 'apollo-client' | |||
import VueApollo from 'vue-apollo' | |||
import { defaultDataIdFromObject, InMemoryCache } from 'apollo-cache-inmemory' | |||
// @ts-ignore | |||
import * as Cookie from 'js-cookie' | |||
import { ref } from '@vue/composition-api' | |||
import gql from 'graphql-tag' | |||
export const clientId = uuid() | |||
const server = process.env.NODE_ENV === 'production' ? 'wss://turnenaufzeit.de/gql' : 'ws://localhost:3000/graphql' | |||
const client = new SubscriptionClient(server, { | |||
reconnect: true, | |||
connectionParams: () => ({ | |||
clientId, | |||
token: Cookie.get('token') | |||
}) | |||
}) | |||
const graphQL = new ApolloClient({ | |||
// @ts-ignore | |||
link: client, | |||
cache: new InMemoryCache({ | |||
dataIdFromObject: object => { | |||
switch (object.__typename) { | |||
default: return defaultDataIdFromObject(object) | |||
} | |||
} | |||
}), | |||
defaultOptions: { | |||
watchQuery: { | |||
fetchPolicy: 'cache-and-network' | |||
}, | |||
query: { | |||
fetchPolicy: 'no-cache' | |||
} | |||
} | |||
}) | |||
export default new VueApollo({ | |||
defaultClient: graphQL | |||
}) | |||
export const useGraphQL = (context: any): any => { | |||
const connected = ref(client?.client?.readyState === 1) | |||
client.on('connecting', () => { connected.value = false }) | |||
client.on('connected', () => { connected.value = true }) | |||
client.on('reconnecting', () => { connected.value = false }) | |||
client.on('reconnected', () => { connected.value = true }) | |||
client.on('disconnected', () => { connected.value = false }) | |||
client.on('error', () => { connected.value = false }) | |||
const login = async (email: string, passwort: string) => { | |||
let query = `login(email: "${email}", passwort: "${passwort}")` | |||
if (!email || !passwort) { | |||
query = `login(token: "${Cookie.get('token')}")` | |||
} | |||
const { data } = await graphQL.mutate({ | |||
mutation: gql`mutation { ${query} { | |||
_id | |||
token | |||
givenName | |||
familyName | |||
adminOf { _id name plz ort } | |||
}}` | |||
}) | |||
console.log(data) | |||
context.root.$store.commit('SET_PROFILE', data.login) | |||
Cookie.set('token', data?.login?.token, '12h', null, null, null, 'strict') | |||
} | |||
return { connected, login } | |||
} |
@@ -0,0 +1,20 @@ | |||
import Vue from 'vue' | |||
import VueRouter, { RouteConfig } from 'vue-router' | |||
Vue.use(VueRouter) | |||
const routes: Array<RouteConfig> = [ | |||
{ | |||
path: '/', | |||
name: 'Home', | |||
component: () => import('../views/Index.vue') | |||
} | |||
] | |||
const router = new VueRouter({ | |||
mode: 'hash', | |||
base: process.env.BASE_URL, | |||
routes | |||
}) | |||
export default router |
@@ -0,0 +1,38 @@ | |||
import Vue from 'vue' | |||
import Vuex from 'vuex' | |||
Vue.use(Vuex) | |||
const state: { profile: any, messages: string[], drawer: boolean, snackbar: {visible: boolean, text: string } } = { | |||
profile: null, | |||
messages: [], | |||
drawer: true, | |||
snackbar: { | |||
visible: false, | |||
text: '' | |||
} | |||
} | |||
export default new Vuex.Store({ | |||
state, | |||
mutations: { | |||
SET_DRAWER (state, payload) { | |||
state.drawer = payload | |||
}, | |||
OPEN_SNACKBAR (state, text) { | |||
state.snackbar.text = text | |||
state.snackbar.visible = true | |||
}, | |||
CLOSE_SNACKBAR (state) { | |||
state.snackbar.visible = false | |||
} | |||
}, | |||
actions: { | |||
}, | |||
modules: { | |||
}, | |||
getters: { | |||
profile: (state) => state.profile || {}, | |||
isMaster: (state) => !!state.profile?.master | |||
} | |||
}) |
@@ -0,0 +1,31 @@ | |||
import Vue from 'vue' | |||
// @ts-ignore | |||
import Vuetify from 'vuetify/lib/framework' | |||
import de from 'vuetify/src/locale/de' | |||
import '@/sass/overrides.sass' | |||
Vue.use(Vuetify) | |||
const theme = { | |||
primary: '#FF041D', | |||
secondary: '#9C27b0', | |||
accent: '#9C27b0', | |||
info: '#00CAE3' | |||
} | |||
export default new Vuetify({ | |||
lang: { | |||
locales: { de }, | |||
current: 'de' | |||
}, | |||
icons: { | |||
iconfont: 'fa' | |||
}, | |||
theme: { | |||
themes: { | |||
dark: theme, | |||
light: theme | |||
} | |||
} | |||
}) |
@@ -0,0 +1,55 @@ | |||
// ========================================================= | |||
// * Vuetify Material Dashboard PRO - v2.1.0 | |||
// ========================================================= | |||
// | |||
// * Product Page: https://www.creative-tim.com/product/vuetify-material-dashboard-pro | |||
// * Copyright 2019 Creative Tim (https://www.creative-tim.com) | |||
// | |||
// * Coded by Creative Tim | |||
// | |||
// ========================================================= | |||
// | |||
// * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |||
// Creative Tim refine style code | |||
@import variables | |||
@import vuetify-material/sidebar | |||
@import vuetify-material/appbar | |||
@import vuetify-material/buttons | |||
@import vuetify-material/pagination | |||
@import vuetify-material/badge | |||
@import vuetify-material/footer | |||
@import vuetify-material/view | |||
@import vuetify-material/settings | |||
@import vuetify-material/card | |||
@import vuetify-material/table | |||
@import vuetify-material/vectormap | |||
@import vuetify-material/tab | |||
@import vuetify-material/notification | |||
@import vuetify-material/modal | |||
@import vuetify-material/form | |||
@import vuetify-material/map | |||
@import vuetify-material/timeline | |||
@import vuetify-material/chip | |||
@import vuetify-material/calendar | |||
@import vuetify-material/pages | |||
@import vuetify-material/rtl | |||
.v-btn.v-size--default, | |||
.v-btn.v-size--large | |||
&:not(.v-btn--icon):not(.v-btn--fab) | |||
padding: 0 30px !important | |||
.theme--light.v-list-item .v-list-item__action-text, | |||
.theme--light.v-list-item .v-list-item__subtitle | |||
color: #999 | |||
.theme--light.v-text-field>.v-input__control>.v-input__slot:before | |||
border-color: #d2d2d2 | |||
.v-label.v-label, | |||
.v-alert.v-alert | |||
font-size: $font-size-root | |||
.theme--light .v-content | |||
background-color: #eee |
@@ -0,0 +1,34 @@ | |||
$font-size-root: 14px; | |||
$sheet-border-radius: 4px; | |||
$list-item-title-font-size: 0.929rem; | |||
$list-item-dense-title-font-size: 0.929rem; | |||
$list-item-dense-title-font-weight: initial; | |||
$fab-icon-sizes: ( small: 20 ); | |||
$btn-font-sizes: ( default: 1rem, large: 1rem ); | |||
$btn-sizes: ( default: 41, large: 54 ); | |||
$btn-letter-spacing: 0; | |||
$btn-font-weight: 400; | |||
$card-text-font-size: 16px; | |||
$headings: ( | |||
'h1': ( | |||
'size': 3.3125rem, | |||
'line-height': 1.15em | |||
), | |||
'h2': ( | |||
'size': 2.25rem, | |||
'line-height': 1.5em | |||
), | |||
'h3': ( | |||
'size': 1.5625rem, | |||
'line-height': 1.4em | |||
), | |||
'h4': ( | |||
'size': 1.125rem, | |||
'line-height': 1.4em | |||
), | |||
'h5': ( 'size': 1.0625rem ), | |||
'h6': ( 'size': .75rem ), | |||
'subtitle-2': ( 'size': 1rem ), | |||
'overline': ( 'letter-spacing': 0 ) | |||
); |
@@ -0,0 +1,54 @@ | |||
#app-bar | |||
.v-badge__badge | |||
font-size: 9px | |||
padding: 5px 6px | |||
// ----------------------- | |||
.v-toolbar__content, | |||
.v-toolbar__extension | |||
padding: 0px 15px 0 31px | |||
.v-sheet | |||
.v-toolbar__content | |||
.v-btn.v-size--default:not(.v-btn--icon):not(.v-btn--fab), | |||
.v-btn.v-size--large:not(.v-btn--icon):not(.v-btn--fab) | |||
margin-bottom: 5px | |||
padding: 10px 15px !important | |||
.theme--light.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) | |||
background-color: #fff | |||
.v-icon | |||
color: #999 | |||
.theme--light.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) | |||
background-color: #fff | |||
margin-right: 17px | |||
margin-bottom: 2px | |||
.theme--light.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined):hover | |||
background-color: #fff | |||
.v-toolbar__content | |||
height: 75px | |||
.v-toolbar__content .v-btn--flat | |||
.v-icon | |||
margin-right: 3px | |||
.theme--light.v-label | |||
color: rgba(0, 0, 0, 0.3) | |||
.v-menu__content .v-list--nav | |||
padding: .3125rem 0 | |||
border-radius: 4px | |||
.v-list-item | |||
padding: 10px 20px | |||
margin: 0 .3125rem | |||
margin-bottom: 0px !important | |||
min-height: 40px | |||
border-radius: 2px | |||
.v-list-item__title | |||
font-weight: 400 | |||
font-size: 13px | |||
.v-navigation-drawer .v-icon.v-icon | |||
font-size: 24px |
@@ -0,0 +1 @@ | |||
@@ -0,0 +1,67 @@ | |||
.v-btn.v-size--default | |||
font-size: .85rem | |||
.v-icon.v-icon | |||
font-size: 20px | |||
.v-btn__content .v-icon--left | |||
margin-right: 4px | |||
.v-sheet button.v-btn.v-size--default:not(.v-btn--icon):not(.v-btn--fab) | |||
padding: 12px 30px !important | |||
.theme--light.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) | |||
background-color: #999 | |||
color: #fff | |||
&:hover | |||
background-color: #999 | |||
color: #fff | |||
.v-btn.white | |||
.v-btn__content | |||
color: #999 | |||
.v-sheet .v-btn.v-size--large:not(.v-btn--icon):not(.v-btn--fab) | |||
padding: 18px 36px !important | |||
.v-btn--fab.v-size--small | |||
height: 41px | |||
width: 41px | |||
.v-btn:not(.v-btn--text):not(.v-btn--outlined):hover:before | |||
opacity: 0 | |||
.v-btn:not(.v-btn--text):not(.v-btn--outlined):focus:before | |||
opacity: 0 | |||
.v-btn.v-size--default:not(.v-btn--icon):not(.v-btn--fab), | |||
.v-btn.v-size--large:not(.v-btn--icon):not(.v-btn--fab) | |||
padding: 10px 15px !important | |||
// Button group | |||
.v-item-group | |||
.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) | |||
margin-right: 0 | |||
.v-btn-toggle | |||
.v-btn | |||
opacity: 1 | |||
.v-btn-toggle > .v-btn.v-size--default | |||
height: inherit | |||
.theme--light.v-btn-toggle .v-btn.v-btn | |||
border-color: #999 !important | |||
&.primary | |||
border-color: #e91e63 !important | |||
&.secondary | |||
border-color: #9c27b0 !important | |||
&.success | |||
border-color: #4caf50 !important | |||
&.warning | |||
border-color: #fb8c00 !important | |||
&.error | |||
border-color: #ff5252 !important | |||
&.info | |||
border-color: #00cae3 !important |
@@ -0,0 +1,30 @@ | |||
#calendar | |||
.v-sheet .v-toolbar__content .theme--light.v-btn:not(.v-btn--flat):not(.v-btn--text):not(.v-btn--outlined) .v-icon | |||
color: #fff | |||
.v-calendar .v-event.v-event-end | |||
width: 100 | |||
.v-calendar-weekly__head-weekday | |||
font-weight: 700 | |||
.theme--light | |||
.v-calendar-weekly__head-weekday | |||
&.v-present | |||
color: #333 !important | |||
.v-calendar-weekly__day-label | |||
.v-btn.transparent | |||
margin-right: 0 !important | |||
.v-btn__content | |||
color: #333 !important | |||
.v-btn--fab | |||
margin-right: 0 !important | |||
.v-btn__content | |||
color: #fff !important | |||
.theme--light.v-calendar-weekly .v-calendar-weekly__head-weekday | |||
border-right: none | |||
border-bottom: #e0e0e0 1px solid | |||
.theme--light.v-calendar-weekly .v-calendar-weekly__head-weekday.v-outside | |||
background: #fff |
@@ -0,0 +1,107 @@ | |||
.v-card | |||
border-radius: 6px | |||
margin-top: 30px | |||
margin-bottom: 15px | |||
.card-title | |||
font-size: 18px | |||
.v-card--material__heading | |||
top: -30px | |||
.subtitle-1 | |||
color: hsla(0,0%,100%,.8) | |||
.display-2 | |||
font-size: 18px !important | |||
.caption | |||
font-size: 12px !important | |||
letter-spacing: 0 !important | |||
.v-card__actions | |||
padding-top: 15px | |||
.display-2 | |||
font-size: 18px !important | |||
.v-divider | |||
border-color: #eee | |||
.ct-label | |||
font-size: 14px | |||
.v-card--material-chart .v-card--material__heading .ct-label | |||
font-weight: 300 | |||
.v-btn--icon.v-size--default .v-icon, | |||
.v-btn--fab.v-size--default .v-icon | |||
font-size: 18px | |||
.v-card--material .v-image | |||
.v-image__image | |||
border-radius: 6px | |||
.v-card__title | |||
font-size: 18px | |||
padding-top: 7px | |||
padding-bottom: 2px | |||
.theme--light | |||
.v-card > .v-card__text | |||
color: #333 | |||
.card-title | |||
color: #3c4858 | |||
.theme--dark | |||
.card-title | |||
color: #fff | |||
.v-timeline-item .v-card | |||
margin-top: 0 | |||
.v-card--wizard | |||
.v-tabs-bar | |||
height: 42px | |||
.v-card__actions | |||
.v-btn | |||
margin-right: 0 !important | |||
.v-tabs .v-tab--active:hover::before, .theme--light.v-tabs .v-tab--active::before | |||
opacity: 0 | |||
.v-tabs .v-tab:hover::before | |||
opacity: 0 | |||
.v-card--plan | |||
.body-2 | |||
font-weight: 500 | |||
letter-spacing: 0 !important | |||
margin-top: 10px | |||
margin-bottom: 8px | |||
.display-2 | |||
margin-top: 30px | |||
.v-card__text | |||
color: #999 | |||
margin-bottom: 16px | |||
.v-btn | |||
margin-right: 0 !important | |||
.v-avatar | |||
margin-top: 10px | |||
.v-card--testimony | |||
.v-card__text | |||
color: #999 !important | |||
.display-2 | |||
font-size: 18px !important | |||
.body-2 | |||
font-weight: 500 | |||
font-size: 12px !important | |||
.v-avatar | |||
left: calc(50% - 50px) | |||
.ct-square:before | |||
float: none |
@@ -0,0 +1,9 @@ | |||
.v-chip.v-size--small | |||
height: 20px | |||
.v-chip__content | |||
font-size: 10px | |||
font-weight: 500 | |||
.v-chip__close | |||
font-size: 15px | |||
margin-top: -1px |
@@ -0,0 +1,29 @@ | |||
.v-footer | |||
padding: 20px 0 20px 4px | |||
border-top: 1px solid #e7e7e7 !important | |||
position: relative | |||
a | |||
padding: 15px 18px 15px 16px | |||
font-size: 12px !important | |||
.body-1 | |||
font-size: 16px !important | |||
padding-right: 18px | |||
letter-spacing: 0px !important | |||
a | |||
color: #9c27b0 !important | |||
padding: 0 | |||
text-transform: inherit !important | |||
font-size: 16px !important | |||
font-weight: 300 !important | |||
.v-icon | |||
margin-top: -3px | |||
&.v-footer--absolute | |||
position: absolute !important | |||
.theme--light.v-footer | |||
background-color: transparent | |||
.body-1 | |||
color: #3c4858 | |||
.v-icon | |||
color: #3c4858 |
@@ -0,0 +1,45 @@ | |||
.v-input--selection-controls__input | |||
margin-right: 0 | |||
.v-input--selection-controls.v-input .v-label | |||
font-weight: 400 | |||
.v-input--selection-controls__ripple | |||
left: -14px | |||
.v-input--switch .v-label | |||
margin-left: 10px | |||
.v-select-list.v-card | |||
margin: 0 | |||
border-top-left-radius: 0 | |||
border-top-right-radius: 0 | |||
.v-picker.v-card | |||
margin: 0 | |||
.theme--light.v-input:not(.v-input--is-disabled) input, .theme--light.v-input:not(.v-input--is-disabled) textarea | |||
font-size: 14px | |||
font-weight: 400 | |||
color: #495057 | |||
.v-select .v-select__selections | |||
font-weight: 400 | |||
font-size: 12px | |||
text-transform: uppercase | |||
.v-input__control .v-counter | |||
margin-top: 5px | |||
.v-application code | |||
padding: 2px 4px | |||
font-size: 90% | |||
font-weight: 400 | |||
color: #c7254e | |||
background-color: #f9f2f4 | |||
border-radius: 4px | |||
&:before, &:after | |||
content: "" | |||
.theme--light.v-text-field:not(.v-input--has-state) > .v-input__control > .v-input__slot:hover:before | |||
border-color: #d2d2d2 |
@@ -0,0 +1,3 @@ | |||
.mapouter | |||
position: relative !important | |||
height: 100vh !important |
@@ -0,0 +1,25 @@ | |||
.v-dialog | |||
.v-card | |||
margin: 0 | |||
.v-card__title | |||
font-weight: 300 | |||
font-size: 18px | |||
display: inline-block | |||
text-align: center | |||
width: 100% | |||
padding: 24px 24px 0 | |||
.v-icon | |||
position: absolute | |||
top: 15px | |||
right: 20px | |||
color: #999 | |||
opacity: .5 | |||
font-size: 16px | |||
&:hover | |||
opacity: 1 | |||
.v-dialog > .v-card > .v-card__text | |||
padding-top: 24px | |||
font-weight: 300 | |||
line-height: 1.75em | |||
letter-spacing: 0 |
@@ -0,0 +1,12 @@ | |||
.v-alert | |||
padding: 20px 15px | |||
.v-alert__wrapper | |||
.v-alert__icon | |||
height: 38px | |||
min-width: 38px | |||
.v-alert__content | |||
font-weight: 300 | |||
span | |||
font-size: 12px | |||
font-weight: 500 |
@@ -0,0 +1,28 @@ | |||
#pages | |||
.v-card--plan | |||
&.transparent | |||
.display-2 | |||
color: #fff | |||
#register | |||
.v-card | |||
.v-list-item__title | |||
font-size: 18px | |||
color: #3c4858 | |||
.v-list-item__subtitle | |||
line-height: 1.5 | |||
margin-bottom: 15px | |||
#login | |||
.v-card | |||
.v-card--material__heading | |||
.v-icon | |||
font-size: 20px | |||
.error-page | |||
h1.title | |||
font-size: 192px !important | |||
letter-spacing: 14px !important | |||
font-weight: 700 !important | |||
margin-top: 60px | |||
margin-bottom: 100px |
@@ -0,0 +1,5 @@ | |||
.v-pagination | |||
.v-pagination__item, | |||
.v-pagination__navigation | |||
&:focus | |||
outline: none |
@@ -0,0 +1,23 @@ | |||
.v-application--is-rtl | |||
.v-toolbar | |||
.v-btn | |||
.v-btn__content span | |||
width: 20px | |||
.v-toolbar__title | |||
margin-right: 15px | |||
.v-card | |||
.v-list-item__action | |||
.v-input--selection-controls__ripple | |||
left: -10px | |||
.v-timeline--align-top .v-timeline-item__body > .v-card:before | |||
right: -11px !important | |||
left: unset !important | |||
#settings | |||
.v-icon | |||
margin-right: 30px | |||
.v-footer | |||
position: relative !important |
@@ -0,0 +1,36 @@ | |||
#settings | |||
z-index: 200 | |||
.v-settings | |||
border-radius: 10px | |||
.v-card | |||
margin-top: 0 | |||
.v-card__text | |||
strong | |||
height: 30px | |||
line-height: 25px | |||
font-size: 12px | |||
font-weight: 600 | |||
text-transform: uppercase | |||
text-align: center | |||
.v-avatar | |||
border-color: #fff | |||
border-radius: 50% !important | |||
cursor: pointer | |||
display: inline-block | |||
height: 23px | |||
margin-right: 12px | |||
position: relative | |||
width: 23px | |||
padding: 8px | |||
.v-settings__item | |||
border-radius: 10px | |||
.v-image | |||
border-radius: 7px !important | |||
.v-settings__item:not(.v-settings__item--active) | |||
border-color: #fff !important | |||
.v-divider.secondary | |||
border-color: rgb(221, 221, 221) !important |
@@ -0,0 +1,79 @@ | |||
.v-application .v-navigation-drawer .v-navigation-drawer__content .v-list-item .v-list-item__content .v-list-item__title.display-2 | |||
font-size: 18px !important | |||
margin-top: 12px | |||
margin-bottom: 12px | |||
.v-application .v-navigation-drawer .v-navigation-drawer__content .v-list .v-list-group .v-list-group__header .v-list-item__content .v-list-item__title | |||
font-size: 14px | |||
font-weight: 300 | |||
.v-application--is-ltr .v-list-item__avatar:first-child | |||
margin-right: 11px | |||
.v-application .v-navigation-drawer .v-navigation-drawer__content .v-list-item__icon.v-list-group__header__append-icon .v-icon | |||
font-size: 19px | |||
.v-application--is-ltr #core-navigation-drawer div.v-list-item__icon--text, | |||
.v-application--is-ltr #core-navigation-drawer div.v-list-item__icon:first-child | |||
margin-left: 5px !important | |||
margin-right: 18px | |||
opacity: .8 | |||
.v-application--is-ltr .v-list-item__action:last-of-type:not(:only-child), | |||
.v-application--is-ltr .v-list-item__avatar:last-of-type:not(:only-child), | |||
.v-application--is-ltr .v-list-item__icon:last-of-type:not(:only-child) | |||
margin-right: 2px | |||
.v-list--nav.v-list--dense .v-list-item:not(:last-child):not(:only-child), | |||
.v-list--nav .v-list-item--dense:not(:last-child):not(:only-child), | |||
.v-list--rounded.v-list--dense .v-list-item:not(:last-child):not(:only-child), | |||
.v-list--rounded .v-list-item--dense:not(:last-child):not(:only-child) | |||
margin-bottom: 3px | |||
.v-list-item .v-list-item__title, .v-list-item .v-list-item__subtitle | |||
line-height: 1.2 | |||
font-weight: 300 | |||
font-size: 14px | |||
.v-list-group__items .v-list-item | |||
font-size: 13px | |||
margin-bottom: 5px !important | |||
.v-list-item__title | |||
font-size: 13px | |||
.v-list-item__icon | |||
margin-top: 14px | |||
.v-list-group__items .v-list-group--sub-group .v-list-group__header .v-list-item__icon--text | |||
margin-top: 15px !important | |||
.v-list-item__icon | |||
margin: 12px 0 | |||
.theme--dark.v-list-item--active:hover::before, .theme--dark.v-list-item--active::before | |||
opacity: 0 | |||
.v-navigation-drawer | |||
.v-list-item__content | |||
transition: all 0.3s linear 0s | |||
.v-list--nav | |||
padding-left: 15px | |||
padding-right: 15px | |||
.v-application .logo-mini | |||
float: left | |||
width: 30px | |||
text-align: center | |||
margin-left: 7px | |||
margin-right: 15px | |||
.theme--dark.v-navigation-drawer .v-divider | |||
background-color: rgba(181, 181, 181, 0.2) | |||
border-color: rgba(181, 181, 181, 0.1) | |||
width: calc(100% - 30px) | |||
margin-left: 15px | |||
.v-navigation-drawer--mini-variant .v-list-item > *:not(:first-child) | |||
display: block | |||
opacity: 0 |
@@ -0,0 +1,12 @@ | |||
.v-card--wizard | |||
.v-tabs-bar | |||
.v-slide-group__wrapper | |||
overflow: visible | |||
display: -webkit-inline-box | |||
contain: inherit | |||
.v-slide-group__content | |||
z-index: 2 | |||
.v-tab:not(:first-child) | |||
margin-left: 5px |
@@ -0,0 +1,22 @@ | |||
.v-data-table td | |||
font-weight: 300 | |||
padding: 12px 8px | |||
.v-data-table table thead tr th | |||
font-weight: 300 | |||
font-size: 17px | |||
padding: 0px 8px | |||
.v-data-table table tbody tr td .v-btn | |||
margin-right: 0px !important | |||
.v-data-table .v-data-table-header__sort-badge | |||
font-size: 10px | |||
.v-data-table.theme--dark | |||
tr th | |||
color: #fff !important | |||
.theme--light | |||
.v-data-table table thead tr th | |||
color: #333 |
@@ -0,0 +1,16 @@ | |||
.v-timeline-item__dot | |||
box-shadow: none !important | |||
.v-timeline-item__inner-dot .v-icon | |||
font-size: 24px | |||
.theme--light.v-timeline::before | |||
background-color: #e5e5e5 | |||
width: 3px | |||
.v-timeline-item__body | |||
.subtitle-1 | |||
font-weight: 300 | |||
font-size: 16px !important | |||
.body-2 | |||
font-size: 12px !important |
@@ -0,0 +1,4 @@ | |||
.vue-world-map | |||
max-height: 350px | |||
.land | |||
stroke: #FFF !important |
@@ -0,0 +1,23 @@ | |||
.v-content__wrap | |||
.container--fluid | |||
padding-left: 30px | |||
padding-right: 30px | |||
.v-application .headline | |||
font-size: 25px !important | |||
padding-bottom: 0 | |||
.v-application .black--text | |||
color: #333 !important | |||
.v-application .small | |||
font-weight: 300 | |||
line-height: 2rem | |||
small | |||
font-weight: 400 | |||
@media(max-width: 960px) | |||
.v-content__wrap | |||
.container--fluid | |||
padding-left: 15px | |||
padding-right: 15px |
@@ -0,0 +1,38 @@ | |||
<template> | |||
<v-app> | |||
<dashboard-core-app-bar v-model="expandOnHover" /> | |||
<v-progress-linear | |||
:active="loading" | |||
indeterminate | |||
absolute | |||
color="red" | |||
/> | |||
<dashboard-core-drawer :expand-on-hover.sync="expandOnHover" /> | |||
<dashboard-core-view /> | |||
</v-app> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DashboardIndex', | |||
components: { | |||
DashboardCoreAppBar: () => import('./components/core/AppBar'), | |||
DashboardCoreDrawer: () => import('./components/core/Drawer'), | |||
DashboardCoreView: () => import('./components/core/View') | |||
}, | |||
data: () => ({ | |||
expandOnHover: false | |||
}), | |||
computed: { | |||
loading () { | |||
return false // this.$store.state.loading | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,183 @@ | |||
<template> | |||
<v-app-bar | |||
id="app-bar" | |||
absolute | |||
app | |||
color="transparent" | |||
flat | |||
height="75" | |||
> | |||
<v-btn | |||
class="mr-3" | |||
elevation="1" | |||
fab | |||
small | |||
@click="$vuetify.breakpoint.smAndDown ? setDrawer(!drawer) : $emit('input', !value)" | |||
> | |||
<v-icon v-if="value"> | |||
mdi-view-quilt | |||
</v-icon> | |||
<v-icon v-else> | |||
mdi-dots-vertical | |||
</v-icon> | |||
</v-btn> | |||
<v-toolbar-title | |||
class="hidden-sm-and-down font-weight-light" | |||
v-text="title" | |||
/> | |||
<v-spacer /> | |||
<v-btn | |||
class="ml-2" | |||
min-width="0" | |||
text | |||
href="/#/" | |||
> | |||
<v-icon>mdi-view-dashboard</v-icon> | |||
</v-btn> | |||
<v-menu | |||
bottom | |||
left | |||
offset-y | |||
origin="top right" | |||
transition="scale-transition" | |||
> | |||
<template #activator="{ attrs, on }"> | |||
<v-btn | |||
class="ml-2" | |||
min-width="0" | |||
text | |||
v-bind="attrs" | |||
v-on="on" | |||
> | |||
<v-badge | |||
color="rgb(255, 4, 29)" | |||
overlap | |||
bordered | |||
> | |||
<template #badge> | |||
<span>{{ messages.length }}</span> | |||
</template> | |||
<v-icon>mdi-bell</v-icon> | |||
</v-badge> | |||
</v-btn> | |||
</template> | |||
<v-list | |||
:tile="false" | |||
nav | |||
> | |||
<div> | |||
<app-bar-item | |||
v-for="(n, i) in messages" | |||
:key="`item-${i}`" | |||
> | |||
<v-list-item-title v-text="n.message" /> | |||
</app-bar-item> | |||
</div> | |||
</v-list> | |||
</v-menu> | |||
<v-btn | |||
v-if="!profile._id" | |||
class="ml-2" | |||
min-width="0" | |||
text | |||
to="/pages/login" | |||
> | |||
<v-icon>far fa-user</v-icon> | |||
</v-btn> | |||
<v-menu | |||
v-else | |||
bottom | |||
left | |||
min-width="200" | |||
offset-y | |||
origin="top right" | |||
transition="scale-transition" | |||
open-on-hover | |||
> | |||
<template #activator="{ attrs, on }"> | |||
<v-btn | |||
class="ml-2" | |||
min-width="0" | |||
text | |||
v-bind="attrs" | |||
v-on="on" | |||
> | |||
<v-icon>fas fa-user</v-icon> | |||
</v-btn> | |||
</template> | |||
<v-list | |||
:tile="false" | |||
flat | |||
nav | |||
> | |||
<v-hover v-slot="{ hover }"> | |||
<v-list-item | |||
to="/profile" | |||
:class="{red: hover}" | |||
> | |||
Profil | |||
</v-list-item> | |||
</v-hover> | |||
<v-divider class="mb-2 mt-2" /> | |||
<v-hover v-slot="{ hover }"> | |||
<v-list-item | |||
:class="{red: hover}" | |||
@click="logout" | |||
> | |||
Logout | |||
</v-list-item> | |||
</v-hover> | |||
</v-list> | |||
</v-menu> | |||
</v-app-bar> | |||
</template> | |||
<script> | |||
import { mapState, mapMutations, mapGetters } from 'vuex' | |||
// import { useGraphQL } from '@/plugins/useGraphQL' | |||
export default { | |||
name: 'DashboardCoreAppBar', | |||
props: { | |||
value: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
setup (props, context) { | |||
// return { ...useGraphQL(props, context) } | |||
}, | |||
computed: { | |||
...mapState(['drawer', 'messages']), | |||
...mapGetters(['profile']), | |||
title () { | |||
if (this.$route.name) { | |||
return this.$route.name | |||
} else { | |||
return this.$store.getters.getTitle | |||
} | |||
} | |||
}, | |||
methods: { | |||
...mapMutations({ | |||
setDrawer: 'SET_DRAWER' | |||
}), | |||
logout () { | |||
// this.login('LOGOUT', 'LOGOUT') | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,151 @@ | |||
<template> | |||
<v-navigation-drawer | |||
id="core-navigation-drawer" | |||
v-model="drawer" | |||
dark | |||
:expand-on-hover="expandOnHover" | |||
mobile-breakpoint="960" | |||
app | |||
mini-variant-width="80" | |||
width="260" | |||
v-bind="$attrs" | |||
> | |||
<template #img="props"> | |||
<v-img | |||
:gradient="`150deg, rgba(192, 4, 29, .8), rgba(0, 0, 0, .8) 50%`" | |||
v-bind="props" | |||
/> | |||
</template> | |||
<v-list-item two-line> | |||
<v-list-item-content> | |||
<v-list-item-title class="text-uppercase font-weight-regular display-2"> | |||
<span class="logo-normal">Turnen auf Zeit</span> | |||
</v-list-item-title> | |||
</v-list-item-content> | |||
</v-list-item> | |||
<v-divider class="mb-2" /> | |||
<v-list | |||
expand | |||
nav | |||
> | |||
<!-- Style cascading bug --> | |||
<!-- https://github.com/vuetifyjs/vuetify/pull/8574 --> | |||
<div /> | |||
<mainmenu /> | |||
<!-- Style cascading bug --> | |||
<!-- https://github.com/vuetifyjs/vuetify/pull/8574 --> | |||
<div /> | |||
</v-list> | |||
</v-navigation-drawer> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DashboardCoreDrawer', | |||
components: { | |||
Mainmenu: () => import('./Menu') | |||
}, | |||
props: { | |||
expandOnHover: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
data: () => ({ | |||
HaupteventList: [], | |||
publicPath: process.env.BASE_URL | |||
}), | |||
computed: { | |||
drawer: { | |||
get () { | |||
return this.$store.state.drawer | |||
}, | |||
set (val) { | |||
this.$store.commit('SET_DRAWER', val) | |||
} | |||
}, | |||
menus () { | |||
return this.HaupteventList.slice().sort((a, b) => a.beginn > b.beginn ? -1 : 1) | |||
} | |||
}, | |||
watch: { | |||
'$vuetify.breakpoint.smAndDown' (val) { | |||
this.$emit('update:expandOnHover', !val) | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="sass"> | |||
@import '../../../../node_modules/vuetify/src/styles/tools/rtl' | |||
#core-navigation-drawer | |||
&.v-navigation-drawer--mini-variant | |||
.v-list-item | |||
justify-content: flex-start !important | |||
.v-list-group--sub-group | |||
display: block !important | |||
.v-list-group__header.v-list-item--active:before | |||
opacity: .24 | |||
.v-list-item | |||
&__icon--text, | |||
&__icon:first-child | |||
justify-content: center | |||
text-align: center | |||
width: 20px | |||
+ltr() | |||
margin-right: 24px | |||
margin-left: 12px !important | |||
+rtl() | |||
margin-left: 24px | |||
margin-right: 12px !important | |||
.v-list--dense | |||
.v-list-item | |||
&__icon--text, | |||
&__icon:first-child | |||
margin-top: 10px | |||
.v-list-group--sub-group | |||
.v-list-item | |||
+ltr() | |||
padding-left: 8px | |||
+rtl() | |||
padding-right: 8px | |||
.v-list-group__header | |||
+ltr() | |||
padding-right: 0 | |||
+rtl() | |||
padding-right: 0 | |||
.v-list-item__icon--text | |||
margin-top: 19px | |||
order: 0 | |||
.v-list-group__header__prepend-icon | |||
order: 2 | |||
+ltr() | |||
margin-right: 8px | |||
+rtl() | |||
margin-left: 8px | |||
</style> |
@@ -0,0 +1,69 @@ | |||
<template> | |||
<v-footer | |||
id="dashboard-core-footer" | |||
> | |||
<v-container> | |||
<v-row | |||
align="center" | |||
no-gutters | |||
> | |||
<v-col | |||
v-for="(link, i) in links" | |||
:key="i" | |||
class="text-center mb-sm-0 mb-5" | |||
cols="auto" | |||
> | |||
<a | |||
:href="'/#'+link.to" | |||
class="mr-0 grey--text text--darken-3" | |||
rel="noopener" | |||
v-text="link.text" | |||
/> | |||
</v-col> | |||
<v-spacer class="hidden-sm-and-down" /> | |||
<v-col | |||
cols="12" | |||
md="auto" | |||
> | |||
<div class="body-1 font-weight-light pt-6 pt-md-0 text-center"> | |||
© 2020 IT Kimmig | |||
</div> | |||
</v-col> | |||
</v-row> | |||
</v-container> | |||
</v-footer> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DashboardCoreFooter', | |||
data: () => ({ | |||
links: [ | |||
{ | |||
to: '/', | |||
text: 'Startseite' | |||
}, | |||
{ | |||
to: '/pages/impress', | |||
text: 'Impressum' | |||
}, | |||
{ | |||
to: '/pages/privacy', | |||
text: 'Datenschutz' | |||
} | |||
] | |||
}) | |||
} | |||
</script> | |||
<style lang="sass"> | |||
#dashboard-core-footer | |||
a | |||
font-size: .825rem | |||
font-weight: 500 | |||
text-decoration: none | |||
text-transform: uppercase | |||
</style> |
@@ -0,0 +1,77 @@ | |||
<template> | |||
<div> | |||
<base-item | |||
:item="{ | |||
icon:'mdi-view-dashboard', | |||
title: 'Startseite', | |||
to: '/' | |||
}" | |||
/> | |||
<base-item-group | |||
v-if="isMaster" | |||
:item="{ | |||
group: '/management', | |||
icon: 'fa-user-crown', | |||
title: 'Admin', | |||
children: [ | |||
{ | |||
title: 'Wettkampforte', | |||
to: 'places', | |||
icon: 'mdi-home-group', | |||
}, | |||
{ | |||
title: 'Hauptevents', | |||
to: 'events', | |||
icon: 'mdi-calendar-multiple', | |||
}, | |||
{ | |||
title: 'Vereine', | |||
to: 'clubs', | |||
icon: 'mdi-account-supervisor-circle', | |||
}, | |||
{ | |||
title: 'Disziplinen', | |||
to: 'disciplines', | |||
icon: 'fa-dumbbell', | |||
}, | |||
{ | |||
title: 'Personen verwalten', | |||
to: 'people', | |||
icon: 'mdi-account-edit', | |||
}, | |||
{ | |||
title: 'Personen zusammenführen', | |||
to: 'merge', | |||
icon: 'mdi-account-switch', | |||
}, | |||
{ | |||
title: 'Turnportalabfrage', | |||
to: 'turnportal', | |||
icon: 'mdi-account-question', | |||
}, | |||
{ | |||
title: 'Serverübersicht', | |||
to: 'server', | |||
icon: 'mdi-server', | |||
}, | |||
], | |||
}" | |||
/> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapGetters } from 'vuex' | |||
export default { | |||
name: 'Menu', | |||
computed: { | |||
...mapGetters(['profile', 'isMaster']) | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,17 @@ | |||
<template> | |||
<v-main> | |||
<router-view /> | |||
<dashboard-core-footer /> | |||
</v-main> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DashboardCoreView', | |||
components: { | |||
DashboardCoreFooter: () => import('./Footer.vue') | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,40 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "esnext", | |||
"module": "esnext", | |||
"strict": true, | |||
"jsx": "preserve", | |||
"importHelpers": true, | |||
"moduleResolution": "node", | |||
"experimentalDecorators": true, | |||
"skipLibCheck": true, | |||
"esModuleInterop": true, | |||
"allowSyntheticDefaultImports": true, | |||
"sourceMap": true, | |||
"baseUrl": ".", | |||
"types": [ | |||
"webpack-env" | |||
], | |||
"paths": { | |||
"@/*": [ | |||
"src/*" | |||
] | |||
}, | |||
"lib": [ | |||
"esnext", | |||
"dom", | |||
"dom.iterable", | |||
"scripthost" | |||
] | |||
}, | |||
"include": [ | |||
"src/**/*.ts", | |||
"src/**/*.tsx", | |||
"src/**/*.vue", | |||
"tests/**/*.ts", | |||
"tests/**/*.tsx" | |||
], | |||
"exclude": [ | |||
"node_modules" | |||
] | |||
} |
@@ -0,0 +1,5 @@ | |||
module.exports = { | |||
transpileDependencies: [ | |||
'vuetify' | |||
] | |||
} |
@@ -0,0 +1,23 @@ | |||
.DS_Store | |||
node_modules | |||
/dist | |||
# local env files | |||
.env.local | |||
.env.*.local | |||
# Log files | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
pnpm-debug.log* | |||
# Editor directories and files | |||
.idea | |||
.vscode | |||
*.suo | |||
*.ntvs* | |||
*.njsproj | |||
*.sln | |||
*.sw? |
@@ -0,0 +1,15 @@ | |||
{ | |||
"name": "Untitled GraphQL Schema", | |||
"schemaPath": "./path/to/schema.graphql", | |||
"extensions": { | |||
"endpoints": { | |||
"Default GraphQL Endpoint": { | |||
"url": "http://localhost:3000/gql", | |||
"headers": { | |||
"user-agent": "JS GraphQL" | |||
}, | |||
"introspect": false | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
# client | |||
## Project setup | |||
``` | |||
npm install | |||
``` | |||
### Compiles and hot-reloads for development | |||
``` | |||
npm run serve | |||
``` | |||
### Compiles and minifies for production | |||
``` | |||
npm run build | |||
``` | |||
### Lints and fixes files | |||
``` | |||
npm run lint | |||
``` | |||
### Customize configuration | |||
See [Configuration Reference](https://cli.vuejs.org/config/). |
@@ -0,0 +1,5 @@ | |||
module.exports = { | |||
presets: [ | |||
'@vue/cli-plugin-babel/preset' | |||
] | |||
} |
@@ -0,0 +1,57 @@ | |||
{ | |||
"name": "client", | |||
"version": "0.1.0", | |||
"private": true, | |||
"scripts": { | |||
"serve": "vue-cli-service serve", | |||
"build": "vue-cli-service build", | |||
"lint": "vue-cli-service lint" | |||
}, | |||
"dependencies": { | |||
"@fortawesome/fontawesome-pro": "^5.15.3", | |||
"@mdi/font": "5.9.55", | |||
"apollo-boost": "^0.4.9", | |||
"apollo-cache-inmemory": "^1.6.6", | |||
"core-js": "^3.12.1", | |||
"graphql": "^15.5.0", | |||
"js-cookie": "^2.2.1", | |||
"roboto-fontface": "*", | |||
"subscriptions-transport-ws": "^0.9.18", | |||
"vue": "^3.0.0", | |||
"vue-apollo": "^3.0.7", | |||
"vue3-cookies": "^1.0.1", | |||
"vuetify": "^3.0.0-alpha.0", | |||
"vuex": "^4.0.0" | |||
}, | |||
"devDependencies": { | |||
"@vue/cli-plugin-babel": "~4.5.13", | |||
"@vue/cli-plugin-eslint": "~4.5.13", | |||
"@vue/cli-service": "~4.5.13", | |||
"@vue/compiler-sfc": "^3.0.11", | |||
"babel-eslint": "^10.1.0", | |||
"eslint": "^6.7.2", | |||
"eslint-plugin-vue": "^7.9.0", | |||
"sass": "^1.32.0", | |||
"sass-loader": "^10.0.0", | |||
"vue-cli-plugin-vuetify": "~2.4.0" | |||
}, | |||
"eslintConfig": { | |||
"root": true, | |||
"env": { | |||
"node": true | |||
}, | |||
"extends": [ | |||
"plugin:vue/vue3-essential", | |||
"eslint:recommended" | |||
], | |||
"parserOptions": { | |||
"parser": "babel-eslint" | |||
}, | |||
"rules": {} | |||
}, | |||
"browserslist": [ | |||
"> 1%", | |||
"last 2 versions", | |||
"not dead" | |||
] | |||
} |
@@ -0,0 +1,17 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> | |||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> | |||
<title><%= htmlWebpackPlugin.options.title %></title> | |||
</head> | |||
<body> | |||
<noscript> | |||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> | |||
</noscript> | |||
<div id="app"></div> | |||
<!-- built files will be auto injected --> | |||
</body> | |||
</html> |
@@ -0,0 +1,110 @@ | |||
<template> | |||
<div> | |||
<!--<router-view />--> | |||
<div | |||
v-if="!connected" | |||
id="loader" | |||
> | |||
<div> | |||
<p>Verbinde zu Server...</p> | |||
</div> | |||
</div> | |||
<!--<confirm ref="confirm" /> | |||
<v-snackbar | |||
v-model="error_visible" | |||
class="error_snack" | |||
:timeout="5000" | |||
top | |||
> | |||
{{ error_text }} | |||
<template #action="{ attrs }"> | |||
<v-btn | |||
dark | |||
text | |||
v-bind="attrs" | |||
@click="error_visible = false" | |||
> | |||
Schließen | |||
</v-btn> | |||
</template> | |||
</v-snackbar>--> | |||
Test: {{ connected ? 'connected' : 'not connected' }} | |||
</div> | |||
</template> | |||
<script> | |||
import { useGraphQL } from '@/plugins/graphql'; | |||
export default { | |||
name: 'App', | |||
setup () { | |||
return { | |||
...useGraphQL() | |||
} | |||
}, | |||
computed: { | |||
}, | |||
mounted () { | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
#loader { | |||
position: fixed; | |||
top: 0; | |||
bottom: 0; | |||
left: 0; | |||
right: 0; | |||
width: 100vw; | |||
height: 100vh; | |||
z-index: 9999; | |||
background: rgba(255, 255, 255, 0.8); | |||
text-align: center; | |||
} | |||
#loader > div { | |||
height: 100vh; | |||
width: 100vw; | |||
display: table-cell; | |||
vertical-align: middle; | |||
text-align: center; | |||
} | |||
#loader > div > p { | |||
font-family: "Roboto", sans-serif; | |||
margin-top: 16px; | |||
} | |||
</style> | |||
<style> | |||
.changed { | |||
color: rgb(255, 4, 29); | |||
} | |||
.changefade { | |||
color: inherit; | |||
transition: color 0.8s; | |||
} | |||
.trhover:hover { | |||
cursor: pointer; | |||
} | |||
.center_switch > div > div > div { | |||
margin: auto !important; | |||
} | |||
.error_snack .v-snack__wrapper { | |||
font-family: "Roboto", sans-serif; | |||
background-color: red !important; | |||
} | |||
.row-clickable tbody tr:hover { | |||
cursor: pointer; | |||
} | |||
</style> |
@@ -0,0 +1 @@ | |||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg> |
@@ -0,0 +1,107 @@ | |||
<template> | |||
<v-dialog | |||
v-model="dialog" | |||
:max-width="options.width" | |||
:style="{ zIndex: options.zIndex }" | |||
@keydown.esc="cancel" | |||
> | |||
<v-card> | |||
<v-toolbar | |||
dark | |||
:color="options.color" | |||
dense | |||
flat | |||
> | |||
<v-toolbar-title class="white--text"> | |||
{{ title }} | |||
</v-toolbar-title> | |||
</v-toolbar> | |||
<v-card-text | |||
v-show="!!message" | |||
class="pa-4" | |||
> | |||
{{ message }} | |||
</v-card-text> | |||
<v-card-actions class="pt-0"> | |||
<v-spacer /> | |||
<v-btn | |||
color="primary darken-1" | |||
@click.native="agree" | |||
> | |||
Ja | |||
</v-btn> | |||
<v-btn | |||
color="grey" | |||
@click.native="cancel" | |||
> | |||
Abbrechen | |||
</v-btn> | |||
</v-card-actions> | |||
</v-card> | |||
</v-dialog> | |||
</template> | |||
<script> | |||
/** | |||
* Vuetify Confirm Dialog component | |||
* | |||
* Insert component where you want to use it: | |||
* <confirm ref="confirm"></confirm> | |||
* | |||
* Call it: | |||
* this.$refs.confirm.open('Delete', 'Are you sure?', { color: 'red' }).then((confirm) => {}) | |||
* Or use await: | |||
* if (await this.$refs.confirm.open('Delete', 'Are you sure?', { color: 'red' })) { | |||
* // yes | |||
* } | |||
* else { | |||
* // cancel | |||
* } | |||
* | |||
* Alternatively you can place it in main App component and access it globally via this.$root.$confirm | |||
* <template> | |||
* <v-app> | |||
* ... | |||
* <confirm ref="confirm"></confirm> | |||
* </v-app> | |||
* </template> | |||
* | |||
* mounted() { | |||
* this.$root.$confirm = this.$refs.confirm.open | |||
* } | |||
*/ | |||
export default { | |||
data: () => ({ | |||
dialog: false, | |||
resolve: null, | |||
reject: null, | |||
message: null, | |||
title: null, | |||
options: { | |||
color: 'primary', | |||
width: 290, | |||
zIndex: 200, | |||
}, | |||
}), | |||
methods: { | |||
open (title, message, options) { | |||
this.dialog = true | |||
this.title = title | |||
this.message = message | |||
this.options = Object.assign(this.options, options) | |||
return new Promise((resolve, reject) => { | |||
this.resolve = resolve | |||
this.reject = reject | |||
}) | |||
}, | |||
agree () { | |||
this.resolve(true) | |||
this.dialog = false | |||
}, | |||
cancel () { | |||
this.resolve(false) | |||
this.dialog = false | |||
}, | |||
}, | |||
} | |||
</script> |
@@ -0,0 +1,90 @@ | |||
<template> | |||
<v-menu | |||
v-model="show" | |||
:close-on-content-click="false" | |||
:nudge-right="40" | |||
transition="scale-transition" | |||
offset-y | |||
min-width="290px" | |||
> | |||
<template #activator="{ on }"> | |||
<v-text-field | |||
v-model="dateFormatted" | |||
:label="label" | |||
:clearable="clearable" | |||
prepend-icon="mdi-calendar" | |||
readonly | |||
v-on="on" | |||
/> | |||
</template> | |||
<v-date-picker | |||
v-model="date" | |||
:first-day-of-week="1" | |||
@input="show = false" | |||
/> | |||
</v-menu> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DateSelector', | |||
props: { | |||
value: { | |||
type: String, | |||
default: '', | |||
}, | |||
label: { | |||
type: String, | |||
default: '', | |||
}, | |||
clearable: { | |||
type: Boolean, | |||
required: false, | |||
default: false, | |||
}, | |||
}, | |||
data: () => ({ | |||
show: false, | |||
}), | |||
computed: { | |||
dateFormatted: { | |||
get () { | |||
return this.formatDate(this.value) | |||
}, | |||
set (val) { | |||
this.$emit('input', this.parseDate(val)) | |||
}, | |||
}, | |||
date: { | |||
get () { | |||
return this.value | |||
}, | |||
set (val) { | |||
this.$emit('input', val) | |||
}, | |||
}, | |||
}, | |||
methods: { | |||
formatDate (date) { | |||
if (!date) return null | |||
const [year, month, day] = date.split('-') | |||
return `${day}.${month}.${year}` | |||
}, | |||
parseDate (date) { | |||
if (!date) return null | |||
const [day, month, year] = date.split('/') | |||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,139 @@ | |||
<template> | |||
<v-row> | |||
<v-col cols="6"> | |||
<v-menu | |||
v-model="showdate" | |||
:close-on-content-click="false" | |||
:nudge-right="40" | |||
transition="scale-transition" | |||
offset-y | |||
min-width="290px" | |||
> | |||
<template #activator="{ on }"> | |||
<v-text-field | |||
:value="dateFormatted" | |||
:label="`${label}-Datum`" | |||
:disabled="disabled" | |||
prepend-icon="far fa-calendar-day" | |||
readonly | |||
v-on="on" | |||
/> | |||
</template> | |||
<v-date-picker | |||
v-model="date" | |||
:first-day-of-week="1" | |||
@input="showdate = false" | |||
/> | |||
</v-menu> | |||
</v-col> | |||
<v-col cols="6"> | |||
<v-menu | |||
v-model="showtime" | |||
:close-on-content-click="false" | |||
:nudge-right="40" | |||
transition="scale-transition" | |||
offset-y | |||
min-width="290px" | |||
> | |||
<template #activator="{ on }"> | |||
<v-text-field | |||
:value="timeFormatted" | |||
:label="`${label}-Uhrzeit`" | |||
:disabled="disabled" | |||
prepend-icon="far fa-clock" | |||
readonly | |||
v-on="on" | |||
/> | |||
</template> | |||
<v-time-picker | |||
v-model="time" | |||
format="24hr" | |||
:allowed-minutes="m => m % 5 === 0" | |||
@input="showtime = false" | |||
/> | |||
</v-menu> | |||
</v-col> | |||
</v-row> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'DateSelector', | |||
props: { | |||
value: { | |||
type: String, | |||
default: '', | |||
}, | |||
label: { | |||
type: String, | |||
default: '', | |||
}, | |||
disabled: { | |||
type: Boolean, | |||
default: false, | |||
}, | |||
}, | |||
data: () => ({ | |||
showdate: false, | |||
showtime: false, | |||
}), | |||
computed: { | |||
_date () { | |||
return (this.value?.split(' ') || [])[0] || '' | |||
}, | |||
_time () { | |||
return (this.value?.split(' ') || [])[1] || '' | |||
}, | |||
dateFormatted () { | |||
return this.formatDate(this._date) | |||
}, | |||
timeFormatted () { | |||
return this.formatTime(this._time) | |||
}, | |||
date: { | |||
get () { | |||
return this._date | |||
}, | |||
set (val) { | |||
this.$emit('input', `${val} ${this._time}`) | |||
}, | |||
}, | |||
time: { | |||
get () { | |||
return this._time | |||
}, | |||
set (val) { | |||
this.$emit('input', `${this._date} ${val}:00`) | |||
}, | |||
}, | |||
}, | |||
methods: { | |||
formatDate (date) { | |||
if (!date) return null | |||
const [year, month, day] = date.split('-') | |||
return `${day}.${month}.${year}` | |||
}, | |||
formatTime (time) { | |||
if (!time) return null | |||
const [hour, minute] = time.split(':') | |||
return `${hour}:${minute}` | |||
}, | |||
parseDate (date) { | |||
if (!date) return null | |||
const [day, month, year] = date.split('/') | |||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,20 @@ | |||
<template> | |||
<div> | |||
T2: {{ get('pub_person','b77038e7-9f17-4ef2-951c-958aeba8a5b5') }} | |||
</div> | |||
</template> | |||
<script> | |||
import { getData } from '../plugins/useSDB' | |||
export default { | |||
name: 'Demo', | |||
setup () { | |||
return { ...getData() } | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,9 @@ | |||
<script> | |||
import { VCard } from 'vuetify/lib' | |||
export default { | |||
name: 'Card', | |||
extends: VCard, | |||
} | |||
</script> |
@@ -0,0 +1,77 @@ | |||
<template> | |||
<v-edit-dialog | |||
v-model:return-value="wert" | |||
large | |||
persistent | |||
:save-text="savebutton" | |||
:cancel-text="cancelbutton" | |||
@open="open" | |||
@save="save" | |||
@close="close" | |||
> | |||
<div style="min-width:40px;"> | |||
{{ value }} | |||
</div> | |||
<template #input> | |||
<v-text-field | |||
v-model="wert" | |||
:label="label" | |||
autofocus | |||
/> | |||
</template> | |||
</v-edit-dialog> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'EditDialog', | |||
props: { | |||
value: { | |||
type: [String, Number], | |||
required: false, | |||
default: '', | |||
}, | |||
label: { | |||
type: String, | |||
required: false, | |||
default: '', | |||
}, | |||
savebutton: { | |||
type: String, | |||
required: false, | |||
default: 'Übernehmen', | |||
}, | |||
cancelbutton: { | |||
type: String, | |||
required: false, | |||
default: 'Abbrechen', | |||
}, | |||
}, | |||
data: () => ({ | |||
wert: null, | |||
}), | |||
methods: { | |||
open () { | |||
if (this.value === null || this.value === undefined) { | |||
this.wert = '' | |||
} else { | |||
this.wert = `${this.value}` | |||
} | |||
}, | |||
close () { | |||
this.wert = null | |||
}, | |||
save () { | |||
this.$emit('input', this.wert) | |||
this.wert = null | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,76 @@ | |||
<template> | |||
<v-edit-dialog | |||
v-model:return-value="wert" | |||
large | |||
persistent | |||
:save-text="savebutton" | |||
cancel-text="Abbrechen" | |||
@open="open" | |||
@save="save" | |||
@close="close" | |||
> | |||
<div>{{ value | dateformat(dateformat) }}</div> | |||
<template #input> | |||
<date-selector | |||
v-model="wert" | |||
:label="label" | |||
:clearable="clearable" | |||
/> | |||
</template> | |||
</v-edit-dialog> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'EditDialog', | |||
props: { | |||
value: { | |||
type: String, | |||
required: false, | |||
default: undefined, | |||
}, | |||
label: { | |||
type: String, | |||
required: false, | |||
default: '', | |||
}, | |||
savebutton: { | |||
type: String, | |||
required: false, | |||
default: 'Übernehmen', | |||
}, | |||
dateformat: { | |||
type: String, | |||
required: false, | |||
default: null, | |||
}, | |||
clearable: { | |||
type: Boolean, | |||
required: false, | |||
default: false, | |||
}, | |||
}, | |||
data: () => ({ | |||
wert: null, | |||
}), | |||
methods: { | |||
open () { | |||
this.wert = this.value | |||
}, | |||
close () { | |||
this.wert = null | |||
}, | |||
save () { | |||
this.$emit('input', this.wert) | |||
this.wert = null | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,85 @@ | |||
<template> | |||
<v-edit-dialog | |||
v-model:return-value="wert" | |||
large | |||
persistent | |||
:save-text="savebutton" | |||
cancel-text="Abbrechen" | |||
@open="open" | |||
@save="save" | |||
@close="close" | |||
> | |||
<div>{{ text }}</div> | |||
<template #input> | |||
<v-select | |||
v-model="wert" | |||
:label="label" | |||
:items="myitems" | |||
autofocus | |||
clearable | |||
/> | |||
</template> | |||
</v-edit-dialog> | |||
</template> | |||
<script> | |||
import { enumtype } from '@/enumtype' | |||
export default { | |||
name: 'EditDialogSelect', | |||
props: { | |||
value: { | |||
type: String, | |||
required: false, | |||
default: undefined, | |||
}, | |||
label: { | |||
type: String, | |||
required: false, | |||
default: '', | |||
}, | |||
savebutton: { | |||
type: String, | |||
required: false, | |||
default: 'Übernehmen', | |||
}, | |||
items: { | |||
type: undefined, | |||
required: true, | |||
}, | |||
}, | |||
data: () => ({ | |||
wert: null, | |||
}), | |||
computed: { | |||
text () { | |||
return this.myitems?.find(e => e.value === this.value)?.text | |||
}, | |||
myitems () { | |||
if (Array.isArray(this.items)) return this.items | |||
else if (typeof enumtype[this.items] === 'function') return enumtype[this.items]() | |||
return [] | |||
}, | |||
}, | |||
methods: { | |||
open () { | |||
this.wert = this.value | |||
}, | |||
close () { | |||
this.wert = null | |||
}, | |||
save () { | |||
this.$emit('input', this.wert) | |||
this.wert = null | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,69 @@ | |||
<template> | |||
<v-list-item | |||
:href="href" | |||
:rel="href && href !== '#' ? 'noopener' : undefined" | |||
:target="href && href !== '#' ? '_blank' : undefined" | |||
:to="item.to" | |||
:active-class="`primary ${!isDark ? 'black' : 'white'}--text`" | |||
> | |||
<v-list-item-icon v-if="item.icon"> | |||
<v-icon v-text="item.icon" /> | |||
</v-list-item-icon> | |||
<v-list-item-icon | |||
v-else-if="text" | |||
class="v-list-item__icon--text" | |||
v-text="computedText" | |||
/> | |||
<v-list-item-content v-if="item.title || item.subtitle"> | |||
<v-list-item-title v-text="item.title" /> | |||
<v-list-item-subtitle v-text="item.subtitle" /> | |||
</v-list-item-content> | |||
</v-list-item> | |||
</template> | |||
<script> | |||
import Themeable from 'vuetify/lib/mixins/themeable' | |||
export default { | |||
name: 'Item', | |||
mixins: [Themeable], | |||
props: { | |||
item: { | |||
type: Object, | |||
default: () => ({ | |||
href: undefined, | |||
icon: undefined, | |||
subtitle: undefined, | |||
title: undefined, | |||
to: undefined, | |||
}), | |||
}, | |||
text: { | |||
type: Boolean, | |||
default: false, | |||
}, | |||
}, | |||
computed: { | |||
computedText () { | |||
if (!this.item || !this.item.title) return '' | |||
let text = '' | |||
this.item.title.split(' ').forEach(val => { | |||
text += val.substring(0, 1) | |||
}) | |||
return text | |||
}, | |||
href () { | |||
return this.item.href || (!this.item.to ? '#' : undefined) | |||
}, | |||
}, | |||
} | |||
</script> |
@@ -0,0 +1,142 @@ | |||
<template> | |||
<v-list-group | |||
:group="group" | |||
:prepend-icon="item.icon" | |||
:sub-group="subGroup" | |||
append-icon="mdi-menu-down" | |||
:color="barColor !== 'rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.7)' ? 'white' : 'grey darken-1'" | |||
> | |||
<template #activator> | |||
<v-list-item-icon | |||
v-if="text" | |||
class="v-list-item__icon--text" | |||
v-text="computedText" | |||
/> | |||
<v-list-item-avatar | |||
v-else-if="item.avatar" | |||
class="align-self-center" | |||
color="grey" | |||
> | |||
<v-img src="https://demos.creative-tim.com/material-dashboard-pro/assets/img/faces/avatar.jpg" /> | |||
</v-list-item-avatar> | |||
<v-list-item-content> | |||
<v-list-item-title v-text="item.title" /> | |||
<v-tooltip bottom> | |||
<template #activator="{ on }"> | |||
<v-list-item-subtitle | |||
v-on="on" | |||
v-text="item.subtitle" | |||
/> | |||
</template> | |||
<span>{{ item.subtitle }}</span> | |||
</v-tooltip> | |||
</v-list-item-content> | |||
</template> | |||
<template v-for="(child, i) in children"> | |||
<base-item-sub-group | |||
v-if="child.children" | |||
:key="`sub-group-${i}`" | |||
:item="buildChild(child)" | |||
/> | |||
<base-item | |||
v-else | |||
:key="`item-${i}`" | |||
:item="child" | |||
text | |||
/> | |||
</template> | |||
</v-list-group> | |||
</template> | |||
<script> | |||
// Utilities | |||
import kebabCase from 'lodash/kebabCase' | |||
import { mapState } from 'vuex' | |||
export default { | |||
name: 'ItemGroup', | |||
inheritAttrs: false, | |||
props: { | |||
item: { | |||
type: Object, | |||
default: () => ({ | |||
avatar: undefined, | |||
group: undefined, | |||
title: undefined, | |||
children: [], | |||
}), | |||
}, | |||
subGroup: { | |||
type: Boolean, | |||
default: false, | |||
}, | |||
text: { | |||
type: Boolean, | |||
default: false, | |||
}, | |||
}, | |||
computed: { | |||
...mapState(['barColor']), | |||
children () { | |||
return this.item.children.map(item => ({ | |||
...item, | |||
to: !item.to ? undefined : `${this.item.group}/${item.to}`, | |||
})) | |||
}, | |||
computedText () { | |||
if (!this.item || !this.item.title) return '' | |||
let text = '' | |||
this.item.title.split(' ').forEach(val => { | |||
text += val.substring(0, 1) | |||
}) | |||
return text | |||
}, | |||
group () { | |||
return this.genGroup(this.item.children) | |||
}, | |||
}, | |||
methods: { | |||
genGroup (children, parentgroup) { | |||
if (!parentgroup) { | |||
parentgroup = this.item.group | |||
} | |||
return children | |||
.filter(item => item.to || item.group) | |||
.map(item => { | |||
let group = `${parentgroup}/${kebabCase(item.to)}` | |||
if (item.children) { | |||
group = `${group}|${this.genGroup(item.children, `${parentgroup}/${item.group}`)}` | |||
} | |||
return group | |||
}).join('|') | |||
}, | |||
buildChild (child) { | |||
return { ...child, group: `${this.item.group}/${child.group}` } | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style> | |||
.v-list-group__activator p { | |||
margin-bottom: 0; | |||
} | |||
.v-list-group .v-list-group__items { | |||
padding-left: 3px; | |||
border-left: 1px dashed red; | |||
} | |||
</style> |