This commit is contained in:
Troplo 2020-09-17 23:29:44 +10:00
parent fe01c13562
commit d92e924139
99 changed files with 27329 additions and 76 deletions

5
admin/.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

21
admin/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.DS_Store
node_modules
dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

21
admin/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2020 JustBoil.me (https://justboil.me)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

109
admin/README.md Normal file
View File

@ -0,0 +1,109 @@
# [Admin One — Free Vue Bulma Dashboard](https://justboil.me/bulma-admin-template/one)
[![version](https://img.shields.io/github/v/release/vikdiesel/admin-one-vue-bulma-dashboard)](https://justboil.me/bulma-admin-template/one) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://justboil.me/bulma-admin-template/one)
[![Vue.js Bulma free admin dashboard](https://justboil.me/images/one/repository-preview-hi-res.png)](https://vikdiesel.github.io/admin-one-vue-bulma-dashboard/)
**Admin One** is simple, beautiful and free Vue.js Buefy Bulma admin dashboard (SPA/PWA).
* Free under MIT License
* PWA/SPA — Single page app
* Built with Bulma, Vue.js, Buefy & Vue CLI
* SCSS sources with variables
* [Premium version](https://justboil.me/bulma-admin-template/one) available
## Table of Contents
* [Other versions](#other-versions)
* [Description & Demo](#description--demo)
* [Quick Start](#quick-start)
* [Browser Support](#browser-support)
* [Reporting Issues](#reporting-issues)
* [Licensing](#licensing)
* [Useful Links](#useful-links)
## Other versions
This is Bulma Vue.js Buefy dashboard version.
More info on free & premium versions of Admin One Dashboard: https://justboil.me/bulma-admin-template/one
<table>
<tr>
<td align="center"><a href="https://github.com/vikdiesel/admin-one-bulma-dashboard" title="Free Bulma admin dashboard HTML CSS SCSS"><img src="https://justboil.me/svg/language-html5.svg" width="64" height="64"></a></td>
<td align="center"><a href="https://github.com/vikdiesel/admin-one-vue-bulma-dashboard" title="Free Bulma Vue.js Buefy admin dashboard"><img src="https://justboil.me/svg/vuejs.svg" width="64" height="64"></a></td>
<td align="center"><a href="https://github.com/justboil/admin-one-nuxt" title="Free Bulma Nuxt.js Buefy admin dashboard"><img src="https://justboil.me/svg/nuxt.svg" width="64" height="64"></a></td>
<td align="center"><a href="https://github.com/vikdiesel/admin-one-laravel-dashboard" title="Free Bulma Laravel admin dashboard"><img src="https://justboil.me/svg/laravel.svg" width="64" height="64"></a></td>
</tr>
<tr>
<td align="center">Bulma admin dashboard<br/>HTML/CSS/SCSS<br/><br/><a href="https://github.com/vikdiesel/admin-one-bulma-dashboard" title="Free Bulma admin dashboard HTML CSS SCSS">Free</a> | <a href="https://justboil.me/bulma-admin-template/one-html" title="Premium Bulma admin dashboard HTML CSS SCSS">Premium</a></td>
<td align="center">Bulma admin dashboard<br/>Vue.js Buefy<br/><br/><a href="https://github.com/vikdiesel/admin-one-vue-bulma-dashboard" title="Free Bulma Vue.js Buefy admin dashboard">Free</a> | <a href="https://justboil.me/bulma-admin-template/one" title="Premium Bulma Vue.js Buefy admin dashboard">Premium</a></td>
<td align="center">Bulma admin dashboard<br/>Nuxt.js Buefy<br/><br/><a href="https://github.com/justboil/admin-one-nuxt" title="Free Bulma Nuxt.js Buefy admin dashboard">Free</a> | <a href="https://justboil.me/bulma-admin-template/one-nuxt" title="Premium Bulma Nuxt.js Buefy admin dashboard">Premium</a></td>
<td align="center">Bulma admin dashboard<br/>Laravel<br/><br/><a href="https://github.com/vikdiesel/admin-one-laravel-dashboard" title="Free Bulma Laravel admin dashboard">Free</a> | <a href="https://justboil.me/bulma-admin-template/one-laravel" title="Free Bulma Laravel admin dashboard">Premium</a></td>
</tr>
</table>
## Description & Demo
#### Description
https://justboil.me/bulma-admin-template/one
#### Free Dashboard Demo
https://vikdiesel.github.io/admin-one-vue-bulma-dashboard/
#### Premium Dashboard Demo
https://admin-one.justboil.me
## Quick Start
#### Get the repo
* [Create new repo](https://github.com/vikdiesel/admin-one-vue-bulma-dashboard/generate) from this template
* &hellip;or clone the repo on GitHub
* &hellip;or [download .zip](https://github.com/vikdiesel/admin-one-vue-bulma-dashboard/archive/master.zip) from GitHub
#### Install
`cd` to project's dir and run `npm install`
#### Serve
To pre-compile & hot-reload for development run `npm run serve`
#### Build
Production-ready with minified bundle `npm run build`
## Browser Support
We try to make sure dashboard works well in the latest versions of all major browsers
<img src="https://justboil.me/images/browsers-svg/chrome.svg" width="64" height="64" alt="Chrome"> <img src="https://justboil.me/images/browsers-svg/firefox.svg" width="64" height="64" alt="Firefox"> <img src="https://justboil.me/images/browsers-svg/edge.svg" width="64" height="64" alt="Edge"> <img src="https://justboil.me/images/browsers-svg/safari.svg" width="64" height="64" alt="Safari"> <img src="https://justboil.me/images/browsers-svg/opera.svg" width="64" height="64" alt="Opera">
## Reporting Issues
JustBoil's free items are limited to community support on GitHub.
The issue list is reserved exclusively for bug reports and feature requests. That means we do not accept usage questions. If you open an issue that does not conform to the requirements, it will be closed.
1. Make sure you are using the latest version of the dashboard
2. Provide steps to reproduce
3. Provide an expected behavior
4. Describe what is actually happening
5. Platform, Browser & version as some issues may be browser specific
## Licensing
- Copyright &copy; 2019-2020 JustBoil.me (https://justboil.me)
- Licensed under MIT
## Useful Links
- [JustBoil.me](https://justboil.me)
- [Vue.js](https://vuejs.org)
- [Vue CLI](https://cli.vuejs.org)
- [Buefy](https://buefy.org)
- [Bulma](https://bulma.io)

5
admin/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

14203
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
admin/package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "admin-one-vue-bulma-dashboard",
"version": "1.5.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:gh-pages": "cross-env DEPLOY_ENV=GH_PAGES vue-cli-service build --modern",
"deploy:gh-pages": "cross-env NODE_DEBUG=gh-pages gh-pages -d dist -t",
"lint": "vue-cli-service lint",
"lintfix": "vue-cli-service lint --fix"
},
"dependencies": {
"axios": "^0.20.0",
"buefy": "^0.9.3",
"bulma": "^0.9.0",
"chart.js": "^2.9.3",
"core-js": "^3.6.5",
"dayjs": "^1.8.35",
"lodash": "^4.17.20",
"numeral": "^2.0.6",
"register-service-worker": "^1.7.1",
"vue": "^2.6.12",
"vue-axios": "^2.1.5",
"vue-chartjs": "^3.5.0",
"vue-router": "^3.4.3",
"vuex": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.4",
"@vue/cli-plugin-eslint": "~4.5.4",
"@vue/cli-plugin-pwa": "~4.5.4",
"@vue/cli-plugin-router": "~4.5.4",
"@vue/cli-plugin-vuex": "~4.5.4",
"@vue/cli-service": "~4.5.4",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.2",
"eslint": "^7.8.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"gh-pages": "^3.1.0",
"sass": "^1.26.10",
"sass-loader": "^10.0.2",
"vue-template-compiler": "^2.6.12"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

File diff suppressed because one or more lines are too long

BIN
admin/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,149 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.000320,-0.000320)"
fill="#000000" stroke="none">
<path d="M18 46618 c45 -75 122 -207 122 -211 0 -2 25 -45 55 -95 30 -50 55
-96 55 -102 0 -5 5 -10 10 -10 6 0 10 -4 10 -9 0 -5 73 -135 161 -288 89 -153
173 -298 187 -323 14 -25 32 -57 41 -72 88 -149 187 -324 189 -335 2 -7 8 -13
13 -13 5 0 9 -4 9 -10 0 -5 46 -89 103 -187 175 -302 490 -846 507 -876 8 -16
20 -36 25 -45 28 -46 290 -498 339 -585 13 -23 74 -129 136 -236 61 -107 123
-215 137 -240 14 -25 29 -50 33 -56 5 -5 23 -37 40 -70 18 -33 38 -67 44 -75
11 -16 21 -33 63 -109 14 -25 29 -50 33 -56 4 -5 21 -35 38 -65 55 -100 261
-455 269 -465 4 -5 14 -21 20 -35 15 -29 41 -75 103 -180 24 -41 52 -88 60
-105 9 -16 57 -100 107 -185 112 -193 362 -626 380 -660 8 -14 23 -38 33 -55
11 -16 23 -37 27 -45 4 -8 26 -46 48 -85 23 -38 53 -90 67 -115 46 -81 64
-113 178 -310 62 -107 121 -210 132 -227 37 -67 56 -99 85 -148 16 -27 32 -57
36 -65 4 -8 15 -27 25 -42 9 -15 53 -89 96 -165 44 -76 177 -307 296 -513 120
-206 268 -463 330 -570 131 -227 117 -203 200 -348 36 -62 73 -125 82 -140 10
-15 21 -34 25 -42 4 -8 20 -37 36 -65 17 -27 38 -65 48 -82 49 -85 64 -111 87
-153 13 -25 28 -49 32 -55 4 -5 78 -134 165 -285 87 -151 166 -288 176 -305
10 -16 26 -43 35 -59 9 -17 125 -217 257 -445 132 -229 253 -441 270 -471 17
-30 45 -79 64 -108 18 -29 33 -54 33 -57 0 -2 20 -37 44 -77 24 -40 123 -212
221 -383 97 -170 190 -330 205 -355 16 -25 39 -65 53 -90 13 -25 81 -144 152
-265 70 -121 137 -238 150 -260 12 -22 37 -65 55 -95 18 -30 43 -73 55 -95 12
-22 48 -85 80 -140 77 -132 163 -280 190 -330 13 -22 71 -123 130 -225 59
-102 116 -199 126 -217 10 -17 29 -50 43 -72 15 -22 26 -43 26 -45 0 -2 27
-50 60 -106 33 -56 60 -103 60 -105 0 -2 55 -98 90 -155 8 -14 182 -316 239
-414 13 -22 45 -79 72 -124 27 -46 49 -86 49 -89 0 -2 14 -24 30 -48 16 -24
30 -46 30 -49 0 -5 74 -135 100 -176 5 -8 24 -42 43 -75 50 -88 58 -101 262
-455 104 -179 199 -345 213 -370 14 -25 28 -49 32 -55 4 -5 17 -26 28 -45 10
-19 62 -109 114 -200 114 -197 133 -230 170 -295 16 -27 33 -57 38 -65 17 -28
96 -165 103 -180 4 -8 16 -28 26 -45 10 -16 77 -131 148 -255 72 -124 181
-313 243 -420 62 -107 121 -209 131 -227 35 -62 323 -560 392 -678 38 -66 83
-145 100 -175 16 -30 33 -59 37 -65 4 -5 17 -27 29 -47 34 -61 56 -100 90
-156 17 -29 31 -55 31 -57 0 -2 17 -32 39 -67 21 -35 134 -229 251 -433 117
-203 235 -407 261 -451 27 -45 49 -85 49 -88 0 -4 8 -19 19 -34 15 -21 200
-341 309 -533 10 -19 33 -58 51 -87 17 -29 31 -54 31 -56 0 -2 25 -44 55 -94
30 -50 55 -95 55 -98 0 -4 6 -15 14 -23 7 -9 27 -41 43 -71 17 -30 170 -297
342 -594 171 -296 311 -542 311 -547 0 -5 5 -9 10 -9 6 0 10 -4 10 -10 0 -5
22 -47 49 -92 27 -46 58 -99 68 -118 24 -43 81 -140 93 -160 5 -8 66 -114 135
-235 69 -121 130 -227 135 -235 12 -21 259 -447 283 -490 10 -19 28 -47 38
-62 11 -14 19 -29 19 -32 0 -3 37 -69 83 -148 99 -170 305 -526 337 -583 13
-22 31 -53 41 -70 11 -16 22 -37 26 -45 7 -14 82 -146 103 -180 14 -24 181
-311 205 -355 13 -22 46 -80 75 -130 29 -49 64 -110 78 -135 14 -25 51 -88 82
-140 31 -52 59 -102 63 -110 4 -8 18 -33 31 -55 205 -353 284 -489 309 -535
17 -30 45 -78 62 -106 18 -28 36 -60 39 -72 4 -12 12 -22 17 -22 5 0 9 -4 9
-10 0 -5 109 -197 241 -427 133 -230 250 -431 259 -448 51 -90 222 -385 280
-485 37 -63 78 -135 92 -160 14 -25 67 -117 118 -205 51 -88 101 -175 111
-193 34 -58 55 -95 149 -257 51 -88 101 -173 110 -190 9 -16 76 -131 147 -255
72 -124 140 -241 151 -260 61 -108 281 -489 355 -615 38 -66 77 -133 87 -150
35 -63 91 -161 100 -175 14 -23 99 -169 128 -220 54 -97 135 -235 142 -245 4
-5 20 -32 35 -60 26 -48 238 -416 276 -480 10 -16 26 -46 37 -65 30 -53 382
-661 403 -695 10 -16 22 -37 26 -45 4 -8 26 -48 50 -88 24 -41 43 -75 43 -77
0 -2 22 -40 50 -85 27 -45 50 -84 50 -86 0 -3 38 -69 83 -147 84 -142 302
-520 340 -587 10 -19 34 -60 52 -90 18 -30 44 -75 57 -100 14 -25 45 -79 70
-120 25 -41 56 -96 70 -121 14 -25 77 -133 138 -240 62 -107 122 -210 132
-229 25 -43 310 -535 337 -581 11 -19 26 -45 34 -59 17 -32 238 -414 266 -460
11 -19 24 -41 28 -49 3 -7 75 -133 160 -278 84 -146 153 -269 153 -274 0 -5 5
-9 10 -9 6 0 10 -4 10 -10 0 -5 82 -150 181 -322 182 -314 201 -346 240 -415
12 -21 80 -139 152 -263 71 -124 141 -245 155 -270 14 -25 28 -49 32 -55 6 -8
145 -248 220 -380 37 -66 209 -362 229 -395 11 -19 24 -42 28 -49 4 -8 67
-118 140 -243 73 -125 133 -230 133 -233 0 -2 15 -28 33 -57 19 -29 47 -78 64
-108 17 -30 53 -93 79 -139 53 -90 82 -141 157 -272 82 -142 115 -199 381
-659 142 -245 268 -463 281 -485 12 -22 71 -125 132 -230 60 -104 172 -298
248 -430 76 -132 146 -253 156 -270 11 -16 22 -36 26 -44 3 -8 30 -54 60 -103
29 -49 53 -91 53 -93 0 -3 18 -34 40 -70 22 -36 40 -67 40 -69 0 -2 37 -66 81
-142 45 -77 98 -168 119 -204 20 -36 47 -81 58 -100 12 -19 27 -47 33 -62 6
-16 15 -28 20 -28 5 0 9 -4 9 -9 0 -6 63 -118 140 -251 77 -133 140 -243 140
-245 0 -2 18 -33 41 -70 22 -37 49 -83 60 -101 10 -19 29 -51 40 -71 25 -45
109 -189 126 -218 7 -11 17 -29 22 -40 6 -11 22 -38 35 -60 14 -22 37 -62 52
-90 14 -27 35 -62 45 -77 11 -14 19 -29 19 -32 0 -3 18 -35 40 -71 22 -36 40
-67 40 -69 0 -2 19 -35 42 -72 23 -38 55 -94 72 -124 26 -47 139 -244 171
-298 6 -9 21 -36 34 -60 28 -48 37 -51 51 -19 6 12 19 36 29 52 10 17 27 46
38 65 11 19 104 181 208 360 103 179 199 345 213 370 14 25 42 74 64 109 21
34 38 65 38 67 0 2 18 33 40 69 22 36 40 67 40 69 0 3 177 310 199 346 16 26
136 234 140 244 2 5 25 44 52 88 27 44 49 81 49 84 0 2 18 34 40 70 22 36 40
67 40 69 0 2 20 36 43 77 35 58 169 289 297 513 9 17 50 86 90 155 40 69 86
150 103 180 16 30 35 62 41 70 6 8 16 24 22 35 35 64 72 129 167 293 59 100
116 199 127 220 11 20 30 53 41 72 43 72 1070 1850 1121 1940 14 25 65 113
113 195 48 83 96 166 107 185 10 19 28 50 38 68 11 18 73 124 137 235 64 111
175 303 246 427 71 124 173 299 225 390 52 91 116 202 143 248 27 45 49 85 49
89 0 4 6 14 14 22 7 9 28 43 46 76 26 47 251 436 378 655 11 19 29 51 40 70
11 19 101 176 201 348 99 172 181 317 181 323 0 5 5 9 10 9 6 0 10 5 10 11 0
6 8 23 18 37 11 15 32 52 49 82 16 30 130 228 253 440 122 212 234 405 248
430 13 25 39 70 57 100 39 65 69 117 130 225 25 44 50 87 55 95 12 19 78 134
220 380 61 107 129 224 150 260 161 277 222 382 246 425 15 28 47 83 71 123
24 41 43 78 43 83 0 5 4 9 8 9 4 0 13 12 19 28 7 15 23 45 36 67 66 110 277
478 277 483 0 3 6 13 14 21 7 9 27 41 43 71 17 30 45 80 63 110 34 57 375 649
394 685 6 11 16 27 22 35 6 8 26 42 44 75 18 33 41 74 51 90 10 17 24 41 32
55 54 97 72 128 88 152 11 14 19 28 19 30 0 3 79 141 175 308 96 167 175 305
175 308 0 3 6 13 14 21 7 9 26 39 41 66 33 60 276 483 338 587 24 40 46 80 50
88 4 8 13 24 20 35 14 23 95 163 125 215 11 19 52 91 92 160 40 69 80 139 90
155 9 17 103 179 207 360 105 182 200 346 211 365 103 181 463 802 489 845 7
11 15 27 19 35 4 8 29 51 55 95 64 110 828 1433 848 1470 9 17 24 41 33 55 9
14 29 48 45 77 15 28 52 93 82 145 30 51 62 107 71 123 17 30 231 398 400 690
51 88 103 179 115 202 12 23 26 48 32 55 6 7 24 38 40 68 17 30 61 107 98 170
37 63 84 144 103 180 19 36 41 72 48 81 8 8 14 18 14 21 0 4 27 51 59 106 32
55 72 124 89 154 16 29 71 125 122 213 51 88 104 180 118 205 13 25 28 50 32
55 4 6 17 26 28 45 11 19 45 80 77 135 31 55 66 116 77 135 11 19 88 152 171
295 401 694 620 1072 650 1125 11 19 87 152 170 295 83 143 158 273 166 288 9
16 21 36 26 45 6 9 31 52 55 96 25 43 54 94 66 115 11 20 95 164 186 321 91
157 173 299 182 315 9 17 26 46 37 65 12 19 66 114 121 210 56 96 108 186 117
200 8 14 24 40 34 59 24 45 383 664 412 713 5 9 17 29 26 45 15 28 120 210
241 419 36 61 68 117 72 125 4 8 12 23 19 34 35 57 245 420 262 453 11 20 35
61 53 90 17 29 32 54 32 56 0 3 28 51 62 108 33 57 70 119 80 138 10 19 23 42
28 50 5 8 32 53 59 100 27 47 149 258 271 470 122 212 234 405 248 430 30 53
62 108 80 135 6 11 15 27 19 35 4 8 85 150 181 315 96 165 187 323 202 350 31
56 116 202 130 225 5 8 25 42 43 75 19 33 92 159 162 280 149 257 157 271 202
350 19 33 38 67 43 75 9 14 228 392 275 475 12 22 55 96 95 165 40 69 80 139
90 155 24 42 202 350 221 383 9 15 27 47 41 72 14 25 75 131 136 236 61 106
121 210 134 232 99 172 271 470 279 482 5 8 23 40 40 70 18 30 81 141 142 245
60 105 121 210 135 235 14 25 71 124 127 220 56 96 143 247 194 335 51 88 96
167 102 175 14 24 180 311 204 355 23 43 340 590 356 615 5 8 50 87 101 175
171 301 517 898 582 1008 25 43 46 81 46 83 0 2 12 23 27 47 14 23 40 67 56
97 16 30 35 62 42 70 7 8 15 22 18 30 4 8 20 38 37 65 16 28 33 57 37 65 6 12
111 196 143 250 5 8 55 95 112 193 57 98 113 195 126 215 12 20 27 46 32 57 6
11 14 27 20 35 5 8 76 130 156 270 80 140 165 287 187 325 23 39 52 90 66 115
13 25 30 52 37 61 8 8 14 18 14 21 0 4 41 77 92 165 50 87 175 302 276 478
101 176 208 360 236 408 28 49 67 117 86 152 19 35 41 70 48 77 6 6 12 15 12
19 0 7 124 224 167 291 12 21 23 40 23 42 0 2 21 40 46 83 26 43 55 92 64 109
54 95 327 568 354 614 19 30 45 75 59 100 71 128 82 145 89 148 4 2 8 8 8 13
0 5 42 82 94 172 311 538 496 858 518 897 14 25 40 70 58 100 18 30 42 71 53
90 10 19 79 139 152 265 73 127 142 246 153 265 10 19 43 76 72 125 29 50 63
108 75 130 65 116 80 140 87 143 4 2 8 8 8 12 0 8 114 212 140 250 6 8 14 24
20 35 5 11 54 97 108 190 l100 170 -9611 3 c-5286 1 -9614 -1 -9618 -5 -5 -6
-419 -719 -619 -1068 -89 -155 -267 -463 -323 -560 -38 -66 -81 -140 -95 -165
-31 -56 -263 -457 -526 -910 -110 -190 -224 -388 -254 -440 -29 -52 -61 -109
-71 -125 -23 -39 -243 -420 -268 -465 -11 -19 -204 -352 -428 -740 -224 -388
-477 -826 -563 -975 -85 -148 -185 -322 -222 -385 -37 -63 -120 -207 -185
-320 -65 -113 -177 -306 -248 -430 -72 -124 -172 -297 -222 -385 -51 -88 -142
-245 -202 -350 -131 -226 -247 -427 -408 -705 -65 -113 -249 -432 -410 -710
-160 -278 -388 -673 -506 -877 -118 -205 -216 -373 -219 -373 -3 0 -52 82
-109 183 -58 100 -144 250 -192 332 -95 164 -402 696 -647 1120 -85 149 -228
396 -317 550 -212 365 -982 1700 -1008 1745 -10 19 -43 76 -72 125 -29 50 -64
110 -77 135 -14 25 -63 110 -110 190 -47 80 -96 165 -110 190 -14 25 -99 171
-188 325 -89 154 -174 300 -188 325 -13 25 -64 113 -112 195 -48 83 -140 242
-205 355 -65 113 -183 317 -263 454 -79 137 -152 264 -163 282 -50 89 -335
583 -354 614 -12 19 -34 58 -50 85 -15 28 -129 226 -253 440 -124 215 -235
408 -247 430 -12 22 -69 121 -127 220 -58 99 -226 389 -373 645 -148 256 -324
561 -392 678 -67 117 -134 232 -147 255 -13 23 -33 59 -46 80 l-22 37 -9615 0
-9615 0 20 -32z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

22
admin/public/index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<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.png">
<title>Kaverti Admin</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
</head>
<body>
<noscript>
<strong>We're sorry but Kaverti Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<link href="https://cdn.materialdesignicons.com/4.9.95/css/materialdesignicons.min.css" rel="stylesheet" type="text/css">
</body>
</html>

View File

@ -0,0 +1,20 @@
{
"name": "admin-one-vue-bulma-dashboard",
"short_name": "admin-one-vue-bulma-dashboard",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}

2
admin/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

92
admin/src/App.vue Normal file
View File

@ -0,0 +1,92 @@
<template>
<div id="app">
<nav-bar />
<aside-menu :menu="menu" />
<router-view />
<footer-bar />
</div>
</template>
<script>
// @ is an alias to /src
import NavBar from '@/components/NavBar'
import AsideMenu from '@/components/AsideMenu'
import FooterBar from '@/components/FooterBar'
export default {
name: 'Home',
components: {
FooterBar,
AsideMenu,
NavBar
},
computed: {
menu () {
return [
'Read Only',
[
{
to: '/',
icon: 'desktop-mac',
label: 'Dashboard'
}
],
'Examples',
[
{
to: '/tables',
label: 'Tables',
icon: 'table',
updateMark: true
},
{
to: '/forms',
label: 'Forms',
icon: 'square-edit-outline'
},
{
to: '/profile',
label: 'Profile',
icon: 'account-circle'
},
{
label: 'Submenus',
subLabel: 'Submenus Example',
icon: 'view-list',
menu: [
{
href: '#void',
label: 'Sub-item One'
},
{
href: '#void',
label: 'Sub-item Two'
}
]
}
],
'About',
[
{
href: 'https://admin-one.justboil.me',
label: 'Premium Demo',
icon: 'credit-card'
},
{
href: 'https://justboil.me/bulma-admin-template/one',
label: 'About',
icon: 'help-circle'
}
]
]
}
},
created () {
this.$store.commit('user', {
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://avatars.dicebear.com/v2/gridy/John-Doe.svg'
})
}
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,45 @@
<template>
<aside v-show="isAsideVisible" class="aside is-placed-left is-expanded">
<aside-tools :is-main-menu="true">
<span slot="label">Kaverti Administration</span>
</aside-tools>
<div class="menu is-menu-main">
<template v-for="(menuGroup, index) in menu">
<p v-if="typeof menuGroup === 'string'" :key="index" class="menu-label">
{{ menuGroup }}
</p>
<aside-menu-list
v-else
:key="index"
:menu="menuGroup"
@menu-click="menuClick"
/>
</template>
</div>
</aside>
</template>
<script>
import { mapState } from 'vuex'
import AsideTools from '@/components/AsideTools'
import AsideMenuList from '@/components/AsideMenuList'
export default {
name: 'AsideMenu',
components: { AsideTools, AsideMenuList },
props: {
menu: {
type: Array,
default: () => []
}
},
computed: {
...mapState(['isAsideVisible'])
},
methods: {
menuClick (item) {
//
}
}
}
</script>

View File

@ -0,0 +1,76 @@
<template>
<li :class="{ 'is-active': isDropdownActive }">
<component
:is="componentIs"
:to="itemTo"
:href="itemHref"
exact-active-class="is-active"
:class="{ 'has-icon': !!item.icon, 'has-dropdown-icon': hasDropdown }"
@click="menuClick"
>
<b-icon
v-if="item.icon"
:icon="item.icon"
:class="{ 'has-update-mark': item.updateMark }"
custom-size="default"
/>
<span v-if="item.label" :class="{ 'menu-item-label': !!item.icon }">{{
item.label
}}</span>
<div v-if="hasDropdown" class="dropdown-icon">
<b-icon :icon="dropdownIcon" custom-size="default" />
</div>
</component>
<aside-menu-list
v-if="hasDropdown"
:menu="item.menu"
:is-submenu-list="true"
/>
</li>
</template>
<script>
export default {
name: 'AsideMenuItem',
components: {
AsideMenuList: () => import('@/components/AsideMenuList')
},
props: {
item: {
type: Object,
default: null
}
},
data () {
return {
isDropdownActive: false
}
},
computed: {
componentIs () {
return this.item.to ? 'router-link' : 'a'
},
hasDropdown () {
return !!this.item.menu
},
dropdownIcon () {
return this.isDropdownActive ? 'minus' : 'plus'
},
itemTo () {
return this.item.to ? this.item.to : null
},
itemHref () {
return this.item.href ? this.item.href : null
}
},
methods: {
menuClick () {
this.$emit('menu-click', this.item)
if (this.hasDropdown) {
this.isDropdownActive = !this.isDropdownActive
}
}
}
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<ul :class="{ 'menu-list': !isSubmenuList }">
<aside-menu-item
v-for="(item, index) in menu"
:key="index"
:item="item"
@menu-click="menuClick"
/>
</ul>
</template>
<script>
import AsideMenuItem from '@/components/AsideMenuItem'
export default {
name: 'AsideMenuList',
components: {
AsideMenuItem
},
props: {
isSubmenuList: {
type: Boolean,
default: false
},
menu: {
type: Array,
default: () => []
}
},
methods: {
menuClick (item) {
this.$emit('menu-click', item)
}
}
}
</script>

View File

@ -0,0 +1,24 @@
<template>
<div class="aside-tools">
<div class="aside-tools-label">
<b-icon v-if="icon" :icon="icon" custom-size="default" />
<slot name="label" />
</div>
</div>
</template>
<script>
export default {
name: 'AsideTools',
props: {
icon: {
type: String,
default: null
},
label: {
type: String,
default: null
}
}
}
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="card">
<header v-if="title" class="card-header">
<p class="card-header-title">
<b-icon v-if="icon" :icon="icon" custom-size="default" />
{{ title }}
</p>
<a
v-if="headerIcon"
href="#"
class="card-header-icon"
aria-label="more options"
@click.prevent="headerIconClick"
>
<b-icon :icon="headerIcon" custom-size="default" />
</a>
</header>
<div class="card-content">
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'CardComponent',
props: {
title: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
headerIcon: {
type: String,
default: null
}
},
methods: {
headerIconClick () {
this.$emit('header-icon-click')
}
}
}
</script>

View File

@ -0,0 +1,28 @@
<template>
<div class="notification is-card-toolbar">
<div class="level" :class="{ 'is-mobile': isMobile }">
<div class="level-left">
<div class="level-item">
<slot name="left" />
</div>
</div>
<div class="level-right">
<div class="level-item">
<slot name="right" />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CardToolbar',
props: {
isMobile: {
type: Boolean,
default: false
}
}
}
</script>

View File

@ -0,0 +1,56 @@
<template>
<card-component>
<div class="level is-mobile">
<div class="level-item">
<div class="is-widget-label">
<h3 class="subtitle is-spaced">
{{ label }}
</h3>
<h1 class="title">
<growing-number :value="number" :prefix="prefix" :suffix="suffix" />
</h1>
</div>
</div>
<div v-if="icon" class="level-item has-widget-icon">
<div class="is-widget-icon">
<b-icon :icon="icon" size="is-large" :type="type" />
</div>
</div>
</div>
</card-component>
</template>
<script>
import CardComponent from '@/components/CardComponent'
import GrowingNumber from '@/components/GrowingNumber'
export default {
name: 'CardWidget',
components: { GrowingNumber, CardComponent },
props: {
icon: {
type: String,
default: null
},
number: {
type: Number,
default: 0
},
prefix: {
type: String,
default: null
},
suffix: {
type: String,
default: null
},
label: {
type: String,
default: null
},
type: {
type: String,
default: null
}
}
}
</script>

View File

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

View File

@ -0,0 +1,60 @@
export const chartColors = {
default: {
primary: '#00D1B2',
info: '#209CEE',
danger: '#FF3860'
}
}
export const baseChartOptions = {
maintainAspectRatio: false,
legend: {
display: false
},
responsive: true
}
export const chartOptionsMain = {
...baseChartOptions,
tooltips: {
backgroundColor: '#f5f5f5',
titleFontColor: '#333',
bodyFontColor: '#666',
bodySpacing: 4,
xPadding: 12,
mode: 'nearest',
intersect: 0,
position: 'nearest'
},
scales: {
yAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: 'rgba(29,140,248,0.0)',
zeroLineColor: 'transparent'
},
ticks: {
padding: 20,
fontColor: '#9a9a9a'
}
}
],
xAxes: [
{
barPercentage: 1.6,
gridLines: {
drawBorder: false,
color: 'rgba(225,78,202,0.1)',
zeroLineColor: 'transparent'
},
ticks: {
padding: 20,
fontColor: '#9a9a9a'
}
}
]
}
}

View File

@ -0,0 +1,52 @@
<template>
<b-field grouped group-multiline>
<div v-for="(v, k) in options" :key="k" class="control">
<b-checkbox
v-model="newValue"
:native-value="k"
:type="type"
@input="input"
>
{{ v }}
</b-checkbox>
</div>
</b-field>
</template>
<script>
export default {
name: 'CheckboxPicker',
props: {
options: {
type: Object,
default: null
},
type: {
type: String,
default: null
},
value: {
type: Array,
default: () => []
}
},
data () {
return {
newValue: []
}
},
watch: {
value (newValue) {
this.newValue = newValue
}
},
created () {
this.newValue = this.value
},
methods: {
input () {
this.$emit('input', this.newValue)
}
}
}
</script>

View File

@ -0,0 +1,148 @@
<template>
<div>
<modal-box
:is-active="isModalActive"
:trash-object-name="trashObjectName"
@confirm="trashConfirm"
@cancel="trashCancel"
/>
<b-table
:checked-rows.sync="checkedRows"
:checkable="checkable"
:loading="isLoading"
:paginated="paginated"
:per-page="perPage"
:striped="true"
:hoverable="true"
default-sort="name"
:data="clients"
>
<b-table-column cell-class="has-no-head-mobile is-image-cell" v-slot="props">
<div class="image">
<img :src="props.row.avatar" class="is-rounded">
</div>
</b-table-column>
<b-table-column label="Name" field="name" sortable v-slot="props">
{{ props.row.name }}
</b-table-column>
<b-table-column label="Company" field="company" sortable v-slot="props">
{{ props.row.company }}
</b-table-column>
<b-table-column label="City" field="city" sortable v-slot="props">
{{ props.row.city }}
</b-table-column>
<b-table-column cell-class="is-progress-col" label="Progress" field="progress" sortable v-slot="props">
<progress class="progress is-small is-primary" :value="props.row.progress" max="100">{{ props.row.progress }}</progress>
</b-table-column>
<b-table-column label="Created" v-slot="props">
<small class="has-text-grey is-abbr-like" :title="props.row.created">{{ props.row.created }}</small>
</b-table-column>
<b-table-column custom-key="actions" cell-class="is-actions-cell" v-slot="props">
<div class="buttons is-right">
<router-link :to="{name:'client.edit', params: {id: props.row.id}}" class="button is-small is-primary">
<b-icon icon="account-edit" size="is-small"/>
</router-link>
<button class="button is-small is-danger" type="button" @click.prevent="trashModal(props.row)">
<b-icon icon="trash-can" size="is-small"/>
</button>
</div>
</b-table-column>
<section slot="empty" class="section">
<div class="content has-text-grey has-text-centered">
<template v-if="isLoading">
<p>
<b-icon icon="dots-horizontal" size="is-large" />
</p>
<p>Fetching data...</p>
</template>
<template v-else>
<p>
<b-icon icon="emoticon-sad" size="is-large" />
</p>
<p>Nothing's here&hellip;</p>
</template>
</div>
</section>
</b-table>
</div>
</template>
<script>
import axios from 'axios'
import ModalBox from '@/components/ModalBox'
export default {
name: 'ClientsTableSample',
components: { ModalBox },
props: {
dataUrl: {
type: String,
default: null
},
checkable: {
type: Boolean,
default: false
}
},
data () {
return {
isModalActive: false,
trashObject: null,
clients: [],
isLoading: false,
paginated: false,
perPage: 10,
checkedRows: []
}
},
computed: {
trashObjectName () {
if (this.trashObject) {
return this.trashObject.name
}
return null
}
},
mounted () {
if (this.dataUrl) {
this.isLoading = true
axios
.get(this.dataUrl)
.then((r) => {
this.isLoading = false
if (r.data && r.data.data) {
if (r.data.data.length > this.perPage) {
this.paginated = true
}
this.clients = r.data.data
}
})
.catch((e) => {
this.isLoading = false
this.$buefy.toast.open({
message: `Error: ${e.message}`,
type: 'is-danger'
})
})
}
},
methods: {
trashModal (trashObject) {
this.trashObject = trashObject
this.isModalActive = true
},
trashConfirm () {
this.isModalActive = false
this.$buefy.snackbar.open({
message: 'Confirmed',
queue: false
})
},
trashCancel () {
this.isModalActive = false
}
}
}
</script>

View File

@ -0,0 +1,64 @@
<template>
<b-field class="file">
<b-upload v-model="file" :accept="accept" @input="upload">
<a class="button is-primary">
<b-icon icon="upload" custom-size="default"></b-icon>
<span>{{ buttonLabel }}</span>
</a>
</b-upload>
<span v-if="file" class="file-name">{{ file.name }}</span>
</b-field>
</template>
<script>
export default {
name: 'FilePicker',
props: {
accept: {
type: String,
default: null
}
},
data () {
return {
file: null,
uploadPercent: 0
}
},
computed: {
buttonLabel () {
return !this.file ? 'Pick a file' : 'Pick another file'
}
},
methods: {
upload (file) {
this.$emit('input', file)
// Use this as an example for handling file uploads
// let formData = new FormData()
// formData.append('file', file)
// axios
// .post(window.routeMediaStore, formData, {
// headers: {
// 'Content-Type': 'multipart/form-data'
// },
// onUploadProgress: this.progressEvent
// })
// .then(r => {
//
// })
// .catch(err => {
// this.$buefy.toast.open({
// message: `Error: ${err.message}`,
// type: 'is-danger'
// })
// })
},
progressEvent (progressEvent) {
this.uploadPercent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
}
}
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<footer v-show="isFooterBarVisible" class="footer">
<div class="container-fluid">
<div class="level">
<div class="level-left">
<div class="level-item">
<div class="footer-copyright">
<b>&copy; {{ year }}, JustBoil.me</b> &mdash; Admin One Demo
<span class="tag">v1.5.0</span>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="logo">
<a href="https://justboil.me">
<img src="../assets/justboil-logo.svg" alt="JustBoil.me" />
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
</template>
<script>
import dayjs from 'dayjs'
import { mapState } from 'vuex'
export default {
name: 'FooterBar',
computed: {
year () {
return dayjs().year()
},
...mapState(['isFooterBarVisible'])
}
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<div>{{ prefix }}{{ newValueFormatted }}{{ suffix }}</div>
</template>
<script>
import numeral from 'numeral'
export default {
name: 'GrowingNumber',
props: {
prefix: {
type: String,
default: null
},
suffix: {
type: String,
default: null
},
value: {
type: Number,
default: 0
},
duration: {
type: Number,
default: 500
}
},
data () {
return {
newValue: 0,
step: 0
}
},
computed: {
newValueFormatted () {
return this.newValue < 1000
? this.newValue
: numeral(this.newValue).format('0,0')
}
},
watch: {
value () {
this.growInit()
}
},
mounted () {
this.growInit()
},
methods: {
growInit () {
const m = this.value / (this.duration / 25)
this.grow(m)
},
grow (m) {
const v = Math.ceil(this.newValue + m)
if (v > this.value) {
this.newValue = this.value
return false
}
this.newValue = v
setTimeout(() => {
this.grow(m)
}, 25)
}
}
}
</script>

View File

@ -0,0 +1,32 @@
<template>
<section class="hero is-hero-bar">
<div class="hero-body">
<div class="level">
<div class="level-left">
<div class="level-item">
<h1 class="title">
<slot />
</h1>
</div>
</div>
<div v-show="hasRightVisible" class="level-right">
<div class="level-item">
<slot name="right" />
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: 'HeroBar',
props: {
hasRightVisible: {
type: Boolean,
default: true
}
}
}
</script>

View File

@ -0,0 +1,58 @@
<template>
<b-modal :active.sync="isModalActive" has-modal-card>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Confirm action</p>
</header>
<section class="modal-card-body">
<p>
This will permanently delete <b>{{ trashObjectName }}</b>
</p>
<p>Action can not be undone.</p>
</section>
<footer class="modal-card-foot">
<button class="button" type="button" @click="cancel">Cancel</button>
<button class="button is-danger" @click="confirm">Delete</button>
</footer>
</div>
</b-modal>
</template>
<script>
export default {
name: 'ModalBox',
props: {
isActive: {
type: Boolean,
default: false
},
trashObjectName: {
type: String,
default: null
}
},
data () {
return {
isModalActive: false
}
},
watch: {
isActive (newValue) {
this.isModalActive = newValue
},
isModalActive (newValue) {
if (!newValue) {
this.cancel()
}
}
},
methods: {
cancel () {
this.$emit('cancel')
},
confirm () {
this.$emit('confirm')
}
}
}
</script>

View File

@ -0,0 +1,147 @@
<template>
<nav v-show="isNavBarVisible" id="navbar-main" class="navbar is-fixed-top">
<div class="navbar-brand">
<a
class="navbar-item is-hidden-desktop"
@click.prevent="menuToggleMobile"
>
<b-icon :icon="menuToggleMobileIcon" />
</a>
<div class="navbar-item has-control no-left-space-touch">
<div class="control">
<input class="input" placeholder="Search everywhere..." />
</div>
</div>
</div>
<div class="navbar-brand is-right">
<a
class="navbar-item navbar-item-menu-toggle is-hidden-desktop"
@click.prevent="menuNavBarToggle"
>
<b-icon :icon="menuNavBarToggleIcon" custom-size="default" />
</a>
</div>
<div
class="navbar-menu fadeIn animated faster"
:class="{ 'is-active': isMenuNavBarActive }"
>
<div class="navbar-end">
<nav-bar-menu class="has-divider">
<b-icon icon="menu" custom-size="default" />
<span>Sample Menu</span>
<div slot="dropdown" class="navbar-dropdown">
<router-link
to="/profile"
class="navbar-item"
exact-active-class="is-active"
>
<b-icon icon="account" custom-size="default" />
<span>My Profile</span>
</router-link>
<a class="navbar-item">
<b-icon icon="settings" custom-size="default" />
<span>Settings</span>
</a>
<a class="navbar-item">
<b-icon icon="email" custom-size="default" />
<span>Messages</span>
</a>
<hr class="navbar-divider" />
<a class="navbar-item">
<b-icon icon="logout" custom-size="default" />
<span>Log Out</span>
</a>
</div>
</nav-bar-menu>
<nav-bar-menu class="has-divider has-user-avatar">
<user-avatar />
<div class="is-user-name">
<span>{{ userName }}</span>
</div>
<div slot="dropdown" class="navbar-dropdown">
<router-link
to="/profile"
class="navbar-item"
exact-active-class="is-active"
>
<b-icon icon="account" custom-size="default" />
<span>My Profile</span>
</router-link>
<a class="navbar-item">
<b-icon icon="settings" custom-size="default"></b-icon>
<span>Settings</span>
</a>
<a class="navbar-item">
<b-icon icon="email" custom-size="default"></b-icon>
<span>Messages</span>
</a>
<hr class="navbar-divider" />
<a class="navbar-item">
<b-icon icon="logout" custom-size="default"></b-icon>
<span>Log Out</span>
</a>
</div>
</nav-bar-menu>
<a
href="https://justboil.me/bulma-admin-template/one"
class="navbar-item has-divider is-desktop-icon-only"
title="About"
>
<b-icon icon="help-circle-outline" custom-size="default" />
<span>About</span>
</a>
<a
class="navbar-item is-desktop-icon-only"
title="Log out"
@click="logout"
>
<b-icon icon="logout" custom-size="default" />
<span>Log out</span>
</a>
</div>
</div>
</nav>
</template>
<script>
import { mapState } from 'vuex'
import NavBarMenu from '@/components/NavBarMenu'
import UserAvatar from '@/components/UserAvatar'
export default {
name: 'NavBar',
components: {
UserAvatar,
NavBarMenu
},
data () {
return {
isMenuNavBarActive: false
}
},
computed: {
menuNavBarToggleIcon () {
return this.isMenuNavBarActive ? 'close' : 'dots-vertical'
},
menuToggleMobileIcon () {
return this.isAsideMobileExpanded ? 'backburger' : 'forwardburger'
},
...mapState(['isNavBarVisible', 'isAsideMobileExpanded', 'userName'])
},
methods: {
menuToggleMobile () {
this.$store.commit('asideMobileStateToggle')
},
menuNavBarToggle () {
this.isMenuNavBarActive = !this.isMenuNavBarActive
},
logout () {
this.$buefy.snackbar.open({
message: 'Log out clicked',
queue: false
})
}
}
}
</script>

View File

@ -0,0 +1,53 @@
<template>
<div
class="navbar-item has-dropdown has-dropdown-with-icons"
:class="{ 'is-hoverable': isHoverable, 'is-active': isDropdownActive }"
@click="toggle"
>
<a class="navbar-link is-arrowless">
<slot />
<b-icon :icon="toggleDropdownIcon" custom-size="default" />
</a>
<slot name="dropdown" />
</div>
</template>
<script>
export default {
name: 'NavBarMenu',
props: {
isHoverable: {
type: Boolean,
default: false
}
},
data () {
return {
isDropdownActive: false
}
},
computed: {
toggleDropdownIcon () {
return this.isDropdownActive ? 'chevron-up' : 'chevron-down'
}
},
mounted () {
window.addEventListener('click', this.forceClose)
},
beforeDestroy () {
window.removeEventListener('click', this.forceClose)
},
methods: {
toggle () {
if (!this.isHoverable) {
this.isDropdownActive = !this.isDropdownActive
}
},
forceClose (e) {
if (!this.$el.contains(e.target)) {
this.isDropdownActive = false
}
}
}
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<div v-if="!isDismissed" class="notification">
<div class="level">
<div class="level-left">
<div class="level-item">
<slot />
</div>
</div>
<div class="level-right">
<button type="button" class="button is-small is-white" @click="dismiss">
Dismiss
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Notification',
data () {
return {
isDismissed: false
}
},
methods: {
dismiss () {
this.isDismissed = true
this.$buefy.snackbar.open({
message: 'Dismissed',
queue: false
})
}
}
}
</script>

View File

@ -0,0 +1,89 @@
<template>
<card-component title="Change Password" icon="lock">
<form @submit.prevent="submit">
<b-field
horizontal
label="Current password"
message="Required. Your current password"
>
<b-input
v-model="form.password_current"
name="password_current"
type="password"
required
autcomplete="current-password"
/>
</b-field>
<hr />
<b-field horizontal label="New password" message="Required. New password">
<b-input
v-model="form.password"
name="password"
type="password"
required
autocomplete="new-password"
/>
</b-field>
<b-field
horizontal
label="Confirm password"
message="Required. New password one more time"
>
<b-input
v-model="form.password_confirmation"
name="password_confirmation"
type="password"
required
autocomplete="new-password"
/>
</b-field>
<hr />
<b-field horizontal>
<div class="control">
<button
type="submit"
class="button is-primary"
:class="{ 'is-loading': isLoading }"
>
Submit
</button>
</div>
</b-field>
</form>
</card-component>
</template>
<script>
import CardComponent from '@/components/CardComponent'
export default {
name: 'PasswordUpdateForm',
components: {
CardComponent
},
data () {
return {
isLoading: false,
form: {
password_current: null,
password: null,
password_confirmation: null
}
}
},
methods: {
submit () {
this.isLoading = true
setTimeout(() => {
this.isLoading = false
this.$buefy.snackbar.open(
{
message: 'Updated',
queue: false
},
500
)
})
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<card-component title="Edit Profile" icon="account-circle">
<form @submit.prevent="submit">
<b-field horizontal label="Avatar">
<file-picker />
</b-field>
<hr />
<b-field horizontal label="Name" message="Required. Your name">
<b-input v-model="form.name" name="name" required />
</b-field>
<b-field horizontal label="E-mail" message="Required. Your e-mail">
<b-input v-model="form.email" name="email" type="email" required />
</b-field>
<hr />
<b-field horizontal>
<div class="control">
<button
type="submit"
class="button is-primary"
:class="{ 'is-loading': isLoading }"
>
Submit
</button>
</div>
</b-field>
</form>
</card-component>
</template>
<script>
import { mapState } from 'vuex'
import FilePicker from '@/components/FilePicker'
import CardComponent from '@/components/CardComponent'
export default {
name: 'ProfileUpdateForm',
components: {
CardComponent,
FilePicker
},
data () {
return {
isFileUploaded: false,
isLoading: false,
form: {
name: null,
email: null
}
}
},
computed: {
...mapState(['userName', 'userEmail'])
},
watch: {
userName (newValue) {
this.form.name = newValue
},
userEmail (newValue) {
this.form.email = newValue
}
},
mounted () {
this.form.name = this.userName
this.form.email = this.userEmail
},
methods: {
submit () {
this.isLoading = true
setTimeout(() => {
this.isLoading = false
this.$store.commit('user', this.form)
this.$buefy.snackbar.open({
message: 'Updated',
queue: false
})
}, 500)
}
}
}
</script>

View File

@ -0,0 +1,47 @@
<template>
<b-field grouped group-multiline>
<div v-for="(v, k) in options" :key="k" class="control">
<b-radio v-model="newValue" :native-value="k" :type="type" @input="input">
{{ v }}
</b-radio>
</div>
</b-field>
</template>
<script>
export default {
name: 'RadioPicker',
props: {
options: {
type: Object,
default: null
},
type: {
type: String,
default: null
},
value: {
type: [String, Number],
default: null
}
},
data () {
return {
newValue: null
}
},
watch: {
value (newValue) {
this.newValue = newValue
}
},
created () {
this.newValue = this.value
},
methods: {
input () {
this.$emit('input', this.newValue)
}
}
}
</script>

View File

@ -0,0 +1,39 @@
<script>
import chunk from 'lodash/chunk'
export default {
name: 'Tiles',
props: {
maxPerRow: {
type: Number,
default: 5
}
},
methods: {
renderAncestor (createElement, elements) {
return createElement(
'div',
{ attrs: { class: 'tile is-ancestor' } },
elements.map((element) => {
return createElement('div', { attrs: { class: 'tile is-parent' } }, [
element
])
})
)
}
},
render (createElement) {
if (this.$slots.default.length <= this.maxPerRow) {
return this.renderAncestor(createElement, this.$slots.default)
} else {
return createElement(
'div',
{ attrs: { class: 'is-tiles-wrapper' } },
chunk(this.$slots.default, this.maxPerRow).map((group) => {
return this.renderAncestor(createElement, group)
})
)
}
}
}
</script>

View File

@ -0,0 +1,41 @@
<template>
<section class="section is-title-bar">
<div class="level">
<div class="level-left">
<div class="level-item">
<ul>
<li v-for="(title, index) in titleStack" :key="index">
{{ title }}
</li>
</ul>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="buttons is-right">
<a
href="https://admin-one.justboil.me/"
target="_blank"
class="button is-primary"
>
<b-icon icon="credit-card" custom-size="default" />
<span>Premium Demo</span>
</a>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: 'TitleBar',
props: {
titleStack: {
type: Array,
default: () => []
}
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="is-user-avatar">
<img :src="newUserAvatar" :alt="userName" />
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'UserAvatar',
props: {
avatar: {
type: String,
default: null
}
},
computed: {
newUserAvatar () {
if (this.avatar) {
return this.avatar
}
if (this.userAvatar) {
return this.userAvatar
}
let name = 'somename'
if (this.userName) {
name = this.userName.replace(/[^a-z0-9]+/i, '')
}
return `https://avatars.dicebear.com/v2/human/${name}.svg?options[mood][]=happy`
},
...mapState(['userAvatar', 'userName'])
}
}
</script>

11
admin/src/css/animate.min.css vendored Normal file
View File

@ -0,0 +1,11 @@
@charset "UTF-8";
/*!
* animate.css -https://daneden.github.io/animate.css/
* Version - 3.7.2
* Licensed under the MIT license - https://opensource.org/licenses/MIT
*
* Copyright (c) 2019 Daniel Eden
*/
@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp}.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.delay-1s{-webkit-animation-delay:1s;animation-delay:1s}.animated.delay-2s{-webkit-animation-delay:2s;animation-delay:2s}.animated.delay-3s{-webkit-animation-delay:3s;animation-delay:3s}.animated.delay-4s{-webkit-animation-delay:4s;animation-delay:4s}.animated.delay-5s{-webkit-animation-delay:5s;animation-delay:5s}.animated.fast{-webkit-animation-duration:.8s;animation-duration:.8s}.animated.faster{-webkit-animation-duration:.5s;animation-duration:.5s}.animated.slow{-webkit-animation-duration:2s;animation-duration:2s}.animated.slower{-webkit-animation-duration:3s;animation-duration:3s}@media (prefers-reduced-motion:reduce),(print){.animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}}

40
admin/src/main.js Normal file
View File

@ -0,0 +1,40 @@
/* Styles */
import '@/scss/main.scss'
/* Core */
import Vue from 'vue'
import Buefy from 'buefy'
/* Router & Store */
import router from './router'
import store from './store'
/* Service Worker */
import './registerServiceWorker'
/* Vue. Main component */
import App from './App.vue'
/* Default title tag */
const defaultDocumentTitle = 'Kaverti Admin'
/* Collapse mobile aside menu on route change & set document title from route meta */
router.afterEach(to => {
store.commit('asideMobileStateToggle', false)
if (to.meta && to.meta.title) {
document.title = `${to.meta.title}${defaultDocumentTitle}`
} else {
document.title = defaultDocumentTitle
}
})
Vue.config.productionTip = false
Vue.use(Buefy)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

View File

@ -0,0 +1,32 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}

76
admin/src/router/index.js Normal file
View File

@ -0,0 +1,76 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
// Document title tag
// We combine it with defaultDocumentTitle set in `src/main.js` on router.afterEach hook
meta: {
title: 'Dashboard'
},
path: '/',
name: 'home',
component: Home
},
{
meta: {
title: 'Tables'
},
path: '/tables',
name: 'tables',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "tables" */ '../views/Tables.vue')
},
{
meta: {
title: 'Forms'
},
path: '/forms',
name: 'forms',
component: () => import(/* webpackChunkName: "forms" */ '../views/Forms.vue')
},
{
meta: {
title: 'Profile'
},
path: '/profile',
name: 'profile',
component: () => import(/* webpackChunkName: "profile" */ '../views/Profile.vue')
},
{
meta: {
title: 'New Client'
},
path: '/client/new',
name: 'client.new',
component: () => import(/* webpackChunkName: "client-form" */ '../views/ClientForm.vue')
},
{
meta: {
title: 'Edit Client'
},
path: '/client/:id',
name: 'client.edit',
component: () => import(/* webpackChunkName: "client-form" */ '../views/ClientForm.vue'),
props: true
}
]
const router = new VueRouter({
base: process.env.BASE_URL,
routes,
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
})
export default router

161
admin/src/scss/_aside.scss Normal file
View File

@ -0,0 +1,161 @@
@include desktop {
html {
&.has-aside-left {
&.has-aside-expanded {
nav.navbar, body {
padding-left: $aside-width;
}
}
nav.navbar, body {
@include transition(padding-left);
}
aside.is-placed-left {
display: block;
}
}
}
aside.aside.is-expanded {
width: $aside-width;
.menu-list {
@include icon-with-update-mark($aside-icon-width);
span.menu-item-label {
display: inline-block;
}
li.is-active {
ul {
display: block;
}
}
}
}
}
aside.aside {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 40;
height: 100vh;
padding: 0;
box-shadow: $aside-box-shadow;
background: $aside-background-color;
.aside-tools {
display: flex;
flex-direction: row;
width: 100%;
background-color: $aside-tools-background-color;
color: $aside-tools-color;
line-height: $navbar-height;
height: $navbar-height;
padding-left: $default-padding * .5;
flex: 1;
.icon {
margin-right: $default-padding * .5;
}
}
.menu-list {
li {
a {
&.has-dropdown-icon {
position: relative;
padding-right: $aside-icon-width;
.dropdown-icon {
position: absolute;
top: $size-base * .5;
right: 0;
}
}
}
ul {
display: none;
border-left: 0;
background-color: darken($base-color, 2.5%);
padding-left: 0;
margin: 0 0 $default-padding * .5;
li {
a {
padding: $default-padding * .5 0 $default-padding * .5 $default-padding * .5;
font-size: $aside-submenu-font-size;
&.has-icon {
padding-left: 0;
}
&.is-active {
&:not(:hover) {
background: transparent;
}
}
}
}
}
}
}
.menu-label {
padding: 0 $default-padding * .5;
margin-top: $default-padding * .5;
margin-bottom: $default-padding * .5;
}
}
@include touch {
#app, nav.navbar {
@include transition(margin-left);
}
aside.aside {
@include transition(left);
}
html.has-aside-mobile-transition {
body {
overflow-x: hidden;
}
body, #app, nav.navbar {
width: 100vw;
}
aside.aside {
width: $aside-mobile-width;
display: block;
left: $aside-mobile-width * -1;
.image {
img {
max-width: $aside-mobile-width * .33;
}
}
.menu-list {
li.is-active {
ul {
display: block;
}
}
a {
@include icon-with-update-mark($aside-icon-width);
span.menu-item-label {
display: inline-block;
}
}
}
}
}
html.has-aside-mobile-expanded {
#app, nav.navbar {
margin-left: $aside-mobile-width;
}
aside.aside {
left: 0;
}
}
}

48
admin/src/scss/_card.scss Normal file
View File

@ -0,0 +1,48 @@
.card:not(:last-child) {
margin-bottom: $default-padding;
}
.card {
border-radius: $radius-large;
border: $card-border;
&.has-table {
.card-content {
padding: 0;
}
.b-table {
border-radius: $radius-large;
overflow: hidden;
}
}
&.is-card-widget {
.card-content {
padding: $default-padding * .5;
}
}
.card-header {
border-bottom: 1px solid $base-color-light;
}
.card-content {
hr {
margin-left: $card-content-padding * -1;
margin-right: $card-content-padding * -1;
}
}
.is-widget-icon {
.icon {
width: 5rem;
height: 5rem;
}
}
.is-widget-label {
.subtitle {
color: $grey;
}
}
}

View File

@ -0,0 +1,14 @@
footer.footer {
.logo {
img {
width: auto;
height: $footer-logo-height;
}
}
}
@include mobile {
.footer-copyright {
text-align: center;
}
}

43
admin/src/scss/_form.scss Normal file
View File

@ -0,0 +1,43 @@
.field {
&.has-check {
.field-body {
margin-top: $default-padding * .125;
}
}
.control {
.mdi-24px.mdi-set, .mdi-24px.mdi:before {
font-size: inherit;
}
}
}
.upload {
.upload-draggable {
display: block;
}
}
.input, .textarea, select {
box-shadow: none;
&:focus, &:active {
box-shadow: none!important;
}
}
.switch input[type=checkbox]+.check:before {
box-shadow: none;
}
.switch, .b-checkbox.checkbox {
input[type=checkbox] {
&:focus + .check, &:focus:checked + .check {
box-shadow: none!important;
}
}
}
.b-checkbox.checkbox input[type=checkbox], .b-radio.radio input[type=radio] {
&+.check {
border: $checkbox-border;
}
}

View File

@ -0,0 +1,34 @@
section.hero.is-hero-bar {
background-color: $hero-bar-background;
border-bottom: $light-border;
.hero-body {
padding: $default-padding;
.level-item {
&.is-hero-avatar-item {
margin-right: $default-padding;
}
> div > .level {
margin-bottom: $default-padding * .5;
}
.subtitle + p {
margin-top: $default-padding * .5;
}
}
.button {
&.is-hero-button {
background-color: rgba($white, .5);
font-weight: 300;
@include transition(background-color);
&:hover {
background-color: $white;
}
}
}
}
}

View File

@ -0,0 +1,3 @@
section.section.is-main-section {
padding-top: $default-padding;
}

29
admin/src/scss/_misc.scss Normal file
View File

@ -0,0 +1,29 @@
.is-user-avatar {
&.has-max-width {
max-width: $size-base * 7;
}
&.is-aligned-center {
margin: 0 auto;
}
img {
margin: 0 auto;
border-radius: $radius-rounded;
}
}
.icon.has-update-mark {
position: relative;
&:after {
content: '';
width: $icon-update-mark-size;
height: $icon-update-mark-size;
position: absolute;
top: 1px;
right: 1px;
background-color: $icon-update-mark-color;
border-radius: $radius-rounded;
}
}

View File

@ -0,0 +1,13 @@
@mixin transition($t) {
transition: $t 250ms ease-in-out 50ms;
}
@mixin icon-with-update-mark ($icon-base-width) {
.icon {
width: $icon-base-width;
&.has-update-mark:after {
right: ($icon-base-width / 2) - .85;
}
}
}

View File

@ -0,0 +1,14 @@
.modal-card {
width: $modal-card-width;
}
.modal-card-foot {
background-color: $modal-card-foot-background-color;
}
@include mobile {
.modal .animation-content .modal-card {
width: $modal-card-width-mobile;
margin: 0 auto;
}
}

View File

@ -0,0 +1,123 @@
nav.navbar {
box-shadow: $navbar-box-shadow;
.navbar-item {
&.has-user-avatar {
.is-user-avatar {
margin-right: $default-padding * .5;
display: inline-flex;
width: $navbar-avatar-size;
height: $navbar-avatar-size;
}
}
&.has-divider {
border-right: $navbar-divider-border;
}
&.no-left-space {
padding-left: 0;
}
&.has-dropdown {
padding-right: 0;
padding-left: 0;
.navbar-link {
padding-right: $navbar-item-h-padding;
padding-left: $navbar-item-h-padding;
}
}
&.has-control {
padding-top: 0;
padding-bottom: 0;
}
.control {
.input {
color: $navbar-input-color;
border: 0;
box-shadow: none;
background: transparent;
&::placeholder {
color: $navbar-input-placeholder-color;
}
}
}
}
}
@include touch {
nav.navbar {
display: flex;
padding-right: 0;
.navbar-brand {
flex: 1;
&.is-right {
flex: none;
}
}
.navbar-item {
&.no-left-space-touch {
padding-left: 0;
}
}
.navbar-menu {
position: absolute;
width: 100vw;
padding-top: 0;
top: $navbar-height;
left: 0;
.navbar-item {
.icon:first-child {
margin-right: $default-padding * .5;
}
&.has-dropdown {
>.navbar-link {
background-color: $white-ter;
.icon:last-child {
display: none;
}
}
}
&.has-user-avatar {
>.navbar-link {
display: flex;
align-items: center;
padding-top: $default-padding * .5;
padding-bottom: $default-padding * .5;
}
}
}
}
}
}
@include desktop {
nav.navbar {
.navbar-item {
padding-right: $navbar-item-h-padding;
padding-left: $navbar-item-h-padding;
&:not(.is-desktop-icon-only) {
.icon:first-child {
margin-right: $default-padding * .5;
}
}
&.is-desktop-icon-only {
span:not(.icon) {
display: none;
}
}
}
}
}

158
admin/src/scss/_table.scss Normal file
View File

@ -0,0 +1,158 @@
table.table {
thead {
th {
border-bottom-width: 1px;
}
}
td, th {
&.checkbox-cell {
.b-checkbox.checkbox:not(.button) {
margin-right: 0;
width: 20px;
.control-label {
display: none;
padding: 0;
}
}
}
}
td {
.image {
margin: 0 auto;
width: $table-avatar-size;
height: $table-avatar-size;
}
&.is-progress-col {
min-width: 5rem;
vertical-align: middle;
}
}
}
.b-table {
.table {
border: 0;
border-radius: 0;
}
/* This stylizes buefy's pagination */
.table-wrapper {
margin-bottom: 0;
}
.table-wrapper + .level {
padding: $notification-padding;
padding-left: $card-content-padding;
padding-right: $card-content-padding;
margin: 0;
border-top: $base-color-light;
background: $notification-background-color;
.pagination-link {
background: $button-background-color;
color: $button-color;
border-color: $button-border-color;
&.is-current {
border-color: $button-active-border-color;
}
}
.pagination-previous, .pagination-next, .pagination-link {
border-color: $button-border-color;
color: $base-color;
&[disabled] {
background-color: transparent;
}
}
}
}
.has-thead-hidden {
thead {
display: none;
}
}
@include mobile {
.card {
&.has-table {
.b-table {
.table-wrapper + .level {
.level-left + .level-right {
margin-top: 0;
}
}
}
}
&.has-mobile-sort-spaced {
.b-table {
.field.table-mobile-sort {
padding-top: $default-padding * .5;
}
}
}
}
.b-table {
.field.table-mobile-sort {
padding: 0 $default-padding * .5;
}
.table-wrapper.has-mobile-cards {
tr {
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1);
margin-bottom: 3px!important;
}
td {
&.is-progress-col {
span, progress {
display: flex;
width: 45%;
align-items: center;
align-self: center;
}
}
&.checkbox-cell, &.is-image-cell {
border-bottom: 0!important;
}
&.checkbox-cell, &.is-actions-cell {
&:before {
display: none;
}
}
&.has-no-head-mobile {
&:before {
display: none;
}
span {
display: block;
width: 100%;
}
&.is-progress-col {
progress {
width: 100%;
}
}
&.is-image-cell {
.image {
width: $table-avatar-size-mobile;
height: auto;
margin: 0 auto $default-padding * .25;
}
}
}
}
}
}
}

View File

@ -0,0 +1,103 @@
/* We'll need some initial vars to use here */
@import "~bulma/sass/utilities/initial-variables";
/* Base: Size */
$size-base: 1rem;
$default-padding: $size-base * 1.5;
/* Default font */
$family-sans-serif: "Nunito", sans-serif;
/* Base color */
$base-color: #2e323a;
$base-color-light: rgba(24, 28, 33, .06);
/* General overrides */
$primary: $turquoise;
$body-background-color: #f8f8f8;
$link: $blue;
$link-visited: $purple;
$light-border: 1px solid $base-color-light;
$hr-height: 1px;
/* NavBar: specifics */
$navbar-input-color: $grey-darker;
$navbar-input-placeholder-color: $grey-lighter;
$navbar-box-shadow: 0 1px 0 rgba(24, 28, 33, .04);
$navbar-divider-border: 1px solid rgba($grey-lighter, .25);
$navbar-item-h-padding: $default-padding * .75;
$navbar-avatar-size: 1.75rem;
/* Aside: Bulma override */
$menu-item-radius: 0;
$menu-list-link-padding: $size-base * .5 0;
$menu-label-color: lighten($base-color, 25%);
$menu-item-color: lighten($base-color, 30%);
$menu-item-hover-color: $white;
$menu-item-hover-background-color: darken($base-color, 3.5%);
$menu-item-active-color: $white;
$menu-item-active-background-color: darken($base-color, 2.5%);
/* Aside: specifics */
$aside-width: $size-base * 14;
$aside-mobile-width: $size-base * 15;
$aside-icon-width: $size-base * 3;
$aside-submenu-font-size: $size-base * .95;
$aside-box-shadow: none;
$aside-background-color: $base-color;
$aside-tools-background-color: darken($aside-background-color, 10%);
$aside-tools-color: $white;
/* Title Bar: specifics */
$title-bar-color: $grey;
$title-bar-active-color: $black-ter;
/* Hero Bar: specifics */
$hero-bar-background: $white;
/* Card: Bulma override */
$card-shadow: none;
$card-header-shadow: none;
/* Card: specifics */
$card-border: 1px solid $base-color-light;
$card-header-border-bottom-color: $base-color-light;
/* Table: Bulma override */
$table-cell-border: 1px solid $white-bis;
/* Table: specifics */
$table-avatar-size: $size-base * 1.5;
$table-avatar-size-mobile: 25vw;
/* Form */
$checkbox-border: 1px solid $base-color;
/* Modal card: Bulma override */
$modal-card-head-background-color: $white-ter;
$modal-card-title-size: $size-base;
$modal-card-body-padding: $default-padding 20px;
$modal-card-head-border-bottom: 1px solid $white-ter;
$modal-card-foot-border-top: 0;
/* Modal card: specifics */
$modal-card-width: 40vw;
$modal-card-width-mobile: 90vw;
$modal-card-foot-background-color: $white-ter;
/* Notification: Bulma override */
$notification-padding: $default-padding * .75 $default-padding;
/* Footer: Bulma override */
$footer-background-color: $white;
$footer-padding: $default-padding * .33 $default-padding;
/* Footer: specifics */
$footer-logo-height: $size-base * 2;
/* Progress: Bulma override */
$progress-bar-background-color: $grey-lighter;
/* Icon: specifics */
$icon-update-mark-size: $size-base * .5;
$icon-update-mark-color: $yellow;

View File

@ -0,0 +1,3 @@
.is-tiles-wrapper {
margin-bottom: $default-padding;
}

View File

@ -0,0 +1,29 @@
section.section.is-title-bar {
padding: $default-padding;
border-bottom: $light-border;
ul {
li {
display: inline-block;
padding: 0 $default-padding * .5 0 0;
font-size: $default-padding;
color: $title-bar-color;
&:after {
display: inline-block;
content: '/';
padding-left: $default-padding * .5;
}
&:last-child {
padding-right: 0;
font-weight: 900;
color: $title-bar-active-color;
&:after {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,12 @@
/* Buefy & Bulma */
/* Actually we need buefy-build, but it has relative paths */
//@import "~buefy/src/scss/buefy-build";
/* This is fixed version of buefy-build */
@import "~bulma/sass/utilities/functions";
@import "~bulma/sass/utilities/initial-variables";
@import "~buefy/src/scss/utils/_all";
@import "~bulma/bulma";
@import "~buefy/src/scss/utils/variables-ext";
@import "~buefy/src/scss/buefy";

25
admin/src/scss/main.scss Normal file
View File

@ -0,0 +1,25 @@
/* Theme style (colors & sizes) */
@import "theme-default";
/* Core Libs & Lib configs */
@import "libs/all";
/* Mixins */
@import "mixins";
/* Theme components */
@import "nav-bar";
@import "aside";
@import "title-bar";
@import "hero-bar";
@import "card";
@import "table";
@import "tiles";
@import "form";
@import "main-section";
@import "modal";
@import "footer";
@import "misc";
/* Animate.css custom build (just selected animations to minimize bundle size) */
@import "../css/animate.min.css";

66
admin/src/store/index.js Normal file
View File

@ -0,0 +1,66 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
/* User */
userName: null,
userEmail: null,
userAvatar: null,
/* NavBar */
isNavBarVisible: true,
/* FooterBar */
isFooterBarVisible: true,
/* Aside */
isAsideVisible: true,
isAsideMobileExpanded: false
},
mutations: {
/* A fit-them-all commit */
basic (state, payload) {
state[payload.key] = payload.value
},
/* User */
user (state, payload) {
if (payload.name) {
state.userName = payload.name
}
if (payload.email) {
state.userEmail = payload.email
}
if (payload.avatar) {
state.userAvatar = payload.avatar
}
},
/* Aside Mobile */
asideMobileStateToggle (state, payload = null) {
const htmlClassName = 'has-aside-mobile-expanded'
let isShow
if (payload !== null) {
isShow = payload
} else {
isShow = !state.isAsideMobileExpanded
}
if (isShow) {
document.documentElement.classList.add(htmlClassName)
} else {
document.documentElement.classList.remove(htmlClassName)
}
state.isAsideMobileExpanded = isShow
}
},
actions: {
}
})

View File

@ -0,0 +1,269 @@
<template>
<div>
<title-bar :title-stack="titleStack" />
<hero-bar>
{{ heroTitle }}
<router-link slot="right" :to="heroRouterLinkTo" class="button">
{{ heroRouterLinkLabel }}
</router-link>
</hero-bar>
<section class="section is-main-section">
<notification class="is-info">
<div>
<span><b>Demo only.</b> No data will be saved/updated</span>
</div>
</notification>
<tiles>
<card-component
:title="formCardTitle"
icon="account-edit"
class="tile is-child"
>
<form @submit.prevent="submit">
<b-field label="ID" horizontal>
<b-input v-model="form.id" custom-class="is-static" readonly />
</b-field>
<hr />
<b-field label="Avatar" horizontal>
<file-picker />
</b-field>
<hr />
<b-field label="Name" message="Client name" horizontal>
<b-input
v-model="form.name"
placeholder="e.g. John Doe"
required
/>
</b-field>
<b-field label="Company" message="Client's company name" horizontal>
<b-input
v-model="form.company"
placeholder="e.g. Berton & Steinway"
required
/>
</b-field>
<b-field label="City" message="Client's city" horizontal>
<b-input
v-model="form.city"
placeholder="e.g. Geoffreyton"
required
/>
</b-field>
<b-field label="Created" horizontal>
<b-datepicker
v-model="form.created_date"
placeholder="Click to select..."
icon="calendar-today"
@input="input"
>
</b-datepicker>
</b-field>
<hr />
<b-field label="Progress" horizontal>
<b-slider v-model="form.progress" />
</b-field>
<hr />
<b-field horizontal>
<b-button
type="is-primary"
:loading="isLoading"
native-type="submit"
>Submit</b-button
>
</b-field>
</form>
</card-component>
<card-component
v-if="isProfileExists"
title="Client Profile"
icon="account"
class="tile is-child"
>
<user-avatar
:avatar="form.avatar"
class="image has-max-width is-aligned-center"
/>
<hr />
<b-field label="Name">
<b-input :value="form.name" custom-class="is-static" readonly />
</b-field>
<b-field label="Company">
<b-input :value="form.company" custom-class="is-static" readonly />
</b-field>
<b-field label="City">
<b-input :value="form.city" custom-class="is-static" readonly />
</b-field>
<b-field label="Created">
<b-input
:value="createdReadable"
custom-class="is-static"
readonly
/>
</b-field>
<hr />
<b-field label="Progress">
<progress
class="progress is-small is-primary"
:value="form.progress"
max="100"
>{{ form.progress }}</progress
>
</b-field>
</card-component>
</tiles>
</section>
</div>
</template>
<script>
import axios from 'axios'
import dayjs from 'dayjs'
import find from 'lodash/find'
import TitleBar from '@/components/TitleBar'
import HeroBar from '@/components/HeroBar'
import Tiles from '@/components/Tiles'
import CardComponent from '@/components/CardComponent'
import FilePicker from '@/components/FilePicker'
import UserAvatar from '@/components/UserAvatar'
import Notification from '@/components/Notification'
export default {
name: 'ClientForm',
components: {
UserAvatar,
FilePicker,
CardComponent,
Tiles,
HeroBar,
TitleBar,
Notification
},
props: {
id: {
type: [String, Number],
default: null
}
},
data () {
return {
isLoading: false,
form: this.getClearFormObject(),
createdReadable: null,
isProfileExists: false
}
},
computed: {
titleStack () {
let lastCrumb
if (this.isProfileExists) {
lastCrumb = this.form.name
} else {
lastCrumb = 'New client'
}
return ['Admin', 'Clients', lastCrumb]
},
heroTitle () {
if (this.isProfileExists) {
return this.form.name
} else {
return 'Create Client'
}
},
heroRouterLinkTo () {
if (this.isProfileExists) {
return { name: 'client.new' }
} else {
return '/'
}
},
heroRouterLinkLabel () {
if (this.isProfileExists) {
return 'New client'
} else {
return 'Dashboard'
}
},
formCardTitle () {
if (this.isProfileExists) {
return 'Edit Client'
} else {
return 'New Client'
}
}
},
watch: {
id (newValue) {
this.isProfileExists = false
if (!newValue) {
this.form = this.getClearFormObject()
} else {
this.getData()
}
}
},
created () {
this.getData()
},
methods: {
getClearFormObject () {
return {
id: null,
name: null,
company: null,
city: null,
created_date: new Date(),
created_mm_dd_yyyy: null,
progress: 0
}
},
getData () {
if (this.$route.params.id) {
axios
.get(`${this.$router.options.base}data-sources/clients.json`)
.then((r) => {
const item = find(
r.data.data,
(item) => item.id === parseInt(this.$route.params.id)
)
if (item) {
this.isProfileExists = true
this.form = item
this.form.created_date = new Date(item.created_mm_dd_yyyy)
this.createdReadable = dayjs(
new Date(item.created_mm_dd_yyyy)
).format('MMM D, YYYY')
} else {
this.$router.push({ name: 'client.new' })
}
})
.catch((e) => {
this.$buefy.toast.open({
message: `Error: ${e.message}`,
type: 'is-danger',
queue: false
})
})
}
},
input (v) {
this.createdReadable = dayjs(v).format('MMM D, YYYY')
},
submit () {
this.isLoading = true
setTimeout(() => {
this.isLoading = false
this.$buefy.snackbar.open({
message: 'Demo only',
queue: false
})
}, 500)
}
}
}
</script>

187
admin/src/views/Forms.vue Normal file
View File

@ -0,0 +1,187 @@
<template>
<div>
<title-bar :title-stack="titleStack" />
<hero-bar>
Forms
<router-link slot="right" to="/" class="button">
Dashboard
</router-link>
</hero-bar>
<section class="section is-main-section">
<card-component title="Forms" icon="ballot">
<form @submit.prevent="submit">
<b-field label="From" horizontal>
<b-field>
<b-input
v-model="form.name"
icon="account"
placeholder="Name"
name="name"
required
/>
</b-field>
<b-field>
<b-input
v-model="form.email"
icon="email"
type="email"
placeholder="E-mail"
name="email"
required
/>
</b-field>
</b-field>
<b-field message="Do not enter the leading zero" horizontal>
<b-field>
<p class="control">
<a class="button is-static">
+44
</a>
</p>
<b-input v-model="form.phone" type="tel" name="phone" expanded />
</b-field>
</b-field>
<b-field label="Department" horizontal>
<b-select
v-model="form.department"
placeholder="Select a department"
required
>
<option
v-for="(department, index) in departments"
:key="index"
:value="department"
>
{{ department }}
</option>
</b-select>
</b-field>
<hr />
<b-field label="Subject" message="Message subject" horizontal>
<b-input
v-model="form.subject"
placeholder="e.g. Partnership proposal"
required
/>
</b-field>
<b-field
label="Question"
message="Your question. Max 255 characters"
horizontal
>
<b-input
v-model="form.question"
type="textarea"
placeholder="Explain how we can help you"
maxlength="255"
required
/>
</b-field>
<hr />
<b-field horizontal>
<b-field grouped>
<div class="control">
<b-button native-type="submit" type="is-primary"
>Submit</b-button
>
</div>
<div class="control">
<b-button type="is-primary is-outlined" @click="reset"
>Reset</b-button
>
</div>
</b-field>
</b-field>
</form>
</card-component>
<card-component title="Custom elements" icon="ballot-outline">
<b-field label="Checkbox" class="has-check" horizontal>
<checkbox-picker
v-model="customElementsForm.checkbox"
:options="{ lorem: 'Lorem', ipsum: 'Ipsum', dolore: 'Dolore' }"
type="is-primary"
/>
</b-field>
<hr />
<b-field label="Radio" class="has-check" horizontal>
<radio-picker
v-model="customElementsForm.radio"
:options="{ one: 'One', two: 'Two' }"
></radio-picker>
</b-field>
<hr />
<b-field label="Switch" horizontal>
<b-switch v-model="customElementsForm.switch">
Default
</b-switch>
</b-field>
<hr />
<b-field label="File" horizontal>
<file-picker v-model="customElementsForm.file" />
</b-field>
</card-component>
</section>
</div>
</template>
<script>
import mapValues from 'lodash/mapValues'
import TitleBar from '@/components/TitleBar'
import CardComponent from '@/components/CardComponent'
import CheckboxPicker from '@/components/CheckboxPicker'
import RadioPicker from '@/components/RadioPicker'
import FilePicker from '@/components/FilePicker'
import HeroBar from '@/components/HeroBar'
export default {
name: 'Forms',
components: {
HeroBar,
FilePicker,
RadioPicker,
CheckboxPicker,
CardComponent,
TitleBar
},
data () {
return {
isLoading: false,
form: {
name: null,
email: null,
phone: null,
department: null,
subject: null,
question: null
},
customElementsForm: {
checkbox: [],
radio: null,
switch: true,
file: null
},
departments: ['Business Development', 'Marketing', 'Sales']
}
},
computed: {
titleStack () {
return ['Admin', 'Forms']
}
},
methods: {
submit () {},
reset () {
this.form = mapValues(this.form, (item) => {
if (item && typeof item === 'object') {
return []
}
return null
})
this.$buefy.snackbar.open({
message: 'Reset successfully',
queue: false
})
}
}
}
</script>

162
admin/src/views/Home.vue Normal file
View File

@ -0,0 +1,162 @@
<template>
<div>
<title-bar :title-stack="titleStack" />
<hero-bar :has-right-visible="false">
Dashboard
</hero-bar>
<section class="section is-main-section">
<tiles>
<card-widget
class="tile is-child"
type="is-primary"
icon="account-multiple"
:number="512"
label="Clients"
/>
<card-widget
class="tile is-child"
type="is-info"
icon="cart-outline"
:number="7770"
prefix="$"
label="Sales"
/>
<card-widget
class="tile is-child"
type="is-success"
icon="chart-timeline-variant"
:number="256"
suffix="%"
label="Performance"
/>
</tiles>
<card-component
title="Performance"
icon="finance"
header-icon="reload"
@header-icon-click="fillChartData"
>
<div v-if="defaultChart.chartData" class="chart-area">
<line-chart
ref="bigChart"
style="height: 100%;"
chart-id="big-line-chart"
:chart-data="defaultChart.chartData"
:extra-options="defaultChart.extraOptions"
>
</line-chart>
</div>
</card-component>
<card-component title="Clients" class="has-table has-mobile-sort-spaced">
<clients-table-sample
:data-url="`${$router.options.base}data-sources/clients.json`"
/>
</card-component>
</section>
</div>
</template>
<script>
// @ is an alias to /src
import * as chartConfig from '@/components/Charts/chart.config'
import TitleBar from '@/components/TitleBar'
import HeroBar from '@/components/HeroBar'
import Tiles from '@/components/Tiles'
import CardWidget from '@/components/CardWidget'
import CardComponent from '@/components/CardComponent'
import LineChart from '@/components/Charts/LineChart'
import ClientsTableSample from '@/components/ClientsTableSample'
export default {
name: 'Home',
components: {
ClientsTableSample,
LineChart,
CardComponent,
CardWidget,
Tiles,
HeroBar,
TitleBar
},
data () {
return {
defaultChart: {
chartData: null,
extraOptions: chartConfig.chartOptionsMain
}
}
},
computed: {
titleStack () {
return ['Admin', 'Dashboard']
}
},
mounted () {
this.fillChartData()
},
methods: {
randomChartData (n) {
const data = []
for (let i = 0; i < n; i++) {
data.push(Math.round(Math.random() * 200))
}
return data
},
fillChartData () {
this.defaultChart.chartData = {
datasets: [
{
fill: false,
borderColor: chartConfig.chartColors.default.primary,
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: chartConfig.chartColors.default.primary,
pointBorderColor: 'rgba(255,255,255,0)',
pointHoverBackgroundColor: chartConfig.chartColors.default.primary,
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: this.randomChartData(9)
},
{
fill: false,
borderColor: chartConfig.chartColors.default.info,
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: chartConfig.chartColors.default.info,
pointBorderColor: 'rgba(255,255,255,0)',
pointHoverBackgroundColor: chartConfig.chartColors.default.info,
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: this.randomChartData(9)
},
{
fill: false,
borderColor: chartConfig.chartColors.default.danger,
borderWidth: 2,
borderDash: [],
borderDashOffset: 0.0,
pointBackgroundColor: chartConfig.chartColors.default.danger,
pointBorderColor: 'rgba(255,255,255,0)',
pointHoverBackgroundColor: chartConfig.chartColors.default.danger,
pointBorderWidth: 20,
pointHoverRadius: 4,
pointHoverBorderWidth: 15,
pointRadius: 4,
data: this.randomChartData(9)
}
],
labels: ['01', '02', '03', '04', '05', '06', '07', '08', '09']
}
}
}
}
</script>

View File

@ -0,0 +1,57 @@
<template>
<div>
<title-bar :title-stack="titleStack" />
<hero-bar>
Profile
<router-link slot="right" to="/" class="button">
Dashboard
</router-link>
</hero-bar>
<section class="section is-main-section">
<tiles>
<profile-update-form class="tile is-child" />
<card-component title="Profile" icon="account" class="tile is-child">
<user-avatar class="image has-max-width is-aligned-center" />
<hr />
<b-field label="Name">
<b-input :value="userName" custom-class="is-static" readonly />
</b-field>
<hr />
<b-field label="E-mail">
<b-input :value="userEmail" custom-class="is-static" readonly />
</b-field>
</card-component>
</tiles>
<password-update-form />
</section>
</div>
</template>
<script>
import { mapState } from 'vuex'
import CardComponent from '@/components/CardComponent'
import TitleBar from '@/components/TitleBar'
import HeroBar from '@/components/HeroBar'
import ProfileUpdateForm from '@/components/ProfileUpdateForm'
import PasswordUpdateForm from '@/components/PasswordUpdateForm'
import Tiles from '@/components/Tiles'
import UserAvatar from '@/components/UserAvatar'
export default {
name: 'Profile',
components: {
UserAvatar,
Tiles,
PasswordUpdateForm,
ProfileUpdateForm,
HeroBar,
TitleBar,
CardComponent
},
computed: {
titleStack () {
return ['Admin', 'Profile']
},
...mapState(['userName', 'userEmail'])
}
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<div>
<title-bar :title-stack="titleStack" />
<hero-bar>
Tables
<router-link slot="right" to="/" class="button">
Dashboard
</router-link>
</hero-bar>
<section class="section is-main-section">
<notification class="is-info">
<div>
<b-icon icon="buffer" custom-size="default" />
<b>Sorted and paginated table.</b>&nbsp;Based on Buefy's table.
</div>
</notification>
<card-component
class="has-table has-mobile-sort-spaced"
title="Clients"
icon="account-multiple"
>
<clients-table-sample
:data-url="`${$router.options.base}data-sources/clients.json`"
:checkable="true"
/>
</card-component>
<hr />
<notification class="is-info">
<div>
<b-icon icon="buffer" custom-size="default" />
<b>Tightly wrapped</b> &mdash; table header becomes card header
</div>
</notification>
<card-component class="has-table has-mobile-sort-spaced">
<clients-table-sample
:data-url="`${$router.options.base}data-sources/clients.json`"
:checkable="true"
/>
</card-component>
<hr />
<notification class="is-info">
<div>
<b-icon icon="buffer" custom-size="default" />
<b>Empty table.</b> When there's nothing to show
</div>
</notification>
<card-component class="has-table has-thead-hidden">
<clients-table-sample />
</card-component>
</section>
</div>
</template>
<script>
import Notification from '@/components/Notification'
import ClientsTableSample from '@/components/ClientsTableSample'
import CardComponent from '@/components/CardComponent'
import TitleBar from '@/components/TitleBar'
import HeroBar from '@/components/HeroBar'
export default {
name: 'Tables',
components: {
HeroBar,
TitleBar,
CardComponent,
ClientsTableSample,
Notification
},
computed: {
titleStack () {
return ['Admin', 'Tables']
}
}
}
</script>

15
admin/vue.config.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
publicPath: process.env.DEPLOY_ENV === 'GH_PAGES'
? '/admin-one-vue-bulma-dashboard/'
: '/',
// Remove moment.js from chart.js
// https://www.chartjs.org/docs/latest/getting-started/integration.html#bundlers-webpack-rollup-etc
configureWebpack: config => {
return {
externals: {
moment: 'moment'
}
}
}
}

9269
admin/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -114,4 +114,4 @@
@include text($font--role-emphasis, 1rem);
}
}
</style>
</style>

View File

@ -37,8 +37,7 @@
if(this.post.User) {
return this.post.User.username
} else {
return '[deleted]'
}
return '[deleted]'}
}
},
methods: {

View File

@ -69,8 +69,7 @@
if(this.thread.Posts[1].User) {
return this.thread.Posts[1].User.username
} else {
return '[deleted]'
}
return '[deleted]'}
}
},
methods: {

View File

@ -163,8 +163,7 @@
if(this.post.User) {
return this.post.User.username
} else {
return '[deleted]'
}
return '[deleted]'}
},
showActions () {
return this.hover || this.showShareModal || this.showReportPostModal

View File

@ -128,8 +128,7 @@ export default {
if(this.post.User) {
return this.post.User.username
} else {
return '[deleted]'
}
return '[deleted]'}
},
showActions () {
return this.hover || this.showShareModal || this.showReportPostModal

View File

@ -121,13 +121,13 @@
//Provide username text if user deleted for post
//or flaggedByUser
if(!report.Post.User) {
clone.PostUserUsername = '[Deleted]';
clone.PostUserUsername = '[deleted]';
} else {
clone.PostUserUsername = report.Post.User.username;
}
if(!report.FlaggedByUser) {
clone.FlaggedByUserUsername = '[Deleted]';
clone.FlaggedByUserUsername = '[deleted]';
} else {
clone.FlaggedByUserUsername = report.FlaggedByUser.username;
}

View File

@ -206,18 +206,13 @@
this.axios.post('/api/v1/forums/thread', {
name: this.name,
category: this.selectedCategory
category: this.selectedCategory,
content: this.editor,
mentions: this.mentions
}).then(res => {
thread = res.data
let ajax = []
ajax.push(
this.axios.post('/api/v1/forums/post', {
threadId: res.data.id,
content: this.editor,
mentions: this.mentions
})
)
if(this.showPoll) {
ajax.push(

View File

@ -1,16 +1,9 @@
<template>
<main>
<br>
<center>
<div class="is-5 column">
<scroll-load
class='category_widget__box'
@loadNext='fetchData'
:loading='loading'
query-selector='.admin_users'
:padding-bottom='100'
>
<div class="card" v-for='user in users' :key='"user-row" + user.username' v-show="user && !user.hidden">
<div class="container">
<div class="columns">
<div class="column is-one-quarter" style="border-width: 5px;" v-for='user in users' :key='"user-row" + user.username' v-show="user && !user.hidden">
<header class="card-header">
<figure class="image is-48x48">
<avatar-icon :user="user"/>
@ -42,31 +35,34 @@
</header>
<div class="card-content">
<div class="content" v-if="user.description">
{{user.description}}<br/>
{{user.description.substring(0,50)+".."}}<br/>
<time v-bind:datetime="user.createdAt">{{user.createdAt | formatDate}}</time>
</div>
<div class="content" v-if="!user.description">
This user doesn't have a user description yet!<br/>
No description set.<br><br>
<time v-bind:datetime="user.createdAt">{{user.createdAt | formatDate}}</time>
</div>
<div>
<b-progress :value="75" size="is-medium" show-value>
Level 1
</b-progress>
</div>
</div>
<footer class="card-footer">
<router-link :to='"/user/" + user.username' class="card-footer-item">View Profile</router-link>
</footer>
</div>
</scroll-load>
</div>
</div>
<p name='fade' mode='out-in'>
<center><loading-message key='loading' v-if='loading'></loading-message></center>
<center><div class='overlay_message' v-if='!loading && !users.length'>
Something went wrong while loading the users, check your internet connection, or check the <a href="https://status.troplo.com">Service Status</a>
</div></center></p>
</center>
</main>
</template>
<script>
import LoadingMessage from '../LoadingMessage';
import ScrollLoad from '../ScrollLoad';
import AvatarIcon from '../AvatarIcon';
import throttle from 'lodash.throttle';
import AjaxErrorHandler from '../../assets/js/errorHandler';
@ -75,7 +71,6 @@ export default {
name: 'UserList',
components: {
LoadingMessage,
ScrollLoad,
AvatarIcon
},
data () {

View File

@ -71,34 +71,6 @@ module.exports = (sequelize, DataTypes) => {
return false
}
},
async DisableLogin (username) {
let { User } = sequelize.models
if(username) {
let user = await User.findOne({ where: {
username
}})
if(user && user.admin) return false
}
if(!users.length) return false
let ban = await Ban.findOne({ where: {
UserId: {
$in: users.map(u => u.id)
},
DisableLogin: true
} })
if(ban) {
throw Errors.sequelizeValidation(sequelize.Sequelize, {
error: 'This IP has been banned from creating accounts or logging in, reason: ' + ban.message ||
'This IP has been banned from creating accounts or logging in'
})
} else {
return false
}
},
async isIpBanned (ip, username) {
let { User, Ip } = sequelize.models

View File

@ -44,6 +44,7 @@
"mysql": "^2.13.0",
"mysql2": "^1.7.0",
"node-email-verification": "^0.0.0",
"passport": "^0.4.1",
"passport-local": "^1.0.0",
"passport-local-sequelize": "^0.9.0",
"postgres": "^1.0.2",

View File

@ -67,7 +67,6 @@ router.post('/', async (req, res, next) => {
try {
//Will throw an error if banned
await Ban.ReadOnlyMode(req.session.username)
await Ban.DisableLogin(req.session.username)
if(req.body.mentions) {
uniqueMentions = Notification.filterMentions(req.body.mentions)

View File

@ -2,7 +2,7 @@ let express = require('express')
let router = express.Router()
const Errors = require('../lib/errors.js')
let { User, Thread, Category, Post, Ban, Report, Sequelize } = require('../models')
let { User, Thread, Notification, Category, Post, Ban, Report, Sequelize } = require('../models')
let pagination = require('../lib/pagination.js')
router.get('/:thread_id', async (req, res, next) => {
@ -37,8 +37,6 @@ router.post('/', async (req, res, next) => {
try {
await Ban.ReadOnlyMode(req.session.username)
await Ban.DisableLogin(req.session.username)
let category = await Category.findOne({ where: {
value: req.body.category
}})
@ -55,7 +53,6 @@ router.post('/', async (req, res, next) => {
throw Errors.verifyEmail
}
let thread = await Thread.create({
name: req.autosan.body.name
})
@ -63,19 +60,73 @@ router.post('/', async (req, res, next) => {
await thread.setCategory(category)
await thread.setUser(user)
req.app.get('io').to('index').emit('new thread', {
name: category.name,
value: category.value
})
if(req.body.mentions) {
uniqueMentions = Notification.filterMentions(req.body.mentions)
}
if(!user.emailVerified) {
throw Errors.verifyEmail
}
threadId = await Thread.findOne({ where: {
id: Thread.id
}})
user = await User.findOne({ where: {
username: req.session.username
}})
if(!threadId) throw Errors.sequelizeValidation(Sequelize, {
error: 'thread does not exist',
path: 'id'
})
if(threadId.locked) throw Errors.threadLocked
post = await Post.create({ content: req.autosan.body.content, postNumber: thread.postsCount })
await post.setUser(user)
await post.setThread(thread)
await thread.increment('postsCount')
if(uniqueMentions.length) {
let ioUsers = req.app.get('io-users')
let io = req.app.get('io')
for(const mention of uniqueMentions) {
let mentionNotification = await Notification.createPostNotification({
usernameTo: mention,
userFrom: user,
type: 'mention',
post
})
if(mentionNotification) {
await mentionNotification.emitNotificationMessage(ioUsers, io)
}
}
}
res.json(await post.reload({
include: Post.includeOptions()
}))
res.json(await thread.reload({
include: [
{ model: User, attributes: ['username', 'createdAt', 'updatedAt', 'id'] },
Category
]
}))
req.app.get('io').to('index').emit('new thread', {
name: category.name,
value: category.value
req.app.get('io').to('thread/' + thread.id).emit('new post', {
postNumber: thread.postsCount,
content: post.content,
username: user.username
})
} catch (e) { next(e) }
} catch (e) { next(e) }
})
//Only admin routes

View File

@ -260,7 +260,6 @@ router.get('/:username', async (req, res, next) => {
router.post('/login', async (req, res, next) => {
try {
await Ban.isIpBanned(req.ip, req.body.email)
await Ban.DisableLogin(req.body.username)
let user = await User.findOne({ where: {
username: req.body.username

View File

@ -64,7 +64,7 @@ var recaptcha = new Recaptcha('6LdlbrwZAAAAAKvtcVQhVl_QaNOqmQ4PgyW3SKHy', '6Ldlb
const expAutoSan = require('express-autosanitizer');
const swaggerUi = require('swagger-ui-express');
let path = require('path')
const passport = require('passport');
let session = expressSession({
secret: config.sessionSecret,
@ -82,7 +82,8 @@ app.use(bodyParser.json({ limit: '5mb' }))
app.use(bodyParser.urlencoded({ extended: true }))
app.use(session)
app.use(expAutoSan.all);
app.use(passport.initialize());
app.use(passport.session());
if(process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'production') {
app.use(require('morgan')('dev'))
}

View File

@ -3311,6 +3311,14 @@ passport-strategy@1.x.x:
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
passport@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
dependencies:
passport-strategy "1.x.x"
pause "0.0.1"
path-exists@^3.0.0:
version "3.0.0"
resolved "https://npm.open-registry.dev/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@ -3343,6 +3351,11 @@ path-type@^2.0.0:
dependencies:
pify "^2.0.0"
pause@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://npm.open-registry.dev/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"