This commit is contained in:
artem 2024-04-14 15:35:11 +03:00
commit 23f15dac6b
199 changed files with 79375 additions and 0 deletions

130
.gitignore vendored Normal file
View File

@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

73
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,73 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Epicmax LLC
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.

112
README.md Normal file
View File

@ -0,0 +1,112 @@
<p align="center">
<a href="https://vuestic.dev" target="_blank">
<img alt="Vuestic UI Logo" width="220" src="./.github/assets/vuestic-admin-logo.png">
</a>
</p>
<p align="center">
Free and beautiful Admin Template utilizing Vue 3, Vite, Pinia, and Tailwind CSS. Designed for building efficient, responsive, and fast-loading admin interfaces.</br>
Developed by <a href="https://epicmax.co">Epicmax</a>.</br>
Based on <a href="https://ui.vuestic.dev">Vuestic UI</a> library.
</p>
<p align="center">
<a href="https://admin-demo.vuestic.dev"> Live Demo </a> |
<a href="https://admin.vuestic.dev/"> About Vuestic Admin </a> |
<a href="https://ui.vuestic.dev/">Vuestic UI documentation</a>
</p>
> Vuestic Admin is built with [Vuestic UI](https://ui.vuestic.dev). See our
> <a href="https://github.com/epicmaxco/vuestic-ui/issues">issues</a>,
> <a href="https://ui.vuestic.dev/en/contribution/guide">contributing guide</a> and join discussions on our
> <a href="https://discord.gg/jTKTjj2weV">Discord server</a> to help us improve Vuestic Admin & Vuestic UI experience.
<p align="center">
<a href="https://admin.vuestic.dev" target="_blank">
<img src="./public/vuestic-admin-image.png" align="center" width="888px"/>
</a>
</p>
### Quick start
Use following command to quickly scaffold new [Vuestic Admin](https://admin-demo.vuestic.dev) or empty Vite or Nuxt project with [Vuestic UI](https://ui.vuestic.dev).
```bash
npm create vuestic@latest
```
After [Vuestic Admin](https://admin.vuestic.dev) is installed, run `npm install` to install dependcies, then run `npm run dev` to start local development server.
### Documentation
Documentation, guides, examples and tutorials are available on [ui.vuestic.dev](https://ui.vuestic.dev)
### Official Discord Server
Ask questions at the official community [discord server](https://discord.gg/jTKTjj2weV)
### Features
- **Vue 3, Vite, Pinia, and Tailwind CSS -** Fast and efficient development
- **Dark Theme -** Modern and eye-catching
- **Global Configuration -** Effortless customization
- **Accessibility -** Inclusive and user-friendly
- **i18n Integration -** Easy localization for global reach
- **Educational Resource -** Ideal for learning and improving skills
- **Responsive Design -** Adapts seamlessly to all devices
- **Professional Support -** Reliable help from the experts
- **Highly Customizable -** Tailor to your projects style
### Contributing
Thanks for all your wonderful PRs, issues and ideas.
<a href="https://github.com/epicmaxco/vuestic-admin/graphs/contributors">
<img src="https://opencollective.com/vuestic-admin/contributors.svg?width=890&button=false" />
</a>
<br>
Youre always welcome to join: check out
our <a href="https://ui.vuestic.dev/en/contribution/guide">
contribution guides</a>
, [open issues](https://github.com/epicmaxco/vuestic-ui/issues)
and [Discord server](https://discord.gg/jTKTjj2weV)
### Partners & Sponsors ❤️
<img src="./.github/assets/sponsors.png" loading="lazy" alt="Epicmax, vuejobs, ag-grid, flatlogic, browserstack and jetbrains" width="400px">
Become a partner: [hello@epicmax.co](mailto:hello@epicmax.co)
### Can I hire you guys?
[Epicmax](https://epicmax.co) is committed to Open Source from its beginning. Vuestic Admin was created and backed by Epicmax, and is supported through all the years.
With 6+ years of dedicated work on both commercial and open-source projects, and more than 47 clients worldwide across various fields, Epicmax has deep expertise in frontend development, especially in Vue.js. We regularly conduct code audits for our projects and now excited to offer this service not only to our existing clients but to anyone looking to understand the state of their frontend code and ensure it's secure and up-to-date!
You can request a consultation or order web development services by Epicmax via this [form](https://epicmax.co/contacts) 😎
Say hi: <a href="mailto:hello@epicmax.co">hello@epicmax.co</a>. We will be happy to work with you!
[Other work](https://epicmax.co) weve done 🤘
[Meet the Team](https://ui.vuestic.dev/introduction/team)
### Awards
<a href="https://flatlogic.com/templates/vuestic-vue-free-admin" target="_blank">
<img src="https://i.imgur.com/ZeQPZ3Q.png" align="center" width="150px"/>
</a>
<p>
By <a href="https://flatlogic.com/templates/vuestic-vue-free-admin" target="_blank">@flatlogic</a> marketplace
</p>
### Follow us
Stay up to date with the latest Vuestic news! Follow us
on [Twitter](https://twitter.com/vuestic_ui)
or [Linkedin](https://www.linkedin.com/company/18509340)
### License
[MIT](https://github.com/epicmaxco/vuestic-admin/blob/master/LICENSE) license.

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,300..600,0,0"
/>
<link rel="icon" href="/favicon.ico" />
<title>Vuestic Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

16058
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

76
package.json Normal file
View File

@ -0,0 +1,76 @@
{
"name": "vuestic-admin",
"private": true,
"version": "3.1.0",
"scripts": {
"prepare": "husky install",
"dev": "vite",
"build": "npm run lint && vue-tsc --noEmit && vite build",
"build:ci": "vite build",
"start:ci": "serve -s ./dist",
"prelint": "npm run format",
"lint": "eslint \"./src/**/*.{ts,js,vue}\" --fix",
"format": "prettier --write .",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"lint-staged": {
"./src/**/*.{ts,js,vue}": [
"npm run lint"
]
},
"dependencies": {
"@gtm-support/vue-gtm": "^2.0.0",
"@vuestic/tailwind": "^0.1.3",
"@vueuse/core": "^10.6.1",
"axios": "^1.6.8",
"chart.js": "^4.4.1",
"chartjs-chart-geo": "^4.2.8",
"epic-spinners": "^2.0.0",
"flag-icons": "^6.15.0",
"ionicons": "^4.6.3",
"medium-editor": "^5.23.3",
"pinia": "^2.1.7",
"register-service-worker": "^1.7.1",
"sass": "^1.69.5",
"serve": "^14.2.1",
"vue": "3.3.9",
"vue-chartjs": "^5.3.0",
"vue-i18n": "^9.6.2",
"vue-router": "^4.2.5",
"vuestic-ui": "^1.9.0"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@storybook/addon-essentials": "^7.4.6",
"@storybook/addon-interactions": "^7.4.6",
"@storybook/addon-links": "^7.4.6",
"@storybook/blocks": "^7.4.6",
"@storybook/testing-library": "^0.2.2",
"@storybook/vue3": "^7.4.6",
"@storybook/vue3-vite": "^7.4.6",
"@types/medium-editor": "^5.0.5",
"@types/node": "^20.9.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.13.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-vue": "^9.18.1",
"husky": "^8.0.1",
"lint-staged": "^15.1.0",
"postcss": "^8.4.21",
"prettier": "^3.1.0",
"storybook": "^7.4.6",
"tailwindcss": "^3.4.0",
"typescript": "^5.2.2",
"vite": "^4.4.6",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.22"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/site.webmanifest Normal file
View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

18
src/App.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<RouterView />
</template>
<style lang="scss">
@import 'scss/main.scss';
#app {
font-family: 'Inter', Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-width: 20rem;
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<svg fill="none" height="200" viewBox="0 0 200 200" width="200" xmlns="http://www.w3.org/2000/svg">
<path
clip-rule="evenodd"
d="M141.03 63.87c2.58-3.14 5.5-6.4 7.4-8.46l-1.4-1.3a215.73 215.73 0 0 0-7.47 8.55 90.25 90.25 0 0 0-3.51 4.5 17.33 17.33 0 0 0-2.08 3.46l1.79.67c.23-.63.87-1.68 1.83-3.02a87.85 87.85 0 0 1 3.44-4.4ZM94.82 51.4c2-2.4 4.27-4.9 5.74-6.46l-1.39-1.3a166.85 166.85 0 0 0-5.81 6.53 69.06 69.06 0 0 0-2.74 3.46 13.4 13.4 0 0 0-1.65 2.7l1.78.68c.18-.45.65-1.23 1.4-2.26.73-1 1.66-2.16 2.67-3.35Z"
fill="var(--va-danger)"
fill-rule="evenodd"
/>
<path
clip-rule="evenodd"
d="M140.87 63.63a82.26 82.26 0 0 1 6.96 4.97l1.2-1.48a84.18 84.18 0 0 0-7.13-5.1c-2.88-1.83-6.02-3.6-8.41-4.36l-.58 1.82c2.13.67 5.1 2.32 7.96 4.15ZM94.05 50.65c1.9 1.73 3.85 3.5 5.14 4.79l1.35-1.35a165.6 165.6 0 0 0-5.49-5.1c-1.81-1.65-3.51-3.19-4.51-4.19l-1.35 1.35c1.04 1.03 2.77 2.6 4.56 4.23l.3.27ZM116 88.33l.15.28c.65 1.2 1.19 2.47 1.72 3.78l.16.38c.48 1.18.97 2.38 1.56 3.55.12.24.25.5.4.7.14.22.33.44.58.6.59.38 1.23.28 1.85-.03a20.32 20.32 0 0 0 3.98-2.82c1.14-.96 2.22-1.86 3.45-2.52a.72.72 0 0 0-.68-1.26c-1.36.74-2.54 1.72-3.65 2.65l-.15.13c-1.18.98-2.3 1.9-3.59 2.54-.2.1-.32.12-.37.13-.04 0-.05 0-.06-.02a.72.72 0 0 1-.18-.2c-.1-.13-.19-.3-.3-.54a50.44 50.44 0 0 1-1.67-3.83 39.3 39.3 0 0 0-1.8-3.93l-.14-.25c-.37-.69-.92-1.72-1.6-2.43-.39-.42-.9-.8-1.51-.93a2.25 2.25 0 0 0-1.92.59c-.62.52-1.22 1.09-1.8 1.64l-.66.62c-.8.75-1.6 1.44-2.46 2-1.4.9-2.66.83-3.86.3-1.26-.57-2.45-1.66-3.62-2.83l-.05-.05c-.44-.44-.84-.84-1.22-1.16-.38-.32-.8-.6-1.27-.73-1.03-.29-1.96.24-2.99 1.15-.15.13-.3.32-.45.5l-.5.67-.26.38-.9 1.24c-.42.55-.81 1.02-1.14 1.32-.15.13-.25.2-.32.24A18.4 18.4 0 0 1 86.39 87l-.17-.17a6.66 6.66 0 0 0-.82-.75c-.27-.2-.7-.46-1.2-.4-.6.08-.95.53-1.14 1.02a234.7 234.7 0 0 0-1.76 5.34l-.02.07v.13a.72.72 0 0 0 1.41.11 7.87 7.87 0 0 1 .2-.65 230.75 230.75 0 0 1 1.53-4.56l.14.1a5.35 5.35 0 0 1 .76.72 19.49 19.49 0 0 0 4.9 3.57c.38.19.77.1 1.04-.01.27-.12.52-.3.74-.5.44-.4.9-.96 1.33-1.52a112.24 112.24 0 0 1 1.65-2.28c.14-.18.23-.28.27-.31.99-.88 1.4-.92 1.66-.84.18.05.4.17.74.45.32.27.68.64 1.16 1.1v.02c1.17 1.17 2.53 2.43 4.06 3.12a5.2 5.2 0 0 0 5.2-.4c.99-.63 1.86-1.4 2.67-2.15l.7-.66c.57-.55 1.13-1.07 1.7-1.56.35-.29.58-.3.73-.27.2.04.46.18.76.5.53.56.99 1.4 1.37 2.11Zm-31.53-1.27v.01ZM82 91.5Z"
fill="var(--va-danger)"
fill-rule="evenodd"
/>
<path
d="M128.72 17.58c10.44 3.5 21.51 8.9 29.59 16.56 8.1 7.69 14.21 17.09 17.58 26.53 3.38 9.44 3.99 18.86 1.2 26.64-1.36 3.8-4.73 7.68-9.2 11.44-4.47 3.74-9.94 7.28-15.4 10.42a234.47 234.47 0 0 1-15.23 7.92c-1.98.95-3.75 1.77-5.2 2.44-1.69.8-2.94 1.38-3.55 1.7-2.26 1.22-4.55 2.82-6.13 5.04a11.17 11.17 0 0 0-1.82 8.65c.45 2.74 2.96 12.33 5.88 23.12 2.93 10.82 6.3 22.94 8.51 30.79l1.55-.44a3483.9 3483.9 0 0 1-10.24-37.22c3.95.12 12.67.68 24.49 2.3 8.19 1.12 15.55 5.5 21.35 10.1a96.82 96.82 0 0 1 9.54 8.8c-1.56 2.16-2.97 5.26-3.96 9.6l1.56.35c.92-4.05 2.19-6.86 3.52-8.78l1.12 1.13c.92.89 1.69 1.5 2.34 1.82.66.33 1.37.46 2 .1.57-.33.84-.94 1-1.43.17-.53.28-1.15.38-1.73l-1.58-.28c-.1.61-.2 1.12-.33 1.52-.13.42-.25.52-.27.53h-.06c-.07 0-.2-.03-.42-.14a8.85 8.85 0 0 1-1.94-1.54c-.33-.32-.74-.74-1.24-1.26 2.27-2.52 4.56-2.84 5.66-2.6l.34-1.57c-1.82-.4-4.57.22-7.1 3.02a98.22 98.22 0 0 0-9.57-8.8c-5.9-4.68-13.53-9.26-22.13-10.44-12.34-1.69-21.34-2.23-25.13-2.31-1.92-7.24-3.37-12.95-3.7-14.9a9.57 9.57 0 0 1 1.56-7.46c1.37-1.93 3.41-3.39 5.58-4.56.58-.3 1.75-.85 3.38-1.6v-.01c1.44-.67 3.24-1.5 5.3-2.5 4.36-2.08 9.82-4.81 15.33-7.97 5.5-3.16 11.07-6.76 15.63-10.58 4.55-3.82 8.18-7.93 9.69-12.13 2.95-8.23 2.26-18.05-1.2-27.72-3.46-9.7-9.71-19.3-18-27.15-8.3-7.88-19.61-13.39-30.17-16.92-10.55-3.52-20.44-5.11-25.98-5.1-11.24 0-18.88 8.25-20.84 10.89-.4.54-.98 1.23-1.68 2.07l-.3.35c-.83.99-1.78 2.14-2.73 3.4-1.9 2.5-3.82 5.51-4.71 8.66-.88 3.08-2.13 12.09-3.16 20.86-1.03 8.8-1.86 17.52-1.86 20.08 0 2.07-.08 5.83-.16 9.64a5654.21 5654.21 0 0 0-.16 8.35 45.88 45.88 0 0 0 .07 3.72V99c.1.98.3 2.93.67 5.07.42 2.4 1.1 5.16 2.17 7.06a7.85 7.85 0 0 0 1.49 1.7l.06.05c.5.49 1.05 1.02 1.61 1.67a13.02 13.02 0 0 1 3.1 6.26c.27 1.4.22 3.4-.06 5.69a68.08 68.08 0 0 1-1.34 7.17 74.77 74.77 0 0 1-2.99 10c-.15.3-.5 1.15-.98 2.4-.55-.32-1.2-.82-1.9-1.5a33.24 33.24 0 0 1-3.3-3.81 117.54 117.54 0 0 1-7.13-10.88 313.14 313.14 0 0 1-7.44-13.39l-.71-1.32a35.6 35.6 0 0 0-1.1-1.97c-2.35-3.53-9.53-5.24-21.71-8.09.75-.82.72-.91 1.65-1.9 2.44-2.02 6.48.36 6.96.84l.48-.95c-2.86-2.39-6.99-2.3-8.34-.96-2.14 2.15-.74 1-3.1 2.97-.7.6-1.89 1.5-2.45 1.85-.55.34-.95.5-1.21.53h-.04a9.43 9.43 0 0 1-2.81-1.94l-1.1 1c.46.46 1 .93 1.62 1.41.44.34.87.62 1.25.82.19.1.38.18.57.23.17.06.4.1.64.08a4.9 4.9 0 0 0 1.91-.76 18.02 18.02 0 0 0 2.75-2.1c10.54 2.34 19.25 4.34 21.6 7.86a36.64 36.64 0 0 1 1.3 2.37l.42.78 1.35 2.52c1.65 3.06 3.8 7 6.13 10.93 2.33 3.94 4.85 7.91 7.23 11.04a34.86 34.86 0 0 0 3.47 4c.81.78 1.63 1.42 2.43 1.84a531.4 531.4 0 0 0-4.68 12.65c-1.4 3.97-2.8 8.05-3.86 11.48a90.14 90.14 0 0 0-1.3 4.6c-.32 1.29-.54 2.38-.58 3.15l1.6.09c.04-.6.22-1.57.54-2.86.31-1.27.75-2.8 1.28-4.5 1.05-3.4 2.43-7.47 3.83-11.43 2.82-7.94 5.7-15.4 6.18-16.4.53-1.12 1.96-5.5 3.1-10.32a69.6 69.6 0 0 0 1.37-7.34c.28-2.33.36-4.53.04-6.19-1.41-4.31-1.86-5.26-5.36-8.96a6.58 6.58 0 0 1-1.08-1.2c-.93-1.64-1.56-4.16-1.98-6.55a67.1 67.1 0 0 1-.66-4.95l-.03-.35c-.04-.35-.05-1.5-.03-3.21a691.73 691.73 0 0 1 .16-8.33c.08-3.8.16-7.59.16-9.68 0-2.44.81-11.07 1.85-19.89 1.03-8.83 2.27-17.68 3.1-20.61.82-2.86 2.6-5.68 4.46-8.14a80.8 80.8 0 0 1 2.68-3.33l.3-.35a48.6 48.6 0 0 0 1.74-2.15c1.85-2.5 9.08-10.23 19.55-10.24 5.32-.01 15.03 1.53 25.47 5.02Z"
fill="var(--va-danger)"
/>
<path
clip-rule="evenodd"
d="M24.85 101.92c.46 1.39 1.12 2.63 1.6 3.36l1.6-1.03a13.9 13.9 0 0 1-1.4-2.94 6.75 6.75 0 0 1-.38-3.48l-1.86-.43a8.59 8.59 0 0 0 .44 4.52Z"
fill="var(--va-danger)"
fill-rule="evenodd"
/>
</svg>
</template>

View File

@ -0,0 +1,29 @@
import VuesticLogo from './VuesticLogo.vue'
export default {
title: 'VuesticLogo',
component: VuesticLogo,
tags: ['autodocs'],
}
export const Default = () => ({
components: { VuesticLogo },
template: `<VuesticLogo start="#6B7AFE" end="#083CC6" />`,
})
export const White = () => ({
components: { VuesticLogo },
template: `<div class="bg-primary">
<VuesticLogo start="#FFF"/>
</div>`,
})
export const Blue = () => ({
components: { VuesticLogo },
template: `<VuesticLogo start="#0E41C9"/>`,
})
export const Height = () => ({
components: { VuesticLogo },
template: `<VuesticLogo start="#6B7AFE" end="#083CC6" :height="48"/>`,
})

View File

@ -0,0 +1,51 @@
<template>
<svg width="231" height="26" xmlns="http://www.w3.org/2000/svg">
<!-- Created with SVG Editor - http://github.com/mzalive/SVG Editor/ -->
<defs>
<filter height="200%" width="200%" y="-50%" x="-50%" id="svg_8_blur">
<feGaussianBlur stdDeviation="0.6" in="SourceGraphic"/>
</filter>
</defs>
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="28" width="233" y="-1" x="-1"/>
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
</g>
</g>
<g>
<title>Layer 1</title>
<path d="m280.5,281.4375c0,0 -2.054138,0.567322 -5,0c-5.287994,-1.018372 -7,-2 -8,-3c-1,-1 -1.289795,-3.042908 -1,-4c1.04483,-3.450836 4.891724,-5.19577 12,-9c7.885925,-4.220428 13.207825,-6.930664 21,-9c2.899506,-0.770004 4,-1 3,-1c-5,0 -21,0 -40,0c-7,0 -17,-2 -17,0c0,4 10.003876,0.232941 17,0c15.024963,-0.500275 22.824158,1.062897 30,-3c1.230652,-0.696777 -3,-1 -9,-1c-13,0 -21,0 -24,1l-2,1" id="svg_2" stroke-width="1.5" stroke="#fff" fill="none"/>
<path d="m192.5,194.4375c1,0 2.74707,1.61702 7,3c5.784607,1.881058 9.04863,2.814651 17,5c10.067017,2.766815 21,5 27,5c8,0 12,0 19,1l5,0l2,0" id="svg_3" stroke-width="1.5" stroke="#fff" fill="none"/>
<path d="m251.5,198.4375c-1,0 -1.934143,0.144287 -4,1c-5.843124,2.420303 -8.925797,2.497559 -14,3c-5.970795,0.591232 -11,0 -13,0c-4,0 -7,0 -9,0l-1,0l-2,0l0,-1" id="svg_4" stroke-width="1.5" stroke="#fff" fill="none"/>
<text font-style="normal" font-weight="normal" filter="url(#svg_8_blur)" opacity="0.78" xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="26" id="svg_8" y="21.4375" x="27" stroke="#000ecc" fill="#ffffff">CYCLE-RIDER</text>
</g>
</svg>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useColors } from 'vuestic-ui'
const { getColor } = useColors()
const props = withDefaults(
defineProps<{
height?: number
start?: string
end?: string
}>(),
{
height: 18,
start: 'primary',
end: undefined,
},
)
const colorsComputed = computed(() => {
return {
start: getColor(props.start),
end: getColor(props.end || props.start),
}
})
</script>

View File

@ -0,0 +1,96 @@
<template>
<div class="flex gap-2">
<VaIconMenuCollapsed
class="cursor-pointer"
:class="{ 'x-flip': !isSidebarMinimized }"
:color="collapseIconColor"
@click="isSidebarMinimized = !isSidebarMinimized"
/>
<nav class="flex items-center">
<VaBreadcrumbs>
<VaBreadcrumbsItem label="Home" :to="{ name: 'dashboard' }" />
<VaBreadcrumbsItem
v-for="item in items"
:key="item.label"
:label="item.label"
@click="handleBreadcrumbClick(item)"
/>
</VaBreadcrumbs>
</nav>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useColors } from 'vuestic-ui'
import VaIconMenuCollapsed from '../icons/VaIconMenuCollapsed.vue'
import { storeToRefs } from 'pinia'
import { useGlobalStore } from '../../stores/global-store'
import NavigationRoutes from '../sidebar/NavigationRoutes'
const { isSidebarMinimized } = storeToRefs(useGlobalStore())
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
type BreadcrumbNavigationItem = {
label: string
to: string
hasChildren: boolean
}
const findRouteName = (name: string) => {
const traverse = (routers: any[]): string => {
for (const router of routers) {
if (router.name === name) {
return router.displayName
}
if (router.children) {
const result = traverse(router.children)
if (result) {
return result
}
}
}
return ''
}
return traverse(NavigationRoutes.routes)
}
const items = computed(() => {
const result: { label: string; to: string; hasChildren: boolean }[] = []
route.matched.forEach((route) => {
const labelKey = findRouteName(route.name as string)
if (!labelKey) {
return
}
result.push({
label: t(labelKey),
to: route.path,
hasChildren: route.children && route.children.length > 0,
})
})
return result
})
const { getColor } = useColors()
const collapseIconColor = computed(() => getColor('secondary'))
const handleBreadcrumbClick = (item: BreadcrumbNavigationItem) => {
if (!item.hasChildren) {
router.push(item.to)
}
}
</script>
<style lang="scss" scoped>
.x-flip {
transform: scaleX(-100%);
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<svg class="va-icon-clean-code" viewBox="0 0 56.02 50.34" xmlns="http://www.w3.org/2000/svg">
<defs />
<title>overview_icon_4</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="M38.23,16.17a10,10,0,1,0-17.67,6.42V47.5l7.33-5,8,5V22.58A10,10,0,0,0,38.23,16.17Z" />
<path
class="cls-2"
d="M28.23,0a13.15,13.15,0,0,0-9.17,22.6V50.34l8.87-6,9.46,5.92V22.6A13.15,13.15,0,0,0,28.23,0ZM34.4,44.79l-6.54-4.08-5.8,4V24.79a13.11,13.11,0,0,0,12.33,0ZM28.23,23.33A10.17,10.17,0,1,1,38.4,13.17,10.18,10.18,0,0,1,28.23,23.33Z"
/>
<path
class="cls-2"
d="M28.23,5.67a7.5,7.5,0,1,0,7.5,7.5A7.51,7.51,0,0,0,28.23,5.67Zm0,12a4.5,4.5,0,1,1,4.5-4.5A4.5,4.5,0,0,1,28.23,17.67Z"
/>
<polygon class="cls-2" points="9.51 15.11 0 24.61 9.51 34.12 11.63 32 4.24 24.61 11.63 17.23 9.51 15.11" />
<polygon
class="cls-2"
points="46.52 15.11 44.39 17.23 51.78 24.61 44.39 32 46.52 34.12 56.02 24.61 46.52 15.11"
/>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-clean-code {
display: inline-block;
width: 56px;
height: 50px;
.cls-1 {
fill: #4ae387;
}
.cls-2 {
fill: #34495e;
}
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<svg
:fill="color"
class="va-icon-color"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M12 22C6.49 22 2 17.51 2 12S6.49 2 12 2s10 4.04 10 9c0 3.31-2.69 6-6 6h-1.77c-.28 0-.5.22-.5.5 0 .12.05.23.13.33.41.47.64 1.06.64 1.67 0 1.38-1.12 2.5-2.5 2.5zm0-18c-4.41 0-8 3.59-8 8s3.59 8 8 8c.28 0 .5-.22.5-.5 0-.16-.08-.28-.14-.35-.41-.46-.63-1.05-.63-1.65 0-1.38 1.12-2.5 2.5-2.5H16c2.21 0 4-1.79 4-4 0-3.86-3.59-7-8-7z"
/>
<circle cx="6.5" cy="11.5" r="1.5" />
<circle cx="9.5" cy="7.5" r="1.5" />
<circle cx="14.5" cy="7.5" r="1.5" />
<circle cx="17.5" cy="11.5" r="1.5" />
</svg>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
}>(),
{
color: 'inherit',
},
)
</script>
<style lang="scss">
.va-icon-color {
display: inline-block;
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<svg class="va-icon-faster" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<!-- Generator: sketchtool 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>62EBC3B8-A55C-4B01-95A2-52FB8EDD4150</title>
<defs />
<g id="symbols" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
<g id="icon-faster" fill="#34495E">
<g>
<path
id="A"
d="M17.748,19 L16.956,16.3 L12.942,16.3 L12.168,19 L8.928,19 L13.302,6.13 L16.614,6.13 L20.988,19 L17.748,19 Z M14.976,9.064 L14.94,9.064 C14.94,9.064 14.652,10.468 14.418,11.278 L13.68,13.78 L16.218,13.78 L15.498,11.278 C15.264,10.468 14.976,9.064 14.976,9.064 Z"
/>
<rect id="Rectangle-4" height="2" rx="1" width="5" x="3" y="11" />
<rect id="Rectangle-4-Copy" height="2" rx="1" width="6" x="4" y="7" />
<rect id="Rectangle-4" height="2" rx="1" width="4" x="2" y="15" />
</g>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-faster {
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,46 @@
<template>
<svg class="va-icon-free" viewBox="0 0 44.99 51.04" xmlns="http://www.w3.org/2000/svg">
<defs />
<title>overview_icon_2</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
class="cls-1"
d="M1.08,28.21C1.08,13.62,8.38,6.29,19,6.29S37,13.69,37,28.21,29.66,50.54,19,50.54,1.08,42.8,1.08,28.21Zm23.56,0c0-11.3-2.58-16.9-5.62-16.9s-5.62,5.6-5.62,16.9S16,41.66,19,41.66,24.65,39.51,24.65,28.21Z"
/>
<line class="cls-2" x1="39.83" x2="39.83" y1="47.62" y2="50.96" />
<path
class="cls-3"
d="M18.73,9.64c-4.9,0-6.9,4.54-6.9,15.66,0,11.29,2.06,16.1,6.9,16.1s6.9-4.81,6.9-16.1C25.63,14.17,23.63,9.64,18.73,9.64Zm0,28.76c-1.07,0-3.9,0-3.9-13.1,0-12.66,2.64-12.66,3.9-12.66s3.9,0,3.9,12.66C22.63,38.4,19.8,38.4,18.73,38.4Z"
/>
<path
class="cls-3"
d="M42.9,43.74A3.76,3.76,0,0,1,40.17,45c-1.95,0-3.24-1.57-3.24-4.4s1.53-4.35,3.29-4.35a3.67,3.67,0,0,1,2.5,1.11l2.08-2.55A6.8,6.8,0,0,0,41.33,33V31h-3v2.12a7.09,7.09,0,0,0-1.64.63,43.71,43.71,0,0,0,.77-8.41c0-15.84-7-25.3-18.73-25.3S0,9.46,0,25.3,7.18,51,18.73,51A16.4,16.4,0,0,0,33.12,43.1,6.77,6.77,0,0,0,40,48.46a6.35,6.35,0,0,0,5-2.22ZM18.73,48C8.88,48,3,39.54,3,25.3S8.73,3,18.73,3s15.73,8.13,15.73,22.3S28.58,48,18.73,48Z"
/>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-free {
display: inline-block;
width: 55px;
height: 47.8px;
.cls-1 {
fill: #4ae387;
}
.cls-2 {
fill: none;
stroke: #34495e;
stroke-miterlimit: 10;
stroke-width: 3px;
}
.cls-3 {
fill: #34495e;
}
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<svg class="va-icon-fresh" viewBox="0 0 50.98 47.66" xmlns="http://www.w3.org/2000/svg">
<defs />
<title>overview_icon_5</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
class="cls-1"
d="M6,19C11,12.66,26.33,3,46.33,6c-3.67,17-8.67,26-8.67,26s-7,14-19.67-3.67C5.67,26.33,9,22.33,6,19Z"
/>
<path
class="cls-2"
d="M48.33.49l-.77,0c-11.22-1.88-30.21,1.46-39,9-3.38,2.89-5,6.11-4.69,9.59a11.06,11.06,0,0,0,4.77,8,11,11,0,0,0,6.24,1.82q.53,0,1.09,0A55.51,55.51,0,0,0,13.2,39.21C9.48,33.07,2.35,30.83,0,30.83v3c.12,0,12.18,1.95,12.5,13.54h0c0,.1,0,.19,0,.29h3a50.57,50.57,0,0,1,3.12-17.21c2.48,5.09,6.36,8,10.91,8.13,5.3.07,10.1-3.85,11.91-9.81.94-3.12,1.88-6.37,2.78-9.51C46.32,11.9,48.31,5,49.62,2.76L51,.49ZM10.28,24.61a8.06,8.06,0,0,1-3.45-5.73c-.19-2.47,1-4.85,3.65-7.08,5.9-5,17.25-8.12,27-8.7-8.35,4-15.7,12.31-20.23,22.51l-.38.1h0A8.82,8.82,0,0,1,10.28,24.61Zm31-6.18c-.9,3.13-1.82,6.37-2.76,9.47-1.4,4.62-4.95,7.69-8.86,7.69h-.13c-4-.07-7.45-3.43-9.33-9.09C25.83,14.27,35.67,4.94,45.92,3.65,44.6,7,43.06,12.39,41.33,18.42Z"
/>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-fresh {
display: inline-block;
width: 51px;
height: 48px;
.cls-1 {
fill: #4ae387;
}
.cls-2 {
fill: #34495e;
}
}
</style>

View File

@ -0,0 +1,10 @@
<template>
<svg viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,22 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M18.3516 2.8125H3.97663C3.53557 2.8125 3.17802 3.17005 3.17802 3.61111C3.17802 4.05217 3.53557 4.40972 3.97663 4.40972H18.3516C18.7927 4.40972 19.1502 4.05217 19.1502 3.61111C19.1502 3.17005 18.7927 2.8125 18.3516 2.8125Z"
fill="#767C88"
/>
<path
d="M10.3655 9.20139H18.3516C18.5634 9.20139 18.7666 9.28553 18.9163 9.4353C19.0661 9.58506 19.1502 9.78819 19.1502 10C19.1502 10.2118 19.0661 10.4149 18.9163 10.5647C18.7666 10.7145 18.5634 10.7986 18.3516 10.7986H10.3655C10.1537 10.7986 9.95058 10.7145 9.80081 10.5647C9.65105 10.4149 9.56691 10.2118 9.56691 10C9.56691 9.78819 9.65105 9.58506 9.80081 9.4353C9.95058 9.28553 10.1537 9.20139 10.3655 9.20139ZM2.37941 9.20139H6.37246C6.58427 9.20139 6.7874 9.28553 6.93717 9.4353C7.08693 9.58506 7.17107 9.78819 7.17107 10C7.17107 10.2118 7.08693 10.4149 6.93717 10.5647C6.7874 10.7145 6.58427 10.7986 6.37246 10.7986H2.37941C2.1676 10.7986 1.96447 10.7145 1.8147 10.5647C1.66493 10.4149 1.5808 10.2118 1.5808 10C1.5808 9.78819 1.66493 9.58506 1.8147 9.4353C1.96447 9.28553 2.1676 9.20139 2.37941 9.20139Z"
fill="#767C88"
/>
<path
d="M18.3516 15.5903H3.97663C3.53557 15.5903 3.17802 15.9478 3.17802 16.3889C3.17802 16.8299 3.53557 17.1875 3.97663 17.1875H18.3516C18.7927 17.1875 19.1502 16.8299 19.1502 16.3889C19.1502 15.9478 18.7927 15.5903 18.3516 15.5903Z"
fill="#767C88"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.64583 7.60417C3.95771 7.29229 4.46336 7.29229 4.77524 7.60417C5.08712 7.91604 5.08712 8.4217 4.77524 8.73357L3.50881 10L4.77524 11.2664C5.08712 11.5783 5.08712 12.084 4.77524 12.3958C4.46336 12.7077 3.95771 12.7077 3.64583 12.3958L1.74342 10.4934C1.47091 10.2209 1.47091 9.77909 1.74342 9.50658L3.64583 7.60417Z"
fill="#767C88"
/>
</svg>
</template>

View File

@ -0,0 +1,30 @@
<template>
<svg class="va-icon-menu" height="18" viewBox="0 0 24 18" width="23" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="nonzero" transform="translate(1 -3)">
<path d="M0 0h24v24H0z" />
<rect :fill="color" height="2" rx="1" width="20" x="2" y="3" />
<path :fill="color" d="M11 11h10a1 1 0 0 1 0 2H11a1 1 0 0 1 0-2zM1 11h5a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2z" />
<rect :fill="color" height="2" rx="1" width="20" x="2" y="19" />
<path :stroke="color" d="M4 9l-3 3 3 3" stroke-width="2" />
</g>
</svg>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
}>(),
{
color: 'inherit',
},
)
</script>
<style lang="scss">
.va-icon-menu {
display: inline-block;
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<svg class="va-icon-menu-collapsed" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="nonzero">
<path d="M0 0h24v24H0z" />
<rect :fill="color" height="2" rx="1" width="20" x="2" y="3" />
<path
:fill="color"
d="M3 11h10a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2zM20.993 11l-2.7-2.7-1.414 1.414L18.164 11H16a1 1 0 0 0 0 2h2.179l-1.3 1.3 1.414 1.414L21.007 13A1 1 0 0 0 21 11h-.007z"
/>
<rect :fill="color" height="2" rx="1" width="20" x="2" y="19" />
</g>
</svg>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
}>(),
{
color: 'inherit',
},
)
</script>
<style lang="scss">
.va-icon-menu-collapsed {
display: inline-block;
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<svg :fill="color" height="16" viewBox="0 0 20 16" width="20" xmlns="http://www.w3.org/2000/svg">
<path
d="M20 2c0-1.1-.9-2-2-2H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V2zm-2 0l-8 5-8-5h16zm0 12H2V4l8 5 8-5v10z"
fill-rule="nonzero"
/>
</svg>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
}>(),
{
color: 'inherit',
},
)
</script>
<style lang="scss">
.va-icon-message {
display: inline-block;
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
:fill="color"
d="M10 20c1.1 0 2-.9 2-2H8c0 1.1.9 2 2 2zm6-6V9c0-3.07-1.63-5.64-4.5-6.32V2c0-.83-.67-1.5-1.5-1.5S8.5 1.17 8.5 2v.68C5.64 3.36 4 5.92 4 9v5l-2 2v1h16v-1l-2-2zm-2 1H6V9c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6zM5.58 2.08L4.15.65C1.75 2.48.17 5.3.03 8.5h2a8.445 8.445 0 0 1 3.55-6.42zM17.97 8.5h2c-.15-3.2-1.73-6.02-4.12-7.85l-1.42 1.43a8.495 8.495 0 0 1 3.54 6.42z"
/>
</svg>
</template>
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
}>(),
{
color: 'inherit',
},
)
</script>

View File

@ -0,0 +1,29 @@
<template>
<svg class="va-icon-responsive" viewBox="0 0 47.5 49" xmlns="http://www.w3.org/2000/svg">
<defs />
<title>overview_icon_3</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<polygon class="cls-1" points="37 26 37 7 11 7 11 18 3 18 3 46 11 46 15 46 30 46 37 46 45 46 45 26 37 26" />
<path class="cls-2" d="M40,19V0H8V11H0V49H47.5V19ZM3,46V14H8V46Zm34,0H11V3H37Zm7.5,0H40V22h4.5Z" />
<circle class="cls-2" cx="24" cy="41" r="2.67" />
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-responsive {
display: inline-block;
width: 47.5px;
height: 49px;
.cls-1 {
fill: #4ae387;
}
.cls-2 {
fill: #34495e;
}
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<svg class="va-icon-rich" viewBox="0 0 56.99 55" xmlns="http://www.w3.org/2000/svg">
<defs />
<title>overview_icon_6</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect class="cls-1" height="23" width="37.33" x="10.31" y="30.5" />
<path
class="cls-2"
d="M57,41.18l-7.85-16V24H8.81v1.11L0,41.11l2.63,1.45L8.81,31.33V55H49.15V32L54.3,42.5ZM46.15,52H11.81V27H46.15Z"
/>
<polygon class="cls-2" points="35.3 1.8 32.9 0 28.12 6.39 26.16 4.63 24.16 6.87 28.56 10.8 35.3 1.8" />
<polygon
class="cls-2"
points="22.3 12.46 19.9 10.67 15.12 17.05 13.16 15.3 11.16 17.54 15.56 21.47 22.3 12.46"
/>
<polygon
class="cls-2"
points="38.89 21.14 45.64 12.13 43.23 10.33 38.45 16.72 36.49 14.97 34.49 17.2 38.89 21.14"
/>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-rich {
display: inline-block;
width: 57px;
height: 55px;
.cls-1 {
fill: #4ae387;
}
.cls-2 {
fill: #34495e;
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<svg class="va-icon-slower" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<!-- Generator: sketchtool 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>67046716-A590-445C-AC65-1EEF69089C00</title>
<defs />
<g id="symbols" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
<g id="icon-slower" fill="#34495E">
<g>
<path
id="A"
d="M16.82,18.87 L16.028,16.17 L12.014,16.17 L11.24,18.87 L8,18.87 L12.374,6 L15.686,6 L20.06,18.87 L16.82,18.87 Z M14.048,8.934 L14.012,8.934 C14.012,8.934 13.724,10.338 13.49,11.148 L12.752,13.65 L15.29,13.65 L14.57,11.148 C14.336,10.338 14.048,8.934 14.048,8.934 Z"
/>
<rect id="Rectangle-4" height="2" rx="1" width="2" x="5" y="11" />
<rect id="Rectangle-4-Copy" height="2" rx="1" width="3" x="6" y="7" />
<rect id="Rectangle-4" height="2" rx="1" width="2" x="4" y="15" />
</g>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-slower {
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<svg class="va-icon-vue" viewBox="0 0 55.05 47.8" xmlns="http://www.w3.org/2000/svg">
<defs />
<title>overview_icon_1</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<polygon
class="cls-1"
points="27.75 21.19 18.48 6.18 4.78 6.18 27.75 42.92 50.89 6.18 36.13 6.18 27.75 21.19"
/>
<path
class="cls-2"
d="M33.08,0,27.44,9.76,21.84,0H0L27.43,47.8,55,0ZM27.43,15.77,34.81,3h4.6l-12,20.72L15.55,3h4.55ZM5.18,3h6.91L27.43,29.73,42.88,3h7L27.44,41.78Z"
/>
</g>
</g>
</svg>
</template>
<style lang="scss">
.va-icon-vue {
display: inline-block;
width: 55px;
height: 47.8px;
.cls-1 {
fill: #4ae387;
}
.cls-2 {
fill: #34495e;
}
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<svg class="va-icon-vuestic" height="31" viewBox="0 0 304 31" width="304" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient :id="'ORIGINAL'" x1="0%" y1="50%" y2="50%">
<stop offset="0%" stop-color="#4AE387" />
<stop offset="100%" stop-color="#C8EA13" />
</linearGradient>
<linearGradient :id="'CORPORATE'" x1="0%" y1="50%" y2="50%">
<stop offset="0%" stop-color="#74BBFF" />
<stop offset="100%" stop-color="#6E85E8" />
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd" transform="scale(2)">
<path
:fill="`url(#${themeGradientId})`"
d="M6.36,15.36 L-7.10542736e-15,0.48 L3.672,0.48 L6.576,7.416 L9.48,0.48 L13.152,0.48 L6.792,15.36 L6.36,15.36 Z M21.744,10.032 L21.744,0.48 L25.248,0.48 L25.248,9.504 C25.248,11.424 26.16,12.096 27.384,12.096 C28.728,12.096 29.568,11.424 29.568,9.504 L29.568,0.48 L33.096,0.48 L33.096,10.032 C33.096,13.44 30.552,15.36 27.384,15.36 C24.096,15.36 21.744,13.344 21.744,10.032 Z M42.6,14.88 L42.6,0.48 L52.128,0.48 L52.128,3.6 L46.128,3.6 L46.128,6.192 L51.672,6.192 L51.672,9.264 L46.128,9.264 L46.128,11.856 L52.392,11.856 L52.392,14.88 L42.6,14.88 Z M61.224,10.656 L64.656,10.656 C64.656,11.496 65.328,12.168 66.144,12.168 C67.032,12.168 67.584,11.664 67.584,10.92 C67.584,9.84 66.168,9.528 64.992,9.12 C62.568,8.256 61.224,7.128 61.224,4.656 C61.224,2.112 63.408,7.10542736e-15 66.12,7.10542736e-15 C69.312,7.10542736e-15 70.824,2.04 71.016,4.704 L67.704,4.704 C67.704,3.888 67.2,3.216 66.216,3.216 C65.448,3.216 64.704,3.672 64.704,4.608 C64.704,5.688 66.024,5.88 67.248,6.24 C69.816,7.008 71.016,8.448 71.016,10.704 C71.016,13.248 68.856,15.36 66.144,15.36 C63,15.36 61.224,13.248 61.224,10.656 Z M82.896,14.88 L82.896,3.6 L79.68,3.6 L79.68,0.48 L89.712,0.48 L89.712,3.6 L86.448,3.6 L86.448,14.88 L82.896,14.88 Z M98.544,14.88 L98.544,0.48 L102.072,0.48 L102.072,14.88 L98.544,14.88 Z M111.72,7.68 C111.72,3.384 114.96,7.10542736e-15 119.52,7.10542736e-15 C122.256,7.10542736e-15 124.152,1.032 125.688,2.64 L123.264,4.944 C122.256,3.96 120.96,3.336 119.52,3.336 C116.952,3.336 115.296,5.256 115.296,7.68 C115.296,10.104 116.952,12.024 119.52,12.024 C120.96,12.024 122.256,11.4 123.264,10.416 L125.616,12.72 C124.176,14.232 122.184,15.36 119.52,15.36 C114.96,15.36 111.72,11.976 111.72,7.68 Z"
fill-rule="nonzero"
/>
<path
:fill="textColor"
d="M139.712,7.88 L139.712,6.152 L138.44,6.152 C138.272,6.152 138.066,6.162 137.822,6.182 C137.578,6.202 137.36,6.224 137.168,6.248 C137.424,5.984 137.682,5.704 137.942,5.408 C138.202,5.112 138.436,4.808 138.644,4.496 C138.852,4.184 139.022,3.87 139.154,3.554 C139.286,3.238 139.352,2.928 139.352,2.624 C139.352,2.248 139.288,1.906 139.16,1.598 C139.032,1.29 138.852,1.028 138.62,0.812 C138.388,0.596 138.112,0.428 137.792,0.308 C137.472,0.188 137.12,0.128 136.736,0.128 C136.456,0.128 136.198,0.152 135.962,0.2 C135.726,0.248 135.502,0.322 135.29,0.422 C135.078,0.522 134.876,0.65 134.684,0.806 C134.492,0.962 134.296,1.144 134.096,1.352 L134.096,1.352 L135.2,2.456 C135.384,2.272 135.574,2.106 135.77,1.958 C135.966,1.81 136.192,1.736 136.448,1.736 C136.728,1.736 136.954,1.818 137.126,1.982 C137.298,2.146 137.384,2.4 137.384,2.744 C137.384,3 137.302,3.274 137.138,3.566 C136.974,3.858 136.748,4.166 136.46,4.49 C136.172,4.814 135.834,5.16 135.446,5.528 C135.058,5.896 134.64,6.288 134.192,6.704 L134.192,6.704 L134.192,7.88 L139.712,7.88 Z M142.56,8.024 C142.912,8.024 143.2,7.904 143.424,7.664 C143.648,7.424 143.76,7.128 143.76,6.776 C143.76,6.424 143.648,6.128 143.424,5.888 C143.2,5.648 142.912,5.528 142.56,5.528 C142.208,5.528 141.92,5.648 141.696,5.888 C141.472,6.128 141.36,6.424 141.36,6.776 C141.36,7.128 141.472,7.424 141.696,7.664 C141.92,7.904 142.208,8.024 142.56,8.024 Z M150.736,7.88 L150.736,6.224 L149.368,6.224 L149.368,0.272 L147.856,0.272 C147.568,0.456 147.272,0.604 146.968,0.716 C146.664,0.828 146.296,0.928 145.864,1.016 L145.864,1.016 L145.864,2.288 L147.304,2.288 L147.304,6.224 L145.672,6.224 L145.672,7.88 L150.736,7.88 Z"
/>
</g>
</svg>
</template>
<script>
export default {
name: 'VaIconVuestic',
inject: ['contextConfig'],
computed: {
themeGradientId() {
return this.contextConfig.invertedColor ? 'CORPORATE' : 'ORIGINAL'
},
textColor() {
return this.contextConfig.invertedColor ? '#6E85E8' : '#E4FF32'
},
},
}
</script>
<style lang="scss">
.va-icon-vuestic {
.st0 {
fill: #4ae387;
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<VaNavbar class="app-layout-navbar py-2 px-0">
<template #left>
<div class="left">
<Transition v-if="isMobile" name="icon-fade" mode="out-in">
<VaIcon
color="primary"
:name="isSidebarMinimized ? 'menu' : 'close'"
size="24px"
style="margin-top: 3px"
@click="isSidebarMinimized = !isSidebarMinimized"
/>
</Transition>
<RouterLink to="/" aria-label="Visit home page">
<VuesticLogo />
</RouterLink>
</div>
</template>
<template #right>
<AppNavbarActions class="app-navbar__actions" :is-mobile="isMobile" />
</template>
</VaNavbar>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useGlobalStore } from '../../stores/global-store'
import AppNavbarActions from './components/AppNavbarActions.vue'
import VuesticLogo from '../VuesticLogo.vue'
defineProps({
isMobile: { type: Boolean, default: false },
})
const GlobalStore = useGlobalStore()
const { isSidebarMinimized } = storeToRefs(GlobalStore)
</script>
<style lang="scss" scoped>
.va-navbar {
z-index: 2;
@media screen and (max-width: 950px) {
.left {
width: 100%;
}
.app-navbar__actions {
display: flex;
justify-content: space-between;
}
}
}
.left {
display: flex;
align-items: center;
margin-left: 1rem;
& > * {
margin-right: 1rem;
}
& > *:last-child {
margin-right: 0;
}
}
.icon-fade-enter-active,
.icon-fade-leave-active {
transition: transform 0.5s ease;
}
.icon-fade-enter,
.icon-fade-leave-to {
transform: scale(0.5);
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="app-navbar-actions">
<!-- <GithubButton v-if="!isMobile" class="app-navbar-actions__item" />
<VaButton
v-if="!isMobile"
preset="secondary"
href="https://admin.vuestic.dev/"
target="_blank"
color="textPrimary"
class="app-navbar-actions__item flex-shrink-0 mx-0"
>
{{ t('aboutVuesticAdmin') }}
</VaButton> -->
<!-- <VaButton
v-if="!isMobile"
preset="secondary"
href="https://discord.gg/u7fQdqQt8c"
target="_blank"
color="textPrimary"
class="app-navbar-actions__item flex-shrink-0 mx-0"
>
{{ t('helpAndSupport') }}
</VaButton> -->
<!-- <NotificationDropdown class="app-navbar-actions__item" /> -->
<ProfileDropdown class="app-navbar-actions__item app-navbar-actions__item--profile mr-1" />
</div>
</template>
<script lang="ts" setup>
import ProfileDropdown from './dropdowns/ProfileDropdown.vue'
import NotificationDropdown from './dropdowns/NotificationDropdown.vue'
import GithubButton from './GitHubButton.vue'
defineProps({
isMobile: { type: Boolean, default: false },
})
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style lang="scss">
.app-navbar-actions {
display: flex;
align-items: center;
.va-dropdown__anchor {
color: var(--va-primary);
fill: var(--va-primary);
}
&__item {
padding: 0;
margin-left: 0.25rem;
margin-right: 0.25rem;
svg {
height: 20px;
}
&--profile {
display: flex;
justify-content: center;
}
.va-dropdown-content {
background-color: var(--va-white);
}
@media screen and (max-width: 640px) {
margin-left: 0;
margin-right: 0;
&:first-of-type {
margin-left: 0;
}
}
}
.fa-github {
color: var(--va-on-background-primary);
}
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<VaButton
preset="secondary"
color="textPrimary"
href="https://github.com/epicmaxco/vuestic-admin"
target="_blank"
aria-label="Visit github"
>
<VaIcon :component="VaIconGitHub" />
</VaButton>
</template>
<script lang="ts" setup>
import VaIconGitHub from '../../icons/VaIconGitHub.vue'
</script>

View File

@ -0,0 +1,180 @@
<template>
<VaDropdown :offset="[13, 0]" class="notification-dropdown" stick-to-edges :close-on-content-click="false">
<template #anchor>
<VaButton preset="secondary" color="textPrimary">
<VaBadge overlap>
<template #text> 2+</template>
<VaIconNotification class="notification-dropdown__icon" />
</VaBadge>
</VaButton>
</template>
<VaDropdownContent class="h-full sm:max-w-[420px] sm:h-auto">
<section class="sm:max-h-[320px] p-4 overflow-auto">
<VaList class="space-y-1 mb-2">
<template v-for="(item, index) in notificationsWithRelativeTime" :key="item.id">
<VaListItem class="text-base">
<VaListItemSection icon class="mx-0 p-0">
<VaIcon :name="item.icon" color="secondary" />
</VaListItemSection>
<VaListItemSection>
{{ item.message }}
</VaListItemSection>
<VaListItemSection icon class="mx-1">
{{ item.updateTimestamp }}
</VaListItemSection>
</VaListItem>
<VaListSeparator v-if="item.separator && index !== notificationsWithRelativeTime.length - 1" class="mx-3" />
</template>
</VaList>
<VaButton preset="primary" class="w-full" @click="displayAllNotifications = !displayAllNotifications"
>{{ displayAllNotifications ? t('notifications.less') : t('notifications.all') }}
</VaButton>
</section>
</VaDropdownContent>
</VaDropdown>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import VaIconNotification from '../../../icons/VaIconNotification.vue'
const { t, locale } = useI18n()
const baseNumberOfVisibleNotifications = 4
const rtf = new Intl.RelativeTimeFormat(locale.value, { style: 'short' })
const displayAllNotifications = ref(false)
interface INotification {
message: string
icon: string
id: number
separator?: boolean
updateTimestamp: Date
}
const makeDateFromNow = (timeFromNow: number) => {
const date = new Date()
date.setTime(date.getTime() + timeFromNow)
return date
}
const notifications: INotification[] = [
{
message: '4 pending requests',
icon: 'favorite_outline',
id: 1,
separator: true,
updateTimestamp: makeDateFromNow(-3 * 60 * 1000),
},
{
message: '3 new reports',
icon: 'calendar_today',
id: 2,
separator: true,
updateTimestamp: makeDateFromNow(-12 * 60 * 60 * 1000),
},
{
message: 'Whoops! Your trial period has expired.',
icon: 'error_outline',
id: 3,
separator: true,
updateTimestamp: makeDateFromNow(-2 * 24 * 60 * 60 * 1000),
},
{
message: 'It looks like your timezone is set incorrectly, please change it to avoid issues with Memory.',
icon: 'schedule',
id: 4,
updateTimestamp: makeDateFromNow(-2 * 7 * 24 * 60 * 60 * 1000),
},
{
message: '2 new team members added',
icon: 'group_add',
id: 5,
separator: false,
updateTimestamp: makeDateFromNow(-3 * 60 * 1000),
},
{
message: 'Monthly budget exceeded by 10%',
icon: 'trending_up',
id: 6,
separator: true,
updateTimestamp: makeDateFromNow(-3 * 24 * 60 * 60 * 1000),
},
{
message: '7 tasks are approaching their deadlines',
icon: 'alarm',
id: 7,
separator: false,
updateTimestamp: makeDateFromNow(-5 * 60 * 60 * 1000),
},
{
message: 'New software update available',
icon: 'system_update',
id: 8,
separator: true,
updateTimestamp: makeDateFromNow(-1 * 24 * 60 * 60 * 1000),
},
].sort((a, b) => new Date(b.updateTimestamp).getTime() - new Date(a.updateTimestamp).getTime())
const TIME_NAMES = {
second: 1000,
minute: 1000 * 60,
hour: 1000 * 60 * 60,
day: 1000 * 60 * 60 * 24,
week: 1000 * 60 * 60 * 24 * 7,
month: 1000 * 60 * 60 * 24 * 30,
year: 1000 * 60 * 60 * 24 * 365,
}
const getTimeName = (differenceTime: number) => {
return Object.keys(TIME_NAMES).reduce(
(acc, key) => (TIME_NAMES[key as keyof typeof TIME_NAMES] < differenceTime ? key : acc),
'month',
) as keyof typeof TIME_NAMES
}
const notificationsWithRelativeTime = computed(() => {
const list = displayAllNotifications.value ? notifications : notifications.slice(0, baseNumberOfVisibleNotifications)
return list.map((item, index) => {
const timeDifference = Math.round(new Date().getTime() - new Date(item.updateTimestamp).getTime())
const timeName = getTimeName(timeDifference)
let separator = false
const nextItem = list[index + 1]
if (nextItem) {
const nextItemDifference = Math.round(new Date().getTime() - new Date(nextItem.updateTimestamp).getTime())
const nextItemTimeName = getTimeName(nextItemDifference)
if (timeName !== nextItemTimeName) {
separator = true
}
}
return {
...item,
updateTimestamp: rtf.format(-1 * Math.round(timeDifference / TIME_NAMES[timeName]), timeName),
separator,
}
})
})
</script>
<style lang="scss" scoped>
.notification-dropdown {
cursor: pointer;
.notification-dropdown__icon {
position: relative;
display: flex;
align-items: center;
}
.va-dropdown__anchor {
display: inline-block;
}
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div class="profile-dropdown-wrapper">
<VaDropdown v-model="isShown" :offset="[9, 0]" class="profile-dropdown" stick-to-edges>
<template #anchor>
<VaButton preset="secondary" color="textPrimary">
<span class="profile-dropdown__anchor min-w-max">
<slot />
<VaAvatar :size="32" color="warning"> 😍 </VaAvatar>
</span>
</VaButton>
</template>
<VaDropdownContent
class="profile-dropdown__content md:w-60 px-0 py-4 w-full"
:style="{ '--hover-color': hoverColor }"
>
<VaList v-for="group in options" :key="group.name">
<header v-if="group.name" class="uppercase text-[var(--va-secondary)] opacity-80 font-bold text-xs px-4">
{{ t(`user.${group.name}`) }}
</header>
<VaListItem
v-for="item in group.list"
:key="item.name"
class="menu-item px-4 text-base cursor-pointer h-8"
v-bind="resolveLinkAttribute(item)"
>
<VaIcon :name="item.icon" class="pr-1" color="secondary" />
{{ t(`user.${item.name}`) }}
</VaListItem>
<VaListSeparator v-if="group.separator" class="mx-3 my-2" />
</VaList>
</VaDropdownContent>
</VaDropdown>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useColors } from 'vuestic-ui'
const { colors, setHSLAColor } = useColors()
const hoverColor = computed(() => setHSLAColor(colors.focus, { a: 0.1 }))
const { t } = useI18n()
type ProfileListItem = {
name: string
to?: string
href?: string
icon: string
}
type ProfileOptions = {
name: string
separator: boolean
list: ProfileListItem[]
}
withDefaults(
defineProps<{
options?: ProfileOptions[]
}>(),
{
options: () => [
{
name: 'account',
separator: true,
list: [
{
name: 'profile',
to: 'preferences',
icon: 'mso-account_circle',
},
{
name: 'settings',
to: 'settings',
icon: 'mso-settings',
},
],
},
{
name: 'explore',
separator: true,
list: [
{
name: 'faq',
to: 'faq',
icon: 'mso-quiz',
},
{
name: 'helpAndSupport',
href: 'https://discord.gg/u7fQdqQt8c',
icon: 'mso-error',
},
],
},
{
name: '',
separator: false,
list: [
{
name: 'logout',
to: 'login',
icon: 'mso-logout',
},
],
},
],
},
)
const isShown = ref(false)
const resolveLinkAttribute = (item: ProfileListItem) => {
return item.to ? { to: { name: item.to } } : item.href ? { href: item.href, target: '_blank' } : {}
}
</script>
<style lang="scss">
.profile-dropdown {
cursor: pointer;
&__content {
.menu-item:hover {
background: var(--hover-color);
}
}
&__anchor {
display: inline-block;
}
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<VaSidebar v-model="writableVisible" :width="sidebarWidth" :color="color" minimized-width="0">
<VaAccordion v-model="value" multiple>
<VaCollapse v-for="(route, index) in navigationRoutes.routes" :key="index">
<template #header="{ value: isCollapsed }">
<VaSidebarItem
:to="route.children ? undefined : { name: route.name }"
:active="routeHasActiveChild(route)"
:active-color="activeColor"
:text-color="textColor(route)"
:aria-label="`${route.children ? 'Open category ' : 'Visit'} ${t(route.displayName)}`"
role="button"
hover-opacity="0.10"
>
<VaSidebarItemContent class="py-3 pr-2 pl-4">
<VaIcon
v-if="route.meta.icon"
aria-hidden="true"
:name="route.meta.icon"
size="20px"
:color="iconColor(route)"
/>
<VaSidebarItemTitle class="flex justify-between items-center leading-5 font-semibold">
{{ t(route.displayName) }}
<VaIcon v-if="route.children" :name="arrowDirection(isCollapsed)" size="20px" />
</VaSidebarItemTitle>
</VaSidebarItemContent>
</VaSidebarItem>
</template>
<template #body>
<div v-for="(childRoute, index2) in route.children" :key="index2">
<VaSidebarItem
:to="{ name: childRoute.name }"
:active="isActiveChildRoute(childRoute)"
:active-color="activeColor"
:text-color="textColor(childRoute)"
:aria-label="`Visit ${t(route.displayName)}`"
hover-opacity="0.10"
>
<VaSidebarItemContent class="py-3 pr-2 pl-11">
<VaSidebarItemTitle class="leading-5 font-semibold">
{{ t(childRoute.displayName) }}
</VaSidebarItemTitle>
</VaSidebarItemContent>
</VaSidebarItem>
</div>
</template>
</VaCollapse>
</VaAccordion>
</VaSidebar>
</template>
<script lang="ts">
import { defineComponent, watch, ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useColors } from 'vuestic-ui'
import navigationRoutes, { type INavigationRoute } from './NavigationRoutes'
export default defineComponent({
name: 'Sidebar',
props: {
visible: { type: Boolean, default: true },
mobile: { type: Boolean, default: false },
},
emits: ['update:visible'],
setup: (props, { emit }) => {
const { getColor, colorToRgba } = useColors()
const route = useRoute()
const { t } = useI18n()
const value = ref<boolean[]>([])
const writableVisible = computed({
get: () => props.visible,
set: (v: boolean) => emit('update:visible', v),
})
const isActiveChildRoute = (child: INavigationRoute) => route.name === child.name
const routeHasActiveChild = (section: INavigationRoute) => {
if (!section.children) {
return route.path.endsWith(`${section.name}`)
}
return section.children.some(({ name }) => route.path.endsWith(`${name}`))
}
const setActiveExpand = () =>
(value.value = navigationRoutes.routes.map((route: INavigationRoute) => routeHasActiveChild(route)))
const sidebarWidth = computed(() => (props.mobile ? '100vw' : '280px'))
const color = computed(() => getColor('background-secondary'))
const activeColor = computed(() => colorToRgba(getColor('focus'), 0.1))
const iconColor = (route: INavigationRoute) => (routeHasActiveChild(route) ? 'primary' : 'secondary')
const textColor = (route: INavigationRoute) => (routeHasActiveChild(route) ? 'primary' : 'textPrimary')
const arrowDirection = (state: boolean) => (state ? 'va-arrow-up' : 'va-arrow-down')
watch(() => route.fullPath, setActiveExpand, { immediate: true })
return {
writableVisible,
sidebarWidth,
value,
color,
activeColor,
navigationRoutes,
routeHasActiveChild,
isActiveChildRoute,
t,
iconColor,
textColor,
arrowDirection,
}
},
})
</script>

View File

@ -0,0 +1,107 @@
export interface INavigationRoute {
name: string
displayName: string
meta: { icon: string }
children?: INavigationRoute[]
}
export default {
root: {
name: '/',
displayName: 'navigationRoutes.home',
},
routes: [
{
name: 'dashboard',
displayName: 'menu.dashboard',
meta: {
icon: 'vuestic-iconset-dashboard',
},
},
// {
// name: 'users',
// displayName: 'menu.users',
// meta: {
// icon: 'group',
// },
// },
// {
// name: 'projects',
// displayName: 'menu.projects',
// meta: {
// icon: 'folder_shared',
// },
// },
// {
// name: 'payments',
// displayName: 'menu.payments',
// meta: {
// icon: 'credit_card',
// },
// children: [
// {
// name: 'payment-methods',
// displayName: 'menu.payment-methods',
// },
// {
// name: 'pricing-plans',
// displayName: 'menu.pricing-plans',
// },
// {
// name: 'billing',
// displayName: 'menu.billing',
// },
// ],
// },
// {
// name: 'auth',
// displayName: 'menu.auth',
// meta: {
// icon: 'login',
// },
// children: [
// {
// name: 'login',
// displayName: 'menu.login',
// },
// {
// name: 'signup',
// displayName: 'menu.signup',
// },
// {
// name: 'recover-password',
// displayName: 'menu.recover-password',
// },
// ],
// },
// {
// name: 'faq',
// displayName: 'menu.faq',
// meta: {
// icon: 'quiz',
// },
// },
// {
// name: '404',
// displayName: 'menu.404',
// meta: {
// icon: 'vuestic-iconset-files',
// },
// },
// {
// name: 'preferences',
// displayName: 'menu.preferences',
// meta: {
// icon: 'manage_accounts',
// },
// },
// {
// name: 'settings',
// displayName: 'menu.settings',
// meta: {
// icon: 'settings',
// },
// },
] as INavigationRoute[],
}

View File

@ -0,0 +1,14 @@
import Typography from './Typography.vue'
export default {
title: 'Typography',
component: Typography,
tags: ['autodocs'],
}
export const Default = () => ({
components: { Typography },
template: `
<Typography/>
`,
})

View File

@ -0,0 +1,210 @@
<template>
<VaContent class="typography content">
<div class="grid grid-cols-12 gap-6">
<VaCard class="col-span-12">
<VaCardTitle>Primary text styles</VaCardTitle>
<VaCardContent>
<div class="mb-8">
<h1>Display 1 Heading</h1>
<p>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a
greater influence on life on planet Earth than its own satellite, the moon. When you think about it.
</p>
</div>
<div class="mb-8">
<h2>Display 2 Heading</h2>
<p>
None has a greater influence on life on planet Earth than its own satellite, the moon. When you think
about it.
</p>
</div>
<div class="mb-8">
<h3>Display 3 Heading</h3>
<p>
Lets talk about meat fondue recipes and what you need to know first. Meat fondue also known as oil fondue
is a method of cooking all kinds of meats, poultry, and seafood in a pot of heated oil.
</p>
</div>
<div class="mb-8">
<h4>Display 4 Heading</h4>
<p>
There is something about parenthood that gives us a sense of history and a deeply rooted desire to send on
into the next generation the great things we have discovered about life.
</p>
</div>
<div class="mb-8">
<h5>Display 5 Heading</h5>
<p>
There is a moment in the life of any aspiring astronomer that it is time to buy that first telescope. Its
exciting to think about setting up your own viewing station.
</p>
</div>
<div class="mb-8">
<p>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a
greater influence on life on planet Earth than its own satellite, the moon. When you think about it.
</p>
</div>
<div class="mb-8">
<div class="text--secondary">
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a
greater influence on life on planet Earth than its own satellite, the moon. When you think about it.
</div>
</div>
<div class="mb-8">
<pre class="code-snippet">
&lt;p class=code-snippet>
This is a wonderful example.
&lt;a href=# onClick=>Read more&lt;/a>
&lt;/p></pre
>
<p>
Of all of the celestial bodies that capture our attention and fascination as astronomers,
<span class="text--code">currentColor</span> none has a greater influence on life on planet Earth than
its own satellite, the moon.
</p>
</div>
</VaCardContent>
</VaCard>
<VaCard class="col-span-12">
<VaCardTitle>Secondary text styles</VaCardTitle>
<VaCardContent>
<p class="va-h3">Lists</p>
<ol class="va-ordered">
<li>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a
greater influence.
</li>
<li>Earth than its own satellite, the moon. When you think about it.</li>
<li>Attention and fascination as.</li>
</ol>
<ol class="va-ordered">
<li>Coffee</li>
<li>
Tea
<ol class="va-ordered">
<li>
Black tea
<ol class="va-ordered">
<li>Brooke Bond</li>
<li>Lipton</li>
</ol>
</li>
<li>
Green tea
<ol class="va-ordered">
<li>Greenfield</li>
<li>Tess</li>
</ol>
</li>
</ol>
</li>
<li>Milk</li>
</ol>
<ul class="va-unordered">
<li>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a
greater influence.
</li>
<li>Earth than its own satellite, the moon. When you think about it.</li>
<li>Attention and fascination as .</li>
</ul>
<ul class="va-unordered">
<li>Coffee</li>
<li>
Tea
<ul class="va-unordered">
<li>
Black tea
<ul class="va-unordered">
<li>Brooke Bond</li>
<li>Lipton</li>
</ul>
</li>
<li>
Green tea
<ul class="va-unordered">
<li>Greenfield</li>
<li>Tess</li>
</ul>
</li>
</ul>
</li>
<li>Milk</li>
</ul>
<p class="va-h3">Links</p>
<div class="mb-8">
<a class="link mr-8" href="/default" @click.prevent> Default Link </a>
<a class="link-secondary" href="/secondary" @click.prevent> Secondary Link </a>
</div>
<div class="mb-8">
<p class="va-h3">Other Elements</p>
<p>
None has a greater influence on
<span class="text--highlighted">highlighted text</span>
life on planet Earth than its own satellite, the selected chunk of text. When you think about it.
</p>
</div>
<div class="mb-8">
<blockquote class="va-blockquote border-primary">
<p>
BQ: Lets talk about meat fondue recipes and what you need to know first. Meat fondue also known as oil
fondue is a method of cooking all kinds.
</p>
<p>
<i> Mister Lebowski</i>
</p>
</blockquote>
</div>
<div class="mb-8">
<div class="text-block">
<p class="va-h3">va-h3 Heading</p>
<span
>Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a
greater influence on life on planet Earth than its own satellite, the moon. When you think about
it.</span
>
</div>
</div>
<div class="mb-8">
<table class="va-table">
<thead>
<tr>
<th v-for="(data, index) in tableData[0]" :key="index">{{ data }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(rowData, rowIndex) in tableData.slice(1)" :key="rowIndex">
<td v-for="(itemData, colIndex) in rowData" :key="colIndex">
{{ itemData }}
</td>
</tr>
</tbody>
</table>
</div>
</VaCardContent>
</VaCard>
</div>
</VaContent>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
// import { useI18n } from 'vue-i18n'
//
// const { t } = useI18n()
const tableData = computed(() => [
['Id', 'FooBar type', 'Actions'],
['1', 'Zebra', 'Delete'],
['2', 'Not Zebra', 'Remove'],
['3', 'Very Zebra', 'Eradicate'],
])
</script>
<style lang="scss" scoped>
.text--secondary {
color: var(--va-secondary);
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<component :is="chartComponent" :chart-data="data" :data="data" :options="chartOptions" class="va-chart" />
</template>
<script lang="ts" setup generic="T extends 'line' | 'bar' | 'bubble' | 'doughnut' | 'pie'">
import { computed } from 'vue'
import type { ChartOptions, ChartData } from 'chart.js'
import { defaultConfig, chartTypesMap } from './vaChartConfigs'
defineOptions({
name: 'VaChart',
})
const props = defineProps<{
data: ChartData<T>
options?: ChartOptions<T>
type: T
}>()
const chartComponent = chartTypesMap[props.type]
const chartOptions = computed<ChartOptions<T>>(() => ({
...(defaultConfig as any),
...props.options,
}))
</script>
<style lang="scss">
.va-chart {
min-width: 100%;
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
> * {
height: 100%;
width: 100%;
}
canvas {
width: 100%;
height: auto;
min-height: 20px;
}
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<Bar :data="data" :options="options" />
</template>
<script lang="ts" setup>
import { Bar } from 'vue-chartjs'
import type { ChartOptions } from 'chart.js'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale } from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale)
defineProps<{
data: any
options?: ChartOptions<'bar'>
}>()
</script>

View File

@ -0,0 +1,17 @@
<template>
<Bubble :data="props.data" :options="options" />
</template>
<script lang="ts" setup>
import { Bubble } from 'vue-chartjs'
import type { ChartOptions } from 'chart.js'
import { Chart as ChartJS, Title, Tooltip, Legend, PointElement, LinearScale } from 'chart.js'
import { TBubbleChartData } from '../../../data/types'
ChartJS.register(Title, Tooltip, Legend, PointElement, LinearScale)
const props = defineProps<{
data: TBubbleChartData
options?: ChartOptions<'bubble'>
}>()
</script>

View File

@ -0,0 +1,17 @@
<template>
<Doughnut :data="props.data" :options="options" />
</template>
<script lang="ts" setup>
import { Doughnut } from 'vue-chartjs'
import type { ChartOptions } from 'chart.js'
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale } from 'chart.js'
import { TDoughnutChartData } from '../../../data/types'
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale)
const props = defineProps<{
data: TDoughnutChartData
options?: ChartOptions<'doughnut'>
}>()
</script>

View File

@ -0,0 +1,26 @@
<template>
<Bar :data="props.data" :options="{ ...options, ...horizontalBarOptions }" />
</template>
<script lang="ts" setup>
import { Bar } from 'vue-chartjs'
import type { ChartOptions } from 'chart.js'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale } from 'chart.js'
import { TBarChartData } from '../../../data/types'
ChartJS.register(Title, Tooltip, Legend, BarElement, LinearScale, CategoryScale)
const horizontalBarOptions = {
indexAxis: 'y' as 'x' | 'y',
elements: {
bar: {
borderWidth: 1,
},
},
}
const props = defineProps<{
data: TBarChartData
options?: ChartOptions<'bar'>
}>()
</script>

View File

@ -0,0 +1,72 @@
<template>
<Line ref="chart" :data="computedChartData" :options="options" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Line } from 'vue-chartjs'
import type { ChartOptions } from 'chart.js'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
Filler,
} from 'chart.js'
import { TLineChartData } from '../../../data/types'
import { computed } from 'vue'
import { useColors } from 'vuestic-ui/web-components'
ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale, Filler)
const chart = ref<typeof Line>()
const props = defineProps<{
data: TLineChartData
options?: ChartOptions<'line'>
}>()
const ctx = computed(() => {
if (!chart.value) {
return null
}
return chart.value.chart?.ctx ?? null
})
const { setHSLAColor, getColor } = useColors()
const colors = ['primary', 'success', 'danger', 'warning']
const computedChartData = computed<TLineChartData>(() => {
if (!ctx.value) {
return props.data
}
const makeGradient = (bg: string) => {
const gradient = ctx.value!.createLinearGradient(0, 0, 0, 90)
gradient.addColorStop(0, setHSLAColor(bg, { a: 0.4 }))
gradient.addColorStop(1, setHSLAColor(bg, { a: 0.0 }))
return gradient
}
const datasets = props.data.datasets.map((dataset, index) => {
const color = getColor(colors[index % colors.length])
return {
...dataset,
fill: true,
backgroundColor: makeGradient(color),
borderColor: color,
pointRadius: 0,
borderWidth: 2,
}
})
return { ...props.data, datasets }
})
</script>

View File

@ -0,0 +1,66 @@
<template>
<canvas ref="canvas" style="max-width: 100%" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, ChartOptions } from 'chart.js'
import { ChoroplethController, ProjectionScale, ColorScale, GeoFeature } from 'chartjs-chart-geo'
import { watchEffect } from 'vue'
import { ChartData } from 'chart.js'
ChartJS.register(
Title,
Tooltip,
Legend,
ArcElement,
CategoryScale,
ChoroplethController,
ProjectionScale,
ColorScale,
GeoFeature,
)
const canvas = ref<HTMLCanvasElement | null>(null)
function getColor(revenue: number) {
return revenue >= 0.9 ? '#63A6F8' : revenue > 0.4 ? '#8FC0FA' : '#EDF0F1'
}
const props = defineProps<{
options?: ChartOptions<'choropleth'>
data: ChartData<'choropleth', { feature: any; value: number }[], string>
}>()
watchEffect(() => {
if (canvas.value === null) {
return
}
new ChartJS(canvas.value.getContext('2d')!, {
type: 'choropleth',
data: props.data,
options: {
plugins: {
legend: {
display: false,
},
},
scales: {
projection: {
axis: 'x',
projection: 'mercator',
projectionScale: 1.6,
},
color: {
axis: 'x',
quantize: 5,
display: false,
interpolate: getColor,
},
},
animation: false,
},
})
})
</script>

View File

@ -0,0 +1,17 @@
<template>
<Pie :data="props.data" :options="options" />
</template>
<script lang="ts" setup>
import { Pie } from 'vue-chartjs'
import type { ChartOptions } from 'chart.js'
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale } from 'chart.js'
import { TPieChartData } from '../../../data/types'
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale)
const props = defineProps<{
data: TPieChartData
options?: ChartOptions<'pie'>
}>()
</script>

View File

@ -0,0 +1,117 @@
import { Chart, TooltipModel } from 'chart.js'
import { computePosition, flip, shift } from '@floating-ui/dom'
const getOrCreateTooltip = (chart: Chart) => {
let tooltipEl = chart.canvas.parentNode?.querySelector('div')
if (!tooltipEl) {
tooltipEl = document.createElement('div')
tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)'
tooltipEl.style.borderRadius = '3px'
tooltipEl.style.color = 'white'
tooltipEl.style.opacity = '1'
tooltipEl.style.pointerEvents = 'none'
tooltipEl.style.position = 'absolute'
// tooltipEl.style.transform = 'translate(-50%, 0)'
tooltipEl.style.left = '0'
tooltipEl.style.top = '0'
tooltipEl.style.transition = 'all .1s ease'
tooltipEl.style.height = 'min-content'
tooltipEl.style.maxWidth = '200px'
tooltipEl.style.zIndex = '9999'
const table = document.createElement('table')
table.style.margin = '0px'
tooltipEl.appendChild(table)
chart.canvas.parentNode?.appendChild(tooltipEl)
}
return tooltipEl
}
export const externalTooltipHandler = (context: { chart: Chart; tooltip: TooltipModel<any> }) => {
// Tooltip Element
const { chart, tooltip } = context
const tooltipEl = getOrCreateTooltip(chart)
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = '0'
return
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || []
const bodyLines = tooltip.body.map((b) => b.lines)
const tableHead = document.createElement('thead')
titleLines.forEach((title) => {
const tr = document.createElement('tr')
tr.style.borderWidth = '0'
const th = document.createElement('th')
th.style.borderWidth = '0'
const text = document.createTextNode(title)
th.appendChild(text)
tr.appendChild(th)
tableHead.appendChild(tr)
})
const tableBody = document.createElement('tbody')
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i]
const span = document.createElement('span')
span.style.background = String(colors.backgroundColor)
span.style.borderColor = String(colors.borderColor)
span.style.borderWidth = '2px'
span.style.marginRight = '10px'
span.style.height = '10px'
span.style.width = '10px'
span.style.display = 'inline-block'
const tr = document.createElement('tr')
tr.style.backgroundColor = 'inherit'
tr.style.borderWidth = '0'
const td = document.createElement('td')
td.style.borderWidth = '0'
const text = document.createTextNode(body as any)
td.appendChild(span)
td.appendChild(text)
tr.appendChild(td)
tableBody.appendChild(tr)
})
const tableRoot = tooltipEl.querySelector('table')
// Remove old children
while (tableRoot?.firstChild) {
tableRoot.firstChild.remove()
}
// Add new children
tableRoot?.appendChild(tableHead)
tableRoot?.appendChild(tableBody)
}
// Display, position, and set styles for font
tooltipEl.style.opacity = '1'
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px'
computePosition(chart.canvas.parentNode! as HTMLElement, tooltipEl!, {
placement: 'top',
middleware: [flip(), shift()],
}).then(({ x, y }) => {
Object.assign(tooltipEl!.style, {
left: `${x}px`,
top: `${y}px`,
})
})
}

View File

@ -0,0 +1,107 @@
import { defineAsyncComponent, markRaw } from 'vue'
const DEFAULT_FONT_FAMILY = "'Inter', sans-serif"
export const defaultConfig = {
scales: {
x: {
ticks: {
font: {
family: DEFAULT_FONT_FAMILY,
},
},
},
y: {
ticks: {
font: {
family: DEFAULT_FONT_FAMILY,
},
},
},
},
plugins: {
legend: {
position: 'bottom',
labels: {
font: {
color: '#34495e',
family: DEFAULT_FONT_FAMILY,
size: 14,
},
usePointStyle: true,
},
},
tooltip: {
bodyFont: {
size: 14,
family: DEFAULT_FONT_FAMILY,
},
boxPadding: 4,
},
},
datasets: {
line: {
fill: 'origin',
tension: 0.3,
borderColor: 'transparent',
},
bubble: {
borderColor: 'transparent',
},
bar: {
borderColor: 'transparent',
},
},
maintainAspectRatio: false,
animation: true,
}
export const doughnutConfig = {
cutout: '80%',
scales: {
x: {
display: false,
grid: {
display: false, // Disable X-axis grid lines ("net")
},
},
y: {
display: false,
grid: {
display: false, // Disable Y-axis grid lines ("net")
},
ticks: {
display: false, // Hide Y-axis values
},
},
},
plugins: {
legend: {
display: false,
},
},
datasets: {
line: {
fill: 'origin',
tension: 0.3,
borderColor: 'transparent',
},
bubble: {
borderColor: 'transparent',
},
bar: {
borderColor: 'transparent',
},
},
maintainAspectRatio: false,
animation: true,
}
export const chartTypesMap = {
pie: markRaw(defineAsyncComponent(() => import('./chart-types/PieChart.vue'))),
doughnut: markRaw(defineAsyncComponent(() => import('./chart-types/DoughnutChart.vue'))),
bubble: markRaw(defineAsyncComponent(() => import('./chart-types/BubbleChart.vue'))),
line: markRaw(defineAsyncComponent(() => import('./chart-types/LineChart.vue'))),
bar: markRaw(defineAsyncComponent(() => import('./chart-types/BarChart.vue'))),
'horizontal-bar': markRaw(defineAsyncComponent(() => import('./chart-types/HorizontalBarChart.vue'))),
}

View File

@ -0,0 +1,190 @@
<template>
<div ref="editorElement" class="va-medium-editor content">
<slot />
</div>
</template>
<script lang="ts" setup>
import { ref, Ref, onMounted, onBeforeUnmount } from 'vue'
import MediumEditor from 'medium-editor'
const props = withDefaults(
defineProps<{
editorOptions?: {
buttonLabels: string
autoLink: boolean
toolbar: {
buttons: string[]
}
}
}>(),
{
editorOptions: () => ({
buttonLabels: 'fontawesome',
autoLink: true,
toolbar: {
buttons: ['bold', 'italic', 'underline', 'anchor', 'h1', 'h2', 'h3'],
},
}),
},
)
const emit = defineEmits<{
(e: 'initialized', editor: typeof MediumEditor): void
}>()
const editorElement: Ref<null | HTMLElement> = ref(null)
let editor: typeof MediumEditor | null = null
onMounted(() => {
if (!editorElement.value) {
return
}
editor = new MediumEditor(editorElement.value, props.editorOptions)
emit('initialized', editor)
})
onBeforeUnmount(() => {
if (editor) {
editor.destroy()
}
})
</script>
<style lang="scss">
@import 'medium-editor/src/sass/medium-editor';
@import 'variables';
$medium-editor-shadow: var(--va-box-shadow);
$medium-editor-background-color: var(--va-divider);
$medium-editor-text-color: var(--va-dark);
$medium-editor-active-background-color: var(--va-primary);
$medium-editor-active-text-color: var(--va-white);
.va-medium-editor {
margin-bottom: var(--va-medium-editor-margin-bottom);
min-width: var(--va-medium-editor-min-width);
max-width: var(--va-medium-editor-max-width);
&:focus {
outline: none;
}
&.content {
i {
font-style: italic;
}
}
}
// isn't a part of the .va-medium-editor, so can't be places inside it
.medium-editor-toolbar,
.medium-editor-toolbar-form,
.medium-editor-toolbar-actions,
.medium-editor-toolbar-anchor-preview {
box-shadow: $medium-editor-shadow;
background-color: $medium-editor-background-color;
border-radius: 1.5rem;
height: 44px;
line-height: 42px;
}
.medium-editor-toolbar-anchor-preview {
a {
padding: 0 2rem;
margin: 0;
line-height: 44px;
}
}
.medium-editor-toolbar {
box-shadow: $medium-editor-shadow;
.medium-editor-toolbar-actions {
overflow: hidden;
height: 44px;
}
.medium-editor-action {
margin: 0;
border: 0;
padding: 0.375rem 1rem;
height: 44px;
background-color: $medium-editor-background-color;
box-shadow: none;
border-radius: 0;
i {
color: $medium-editor-text-color;
}
&.medium-editor-button-active {
background-color: $medium-editor-active-background-color;
color: $medium-editor-active-text-color;
i {
color: $medium-editor-active-text-color;
}
}
}
& > .medium-editor-action:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
& > .medium-editor-action + .medium-editor-action {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
}
.medium-editor-toolbar-form {
color: $medium-editor-text-color;
overflow: hidden;
a {
color: $medium-editor-text-color;
transform: translateY(1px);
}
input {
margin-left: 4px !important;
transform: translateY(-2px);
border-radius: 13px;
}
.medium-editor-toolbar-close {
margin-right: 1rem;
}
}
.medium-toolbar-arrow-under::after {
border-color: $medium-editor-background-color transparent transparent transparent;
top: 100%;
}
.medium-toolbar-arrow-over::before {
border-color: transparent transparent var(--va-primary) transparent;
}
.medium-editor-toolbar-anchor-preview {
// @include va-button($btn-padding-y-nrm, $btn-padding-x-nrm, $btn-font-size-nrm, $btn-line-height-nrm, $btn-border-radius-nrm);
.medium-editor-toolbar-anchor-preview {
margin: 0;
}
}
.medium-editor-anchor-preview {
max-width: 50%;
a {
color: $medium-editor-text-color;
text-decoration: none;
}
}
</style>

View File

@ -0,0 +1,9 @@
:root {
--va-medium-editor-margin-bottom: 2.25rem;
--va-medium-editor-min-width: 6rem;
--va-medium-editor-max-width: 600px;
/* Toolbar */
--va-medium-editor-toolbar-max-width: 90%;
--va-medium-editor-toolbar-box-shadow: none;
}

View File

@ -0,0 +1,82 @@
<template>
<tr class="va-timeline-item">
<td class="va-timeline-item__icon-cell">
<div class="va-timeline-item__icon">
<VaIcon name="schedule" size="22px" color="backgroundBorder" />
</div>
</td>
<td class="va-timeline-item__content-cell">
<div class="va-timeline-item__content">
<slot />
</div>
</td>
<td class="va-timeline-item__date-cell">
<slot name="date">
{{ $props.date }}
</slot>
</td>
</tr>
</template>
<script setup lang="ts">
defineProps({
date: {
type: String,
default: '',
},
})
</script>
<style lang="scss" scoped>
.va-timeline-item {
display: table-row;
&__icon-cell {
vertical-align: top;
height: 1px;
padding-right: 1rem;
}
&__icon {
width: 24px;
position: relative;
display: inline-flex;
justify-content: center;
flex-direction: column;
align-items: center;
height: 100%;
&::after {
content: '';
width: 2px;
height: 100%;
background: var(--va-background-border);
}
}
&__content {
margin-bottom: 1rem;
}
&__content-cell {
width: 100%;
}
&__date-cell {
vertical-align: top;
color: var(--va-secondary);
text-wrap: nowrap;
white-space: nowrap;
padding-left: 0.5rem;
text-align: end;
}
&:last-child {
.va-timeline-item__icon {
&::after {
background: transparent;
}
}
}
}
</style>

243
src/data/CountriesList.ts Normal file
View File

@ -0,0 +1,243 @@
export default [
'Afghanistan',
'Albania',
'Algeria',
'American Samoa',
'Andorra',
'Angola',
'Anguilla',
'Antarctica',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Aruba',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bermuda',
'Bhutan',
'Bolivia',
'Bosnia and Herzegowina',
'Botswana',
'Bouvet Island',
'Brazil',
'British Indian Ocean Territory',
'Brunei Darussalam',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cambodia',
'Cameroon',
'Canada',
'Cape Verde',
'Cayman Islands',
'Central African Republic',
'Chad',
'Chile',
'China',
'Christmas Island',
'Cocos (Keeling) Islands',
'Colombia',
'Comoros',
'Congo',
'Congo, the Democratic Republic of the',
'Cook Islands',
'Costa Rica',
"Cote d'Ivoire",
'Croatia (Hrvatska)',
'Cuba',
'Cyprus',
'Czech Republic',
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic',
'East Timor',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Ethiopia',
'Falkland Islands (Malvinas)',
'Faroe Islands',
'Fiji',
'Finland',
'France',
'France Metropolitan',
'French Guiana',
'French Polynesia',
'French Southern Territories',
'Gabon',
'Gambia',
'Georgia',
'Germany',
'Ghana',
'Gibraltar',
'Greece',
'Greenland',
'Grenada',
'Guadeloupe',
'Guam',
'Guatemala',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Heard and Mc Donald Islands',
'Holy See (Vatican City State)',
'Honduras',
'Hong Kong',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran (Islamic Republic of)',
'Iraq',
'Ireland',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
"Korea, Democratic People's Republic of",
'Korea, Republic of',
'Kuwait',
'Kyrgyzstan',
"Lao, People's Democratic Republic",
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libyan Arab Jamahiriya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Macau',
'Macedonia, The Former Yugoslav Republic of',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands',
'Martinique',
'Mauritania',
'Mauritius',
'Mayotte',
'Mexico',
'Micronesia, Federated States of',
'Moldova, Republic of',
'Monaco',
'Mongolia',
'Montserrat',
'Morocco',
'Mozambique',
'Myanmar',
'Namibia',
'Nauru',
'Nepal',
'Netherlands',
'Netherlands Antilles',
'New Caledonia',
'New Zealand',
'Nicaragua',
'Niger',
'Nigeria',
'Niue',
'Norfolk Island',
'Northern Mariana Islands',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines',
'Pitcairn',
'Poland',
'Portugal',
'Puerto Rico',
'Qatar',
'Reunion',
'Romania',
'Russian Federation',
'Rwanda',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Slovakia (Slovak Republic)',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Georgia and the South Sandwich Islands',
'Spain',
'Sri Lanka',
'St. Helena',
'St. Pierre and Miquelon',
'Sudan',
'Suriname',
'Svalbard and Jan Mayen Islands',
'Swaziland',
'Sweden',
'Switzerland',
'Syrian Arab Republic',
'Taiwan, Province of China',
'Tajikistan',
'Tanzania, United Republic of',
'United States of America',
'Thailand',
'Togo',
'Tokelau',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Turks and Caicos Islands',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates',
'United Kingdom',
'United States',
'United States Minor Outlying Islands',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Venezuela',
'Vietnam',
'Virgin Islands (British)',
'Virgin Islands (U.S.)',
'Wallis and Futuna Islands',
'Western Sahara',
'Yemen',
'Yugoslavia',
'Zambia',
'Zimbabwe',
]

View File

@ -0,0 +1,30 @@
import { TBarChartData } from '../types'
export const barChartData: TBarChartData = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Last year',
backgroundColor: 'primary',
data: [50, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11],
},
{
label: 'Current year',
backgroundColor: 'info',
data: [50, 10, 22, 39, 15, 20, 85, 32, 60, 50, 20, 30],
},
],
}

View File

@ -0,0 +1,231 @@
import { TBubbleChartData } from '../types'
export const bubbleChartData: TBubbleChartData = {
datasets: [
{
label: 'USA',
backgroundColor: 'danger',
data: [
{
x: 23,
y: 25,
r: 15,
},
{
x: 40,
y: 10,
r: 10,
},
{
x: 30,
y: 22,
r: 30,
},
{
x: 7,
y: 43,
r: 40,
},
{
x: 23,
y: 27,
r: 12,
},
{
x: 20,
y: 15,
r: 11,
},
{
x: 7,
y: 10,
r: 35,
},
{
x: 10,
y: 20,
r: 40,
},
],
},
{
label: 'Russia',
backgroundColor: 'primary',
data: [
{
x: 0,
y: 30,
r: 15,
},
{
x: 20,
y: 20,
r: 20,
},
{
x: 15,
y: 15,
r: 50,
},
{
x: 31,
y: 46,
r: 30,
},
{
x: 20,
y: 14,
r: 25,
},
{
x: 34,
y: 17,
r: 30,
},
{
x: 44,
y: 44,
r: 10,
},
{
x: 39,
y: 25,
r: 35,
},
],
},
{
label: 'Canada',
backgroundColor: 'warning',
data: [
{
x: 10,
y: 30,
r: 45,
},
{
x: 10,
y: 50,
r: 20,
},
{
x: 5,
y: 5,
r: 30,
},
{
x: 40,
y: 30,
r: 20,
},
{
x: 33,
y: 15,
r: 18,
},
{
x: 40,
y: 20,
r: 40,
},
{
x: 33,
y: 33,
r: 40,
},
],
},
{
label: 'Belarus',
backgroundColor: 'info',
data: [
{
x: 35,
y: 30,
r: 45,
},
{
x: 25,
y: 40,
r: 35,
},
{
x: 5,
y: 5,
r: 30,
},
{
x: 5,
y: 20,
r: 40,
},
{
x: 10,
y: 40,
r: 15,
},
{
x: 3,
y: 10,
r: 10,
},
{
x: 15,
y: 40,
r: 40,
},
{
x: 7,
y: 15,
r: 10,
},
],
},
{
label: 'Ukraine',
backgroundColor: 'success',
data: [
{
x: 25,
y: 10,
r: 40,
},
{
x: 17,
y: 40,
r: 40,
},
{
x: 35,
y: 10,
r: 20,
},
{
x: 3,
y: 40,
r: 10,
},
{
x: 40,
y: 40,
r: 40,
},
{
x: 20,
y: 10,
r: 10,
},
{
x: 10,
y: 27,
r: 35,
},
{
x: 7,
y: 26,
r: 40,
},
],
},
],
}

View File

@ -0,0 +1,34 @@
import { computed, ref, watch } from 'vue'
import { useColors, useGlobalConfig } from 'vuestic-ui'
type chartColors = string | string[]
export function useChartColors(chartColors = [] as chartColors, alfa = 0.6) {
const { getGlobalConfig } = useGlobalConfig()
const { setHSLAColor, getColor } = useColors()
const generateHSLAColors = (colors: chartColors) =>
typeof colors === 'string'
? setHSLAColor(getColor(colors), { a: alfa })
: colors.map((color) => setHSLAColor(getColor(color), { a: alfa }))
const generateColors = (colors: chartColors) =>
typeof colors === 'string' ? getColor(colors) : colors.map((color) => getColor(color))
const generatedHSLAColors = ref(generateHSLAColors(chartColors))
const generatedColors = ref(generateColors(chartColors))
const theme = computed(() => getGlobalConfig().colors!)
watch(theme, () => {
generatedHSLAColors.value = generateHSLAColors(chartColors)
generatedColors.value = generateColors(chartColors)
})
return {
generateHSLAColors,
generateColors,
generatedColors,
generatedHSLAColors,
}
}

View File

@ -0,0 +1,20 @@
import { computed, ComputedRef } from 'vue'
import { useChartColors } from './useChartColors'
import { TChartData } from '../../types'
export function useChartData<T extends TChartData>(data: T, alfa?: number): ComputedRef<T> {
const datasetsColors = data.datasets.map((dataset) => dataset.backgroundColor as string)
const datasetsThemesColors = datasetsColors.map(
(colors) => useChartColors(colors, alfa)[alfa ? 'generatedHSLAColors' : 'generatedColors'],
)
return computed(() => {
const datasets = data.datasets.map((dataset, idx) => ({
...dataset,
backgroundColor: datasetsThemesColors[idx].value,
}))
return { ...data, datasets } as T
})
}

View File

@ -0,0 +1,16 @@
import { TDoughnutChartData } from '../types'
export const profitBackground = '#154EC1'
export const expensesBackground = '#fff'
export const earningsBackground = '#ECF0F1'
export const doughnutChartData: TDoughnutChartData = {
labels: ['Profit', 'Expenses'],
datasets: [
{
label: 'Yearly Breakdown',
backgroundColor: [profitBackground, earningsBackground],
data: [432, 167],
},
],
}

View File

@ -0,0 +1,30 @@
import { TBarChartData } from '../types'
export const horizontalBarChartData: TBarChartData = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Vuestic Satisfaction Score',
backgroundColor: 'primary',
data: [80, 90, 50, 70, 60, 90, 50, 90, 80, 40, 72, 93],
},
{
label: 'Bulma Satisfaction Score',
backgroundColor: 'danger',
data: [20, 30, 20, 40, 50, 40, 15, 60, 30, 20, 42, 53],
},
],
}

8
src/data/charts/index.ts Normal file
View File

@ -0,0 +1,8 @@
export { bubbleChartData } from './bubbleChartData'
export { doughnutChartData } from './doughnutChartData'
export { barChartData } from './barChartData'
export { horizontalBarChartData } from './horizontalBarChartData'
export { lineChartData } from './lineChartData'
export { pieChartData } from './pieChartData'
// TODO: clean up charts data, after dashboard rework

View File

@ -0,0 +1,25 @@
import { TLineChartData } from '../types'
export const lineChartData: TLineChartData = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
datasets: [
{
label: 'Monthly Earnings',
backgroundColor: 'rgba(75,192,192,0.4)',
data: [10, 35, 14, 17, 12, 40, 75, 55, 30, 51, 25, 7], // Random values
},
],
}

View File

@ -0,0 +1,12 @@
import { TLineChartData } from '../types'
export const pieChartData: TLineChartData = {
labels: ['Africa', 'Asia', 'Europe'],
datasets: [
{
label: 'Population (millions)',
backgroundColor: ['primary', 'warning', 'danger'],
data: [2478, 5267, 734],
},
],
}

View File

@ -0,0 +1,33 @@
export const earningsColor = '#49A8FF'
export const expensesColor = '#154EC1'
export const months: string[] = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
export type Revenues = {
month: string
earning: number
expenses: number
}
export const generateRevenues = (months: string[]): Revenues[] => {
return months.map((month: string) => {
const earning = Math.floor(Math.random() * 100000 + 10000)
return {
month,
earning,
expenses: Math.floor(earning * Math.random()),
}
})
}
export const getRevenuePerMonth = (month: string, revenues: Revenues[]): Revenues => {
const revenue = revenues.find((revenue) => revenue.month === month)
return revenue || { month, earning: 0, expenses: 0 }
}
export const formatMoney = (amount: number, currency = 'USD'): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount)
}

43198
src/data/geo.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
[
{
"id": 0,
"project_name": "Vuestic",
"project_owner": 13,
"team": [13, 5, 28, 14, 17, 28, 23, 11, 16, 19, 12, 28, 11],
"status": "in progress",
"creation_date": "20 Nov 2023"
},
{
"id": 1,
"project_name": "Mood board",
"project_owner": 28,
"team": [28, 10, 12, 28, 14, 27, 5, 4, 8, 23, 19, 18, 24, 11, 18, 12, 28],
"status": "important",
"creation_date": "16 Oct 2023"
},
{
"id": 2,
"project_name": "Jenkins",
"project_owner": 3,
"team": [3, 21, 7, 19, 4, 4, 7, 24],
"status": "important",
"creation_date": "1 Oct 2023"
},
{
"id": 3,
"project_name": "Springfield media",
"project_owner": 17,
"team": [17, 25, 21, 9, 18, 12, 15, 0, 7, 2, 7],
"status": "important",
"creation_date": "19 Sept 2023"
},
{
"id": 4,
"project_name": "Galileo",
"project_owner": 7,
"team": [7, 1, 28, 19, 3],
"status": "completed",
"creation_date": "23 Sept 2023"
},
{
"id": 5,
"project_name": "Website redesign",
"project_owner": 24,
"team": [24, 19, 1, 8, 9],
"status": "completed",
"creation_date": "9 Sept 2023"
},
{
"id": 6,
"project_name": "Toolset landing",
"project_owner": 15,
"team": [15, 16, 8, 6, 11, 21, 3, 20],
"status": "archived",
"creation_date": "17 Aug 2023"
},
{
"id": 7,
"project_name": "Complete product redesign",
"project_owner": 25,
"team": [25, 18, 24, 13, 5, 3, 4, 16, 25, 12, 18, 9, 22],
"status": "completed",
"creation_date": "11 Aug 2023"
},
{
"id": 8,
"project_name": "Design team project",
"project_owner": 17,
"team": [17, 6, 21, 17, 7, 6, 14, 13, 27, 7, 20],
"status": "archived",
"creation_date": "9 Aug 2023"
},
{
"id": 9,
"project_name": "Regular logistics",
"project_owner": 3,
"team": [3, 26, 8, 15, 21, 23, 18, 11, 22, 6, 20, 9],
"status": "archived",
"creation_date": "2 Aug 2023"
},
{
"id": 10,
"project_name": "Aurora Analytics",
"project_owner": 12,
"team": [12, 7, 18, 24, 13, 5],
"status": "in progress",
"creation_date": "10 Dec 2023"
},
{
"id": 11,
"project_name": "Quantum Leap",
"project_owner": 14,
"team": [14, 17, 3, 21, 11, 20],
"status": "planning",
"creation_date": "22 Nov 2023"
},
{
"id": 12,
"project_name": "Deep Dive Research",
"project_owner": 8,
"team": [8, 15, 9, 3, 27, 6],
"status": "important",
"creation_date": "15 Nov 2023"
},
{
"id": 13,
"project_name": "Sky High Architecture",
"project_owner": 21,
"team": [21, 2, 17, 18, 4],
"status": "completed",
"creation_date": "1 Nov 2023"
},
{
"id": 14,
"project_name": "Tech Horizon",
"project_owner": 9,
"team": [9, 19, 24, 1, 22],
"status": "in progress",
"creation_date": "28 Oct 2023"
},
{
"id": 15,
"project_name": "Edge of Innovation",
"project_owner": 16,
"team": [16, 11, 5, 14, 23],
"status": "planning",
"creation_date": "21 Oct 2023"
},
{
"id": 16,
"project_name": "Crypto Ventures",
"project_owner": 20,
"team": [20, 7, 15, 26, 12],
"status": "important",
"creation_date": "10 Oct 2023"
},
{
"id": 17,
"project_name": "Blockchain Basics",
"project_owner": 4,
"team": [4, 8, 3, 22, 27],
"status": "archived",
"creation_date": "5 Oct 2023"
},
{
"id": 18,
"project_name": "Virtual Reality Exploration",
"project_owner": 26,
"team": [26, 2, 14, 20, 9],
"status": "in progress",
"creation_date": "29 Sept 2023"
},
{
"id": 19,
"project_name": "AI in Daily Life",
"project_owner": 2,
"team": [2, 13, 24, 5, 11],
"status": "completed",
"creation_date": "22 Sept 2023"
}
]

102
src/data/pages/projects.ts Normal file
View File

@ -0,0 +1,102 @@
import { sleep } from '../../services/utils'
import projectsDb from './projects-db.json'
import usersDb from './users-db.json'
// Simulate API calls
export type Pagination = {
page: number
perPage: number
total: number
}
export type Sorting = {
sortBy: keyof (typeof projectsDb)[number] | undefined
sortingOrder: 'asc' | 'desc' | null
}
const getSortItem = (obj: any, sortBy: keyof (typeof projectsDb)[number]) => {
if (sortBy === 'project_owner') {
return obj.project_owner.fullname
}
if (sortBy === 'team') {
return obj.team.map((user: any) => user.fullname).join(', ')
}
if (sortBy === 'creation_date') {
return new Date(obj[sortBy])
}
return obj[sortBy]
}
export const getProjects = async (options: Sorting & Pagination) => {
await sleep(1000)
const projects = projectsDb.map((project) => ({
...project,
project_owner: usersDb.find((user) => user.id === project.project_owner)! as (typeof usersDb)[number],
team: usersDb.filter((user) => project.team.includes(user.id)) as (typeof usersDb)[number][],
}))
if (options.sortBy && options.sortingOrder) {
projects.sort((a, b) => {
a = getSortItem(a, options.sortBy!)
b = getSortItem(b, options.sortBy!)
if (a < b) {
return options.sortingOrder === 'asc' ? -1 : 1
}
if (a > b) {
return options.sortingOrder === 'asc' ? 1 : -1
}
return 0
})
}
const normalizedProjects = projects.slice((options.page - 1) * options.perPage, options.page * options.perPage)
return {
data: normalizedProjects,
pagination: {
page: options.page,
perPage: options.perPage,
total: projectsDb.length,
},
}
}
export const addProject = async (project: Omit<(typeof projectsDb)[number], 'id' | 'creation_date'>) => {
await sleep(1000)
const newProject = {
...project,
id: projectsDb.length + 1,
creation_date: new Date().toLocaleDateString('gb', { day: 'numeric', month: 'short', year: 'numeric' }),
}
projectsDb.push(newProject)
return {
...newProject,
project_owner: usersDb.find((user) => user.id === project.project_owner)! as (typeof usersDb)[number],
team: usersDb.filter((user) => project.team.includes(user.id)) as (typeof usersDb)[number][],
}
}
export const updateProject = async (project: (typeof projectsDb)[number]) => {
await sleep(1000)
const index = projectsDb.findIndex((p) => p.id === project.id)
projectsDb[index] = project
return project
}
export const removeProject = async (project: (typeof projectsDb)[number]) => {
await sleep(1000)
const index = projectsDb.findIndex((p) => p.id === project.id)
projectsDb.splice(index, 1)
return project
}

View File

@ -0,0 +1,292 @@
[
{
"id": 1,
"fullname": "Patrik Radkow",
"email": "magicpan@example.gg",
"username": "magicpan",
"role": "user",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 2,
"fullname": "Martin Hoff",
"email": "niceadmin@mail.com",
"username": "admin",
"role": "admin",
"avatar": "😍",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 3,
"fullname": "Liz Macintosh",
"email": "ebrown@gmail.com",
"username": "ebrown",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/1.jpg",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 4,
"fullname": "M2",
"email": "mrm@gmail.com",
"username": "mrm",
"role": "owner",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 5,
"fullname": "Kevin Smith",
"email": "kevin@gmail.com",
"username": "kevin13",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/2.jpg",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 6,
"fullname": "Martin Hoff",
"email": "martin@gmail.com",
"username": "martin3",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/3.jpg",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 7,
"fullname": "John Doe",
"email": "john@mail.com",
"username": "john",
"role": "user",
"avatar": "",
"active": true,
"notes": ""
},
{
"id": 8,
"fullname": "Maksim Nedo",
"email": "maksim@epic.com",
"username": "maksim",
"role": "admin",
"avatar": "https://avatars.githubusercontent.com/u/23530004?v=4",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 9,
"fullname": "Dmitry Kuzmenko",
"email": "dd@pp.com",
"username": "dd",
"role": "user",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 10,
"fullname": "Rayan Gosling",
"email": "rayan@u.ua",
"username": "rayan",
"role": "user",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 11,
"active": true,
"fullname": "Laura Smith",
"email": "laura@example.gg",
"username": "bbb",
"role": "user",
"avatar": "",
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 12,
"active": true,
"fullname": "Ted Mosby",
"email": "tedmosby@mail.com",
"username": "gamer777",
"role": "user",
"avatar": "😭",
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 13,
"active": true,
"fullname": "Forrest Schmidt Jr.",
"email": "Willard23@gmail.com",
"username": "Clementine72",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/4.jpg",
"notes": "sed asperiores sed"
},
{
"id": 14,
"active": true,
"fullname": "Emilio Bruen",
"email": "Amya51@hotmail.com",
"username": "Madalyn_Brekke55",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/5.jpg",
"notes": "architecto amet deleniti"
},
{
"id": 15,
"active": false,
"fullname": "Jenny Heathcote",
"email": "Granville_Lebsack38@yahoo.com",
"role": "user",
"username": "Vivienne98",
"avatar": "https://randomuser.me/api/portraits/men/6.jpg",
"notes": "provident ipsam recusandae"
},
{
"id": 16,
"active": true,
"fullname": "Sonya Cummerata III",
"email": "Toni2@yahoo.com",
"role": "user",
"username": "Norwood79",
"avatar": "https://randomuser.me/api/portraits/men/7.jpg",
"notes": "aut quaerat totam"
},
{
"id": 17,
"active": true,
"fullname": "Ruben Mitchell",
"email": "Lisette41@yahoo.com",
"role": "user",
"username": "Dariana_Schulist",
"avatar": "https://randomuser.me/api/portraits/men/8.jpg",
"notes": "minima harum ut"
},
{
"id": 18,
"active": true,
"fullname": "Blake Hudson I",
"email": "Israel88@hotmail.com",
"role": "user",
"username": "Crystal.Brakus29",
"avatar": "https://randomuser.me/api/portraits/men/9.jpg",
"notes": "sint culpa voluptatem"
},
{
"id": 19,
"active": true,
"fullname": "Alison Mueller",
"email": "Darien_Mayer@gmail.com",
"role": "user",
"username": "Cordie.Grant",
"avatar": "https://randomuser.me/api/portraits/men/10.jpg",
"notes": "officia autem aliquam"
},
{
"id": 20,
"active": false,
"fullname": "Miss Angelina Jenkins",
"email": "Cristal.Sauer@yahoo.com",
"role": "user",
"username": "Peggie.Runolfsdottir",
"avatar": "https://randomuser.me/api/portraits/men/11.jpg",
"notes": "rerum rerum rerum"
},
{
"id": 21,
"active": true,
"fullname": "Mack Boyle",
"email": "Shanny30@gmail.com",
"role": "user",
"username": "Phoebe67",
"avatar": "https://randomuser.me/api/portraits/men/12.jpg",
"notes": "voluptatibus et soluta"
},
{
"id": 22,
"active": true,
"fullname": "Raymond Simonis",
"email": "Tressie.Bruen45@gmail.com",
"role": "user",
"username": "Percy37",
"avatar": "https://randomuser.me/api/portraits/men/13.jpg",
"notes": "aut id molestiae"
},
{
"id": 23,
"active": true,
"fullname": "Janice Sporer",
"email": "Anastasia85@hotmail.com",
"role": "user",
"username": "Kali84",
"avatar": "https://randomuser.me/api/portraits/men/14.jpg",
"notes": "magnam eum aliquam"
},
{
"id": 24,
"active": true,
"fullname": "Francis Schowalter",
"email": "Tess56@gmail.com",
"role": "user",
"username": "Robyn.Kris",
"avatar": "https://randomuser.me/api/portraits/men/0.jpg",
"notes": "similique architecto in"
},
{
"id": 25,
"active": true,
"fullname": "Emilio Hoppe",
"email": "Bruce49@yahoo.com",
"role": "user",
"username": "Clemmie.Kutch",
"avatar": "https://randomuser.me/api/portraits/men/16.jpg",
"notes": "rerum quae dolorem"
},
{
"id": 26,
"active": true,
"fullname": "Janice Harber",
"email": "Jude38@hotmail.com",
"role": "user",
"username": "Neal70",
"avatar": "https://randomuser.me/api/portraits/men/17.jpg",
"notes": "iure dolor provident"
},
{
"id": 27,
"fullname": "Evelyn Morar",
"email": "Laverne.Roberts@hotmail.com",
"role": "user",
"username": "Neal_Thompson84",
"active": true,
"avatar": "https://randomuser.me/api/portraits/men/18.jpg",
"notes": "quae eos placeat"
},
{
"id": 28,
"fullname": "Antoinette Schneider",
"email": "Ambrose_Stehr25@gmail.com",
"role": "user",
"username": "Esta.Hickle",
"active": true,
"avatar": "https://randomuser.me/api/portraits/men/19.jpg",
"notes": "qui cumque unde"
},
{
"id": 29,
"fullname": "Daniel Ebony",
"email": "Nyah44@hotmail.com",
"role": "user",
"username": "Jade.Kuhlman90",
"active": true,
"avatar": "https://randomuser.me/api/portraits/men/20.jpg",
"notes": "exercitationem velit consectetur"
}
]

101
src/data/pages/users.ts Normal file
View File

@ -0,0 +1,101 @@
import { sleep } from '../../services/utils'
import { User } from './../../pages/users/types'
import usersDb from './users-db.json'
import projectsDb from './projects-db.json'
import { Project } from '../../pages/projects/types'
export const users = usersDb as User[]
const getUserProjects = (userId: number | string) => {
return projectsDb
.filter((project) => project.team.includes(Number(userId)))
.map((project) => ({
...project,
project_owner: users.find((user) => user.id === project.project_owner)!,
team: project.team.map((userId) => users.find((user) => user.id === userId)!),
status: project.status as Project['status'],
}))
}
// Simulate API calls
export type Pagination = {
page: number
perPage: number
total: number
}
export type Sorting = {
sortBy: keyof User | undefined
sortingOrder: 'asc' | 'desc' | null
}
export type Filters = {
isActive: boolean
search: string
}
const getSortItem = (obj: any, sortBy: string) => {
if (sortBy === 'projects') {
return obj.projects.map((project: any) => project.project_name).join(', ')
}
return obj[sortBy]
}
export const getUsers = async (filters: Partial<Filters & Pagination & Sorting>) => {
await sleep(1000)
const { isActive, search, sortBy, sortingOrder } = filters
let filteredUsers = users
filteredUsers = filteredUsers.filter((user) => user.active === isActive)
if (search) {
filteredUsers = filteredUsers.filter((user) => user.fullname.toLowerCase().includes(search.toLowerCase()))
}
filteredUsers = filteredUsers.map((user) => ({ ...user, projects: getUserProjects(user.id) }))
if (sortBy && sortingOrder) {
filteredUsers = filteredUsers.sort((a, b) => {
const first = getSortItem(a, sortBy)
const second = getSortItem(b, sortBy)
if (first > second) {
return sortingOrder === 'asc' ? 1 : -1
}
if (first < second) {
return sortingOrder === 'asc' ? -1 : 1
}
return 0
})
}
const { page = 1, perPage = 10 } = filters || {}
return {
data: filteredUsers.slice((page - 1) * perPage, page * perPage),
pagination: {
page,
perPage,
total: filteredUsers.length,
},
}
}
export const addUser = async (user: User) => {
await sleep(1000)
users.unshift(user)
}
export const updateUser = async (user: User) => {
await sleep(1000)
const index = users.findIndex((u) => u.id === user.id)
users[index] = user
}
export const removeUser = async (user: User) => {
await sleep(1000)
users.splice(
users.findIndex((u) => u.id === user.id),
1,
)
}

13
src/data/types.ts Normal file
View File

@ -0,0 +1,13 @@
import type { ChartData } from 'chart.js'
export type ColorThemes = {
[key: string]: string
}
export type TLineChartData = ChartData<'line', any, any>
export type TBarChartData = ChartData<'bar', any, any>
export type TBubbleChartData = ChartData<'bubble', any, any>
export type TDoughnutChartData = ChartData<'doughnut', any, any>
export type TPieChartData = ChartData<'pie', any, any>
export type TChartData = TLineChartData | TBarChartData | TBubbleChartData | TDoughnutChartData | TPieChartData

386
src/data/users.json Normal file
View File

@ -0,0 +1,386 @@
[
{
"id": "5d2c865e9a0bae79a6ef7cfa",
"firstName": "Ashley",
"lastName": "Mcdaniel",
"fullName": "Ashley Mcdaniel",
"email": "ashleymcdaniel@nebulean.com",
"country": "Cayman Islands",
"starred": true,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865ec73341e16e5f2251",
"firstName": "Sellers",
"lastName": "Todd",
"fullName": "Todd Sellers",
"email": "sellerstodd@nebulean.com",
"country": "Togo",
"starred": false,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "none",
"color": "primary",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e38800c5ce28f2f6b",
"firstName": "Sherman",
"lastName": "Knowles",
"fullName": "Sherman Knowles",
"email": "shermanknowles@nebulean.com",
"country": "Central African Republic",
"starred": true,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "none",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e957cd150b82e17a6",
"firstName": "Vasquez",
"lastName": "Lawson",
"fullName": "Vasquez Lawson",
"email": "vasquezlawson@nebulean.com",
"country": "Bouvet Island",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "down",
"color": "warning",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e9194dbe2faf99227",
"firstName": "April",
"lastName": "Sykes",
"fullName": "April Sykes",
"email": "aprilsykes@nebulean.com",
"country": "Saint Vincent and The Grenadines",
"starred": false,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "down",
"color": "primary",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e1ed74d83f6b26934",
"firstName": "Hodges",
"lastName": "Garrison",
"fullName": "Hodges Garrison",
"email": "hodgesgarrison@nebulean.com",
"country": "Zimbabwe",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "none",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e0ef31380880c3de5",
"firstName": "Therese",
"lastName": "Stokes",
"fullName": "Therese Stokes",
"email": "theresestokes@nebulean.com",
"country": "Mali",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e4b5ab4727e5c8b69",
"firstName": "Goodwin",
"lastName": "Brewer",
"fullName": "Goodwin Brewer",
"email": "goodwinbrewer@nebulean.com",
"country": "Iraq",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "none",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e4c4d675787cfe1c0",
"firstName": "Gomez",
"lastName": "Wise",
"fullName": "Gomez Wise",
"email": "gomezwise@nebulean.com",
"country": "Portugal",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "none",
"color": "primary",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e1017c3229017fc68",
"firstName": "Laverne",
"lastName": "Ayers",
"fullName": "Laverne Ayers",
"email": "laverneayers@nebulean.com",
"country": "Micronesia",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865ee66676fd7464f8b9",
"firstName": "Stewart",
"lastName": "Leon",
"fullName": "Stewart Leon",
"email": "stewartleon@nebulean.com",
"country": "Seychelles",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e644d8acbed1e0e97",
"firstName": "Lindsey",
"lastName": "Hopkins",
"fullName": "Lindsey Hopkins",
"email": "lindseyhopkins@nebulean.com",
"country": "Costa Rica",
"starred": false,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "up",
"color": "primary",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865ef2b732c74dc3d6a2",
"firstName": "Head",
"lastName": "Lloyd",
"fullName": "Head Lloyd",
"email": "headlloyd@nebulean.com",
"country": "Turkey",
"starred": true,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e4ee4f09e92ead2e7",
"firstName": "Fisher",
"lastName": "Bradford",
"fullName": "Fisher Bradford",
"email": "fisherbradford@nebulean.com",
"country": "Ethiopia",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e88d46a9e9049a549",
"firstName": "Aurora",
"lastName": "Bird",
"fullName": "Aurora Bird",
"email": "aurorabird@nebulean.com",
"country": "Burkina Faso",
"starred": false,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e44bf14ea96d6e752",
"firstName": "Bonita",
"lastName": "Shields",
"fullName": "Bonita Shields",
"email": "bonitashields@nebulean.com",
"country": "Cote D'Ivoire (Ivory Coast)",
"starred": true,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "down",
"color": "primary",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e2a8be26f6ac4369c",
"firstName": "Ethel",
"lastName": "Underwood",
"fullName": "Ethel Underwood",
"email": "ethelunderwood@nebulean.com",
"country": "Vanuatu",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e5e0aea40111c37f8",
"firstName": "Parker",
"lastName": "May",
"fullName": "Parker May",
"email": "parkermay@nebulean.com",
"country": "Pakistan",
"starred": true,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e7e0c05ecc2d0c186",
"firstName": "Hillary",
"lastName": "Waters",
"fullName": "Hillary Waters",
"email": "hillarywaters@nebulean.com",
"country": "Comoros",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "down",
"color": "primary",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e80a72eeda016b169",
"firstName": "Raquel",
"lastName": "Ferrell",
"fullName": "Raquel Ferrell",
"email": "raquelferrell@nebulean.com",
"country": "China",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865eafacadd378add679",
"firstName": "Pickett",
"lastName": "Page",
"fullName": "Pickett Page",
"email": "pickettpage@nebulean.com",
"country": "Bermuda",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e772b1a75bb0a07b5",
"firstName": "Alyson",
"lastName": "Bailey",
"fullName": "Alyson Bailey",
"email": "alysonbailey@nebulean.com",
"country": "United Arab Emirates",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "up",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e137c19a76b56210c",
"firstName": "Farley",
"lastName": "Meyers",
"fullName": "Farley Meyers",
"email": "farleymeyers@nebulean.com",
"country": "Christmas Island",
"starred": false,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "warning",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865eb0ba37a27aa9afe0",
"firstName": "Hinton",
"lastName": "Avery",
"fullName": "Hinton Avery",
"email": "hintonavery@nebulean.com",
"country": "Liechtenstein",
"starred": false,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
}
]

1
src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
src/i18n/index.ts Normal file
View File

@ -0,0 +1,25 @@
import { createI18n } from 'vue-i18n'
const fileNameToLocaleModuleDict = import.meta.glob<{ default: Record<string, string> }>('./locales/*.json', {
eager: true,
})
const messages: { [P: string]: Record<string, string> } = {}
Object.entries(fileNameToLocaleModuleDict)
.map(([fileName, localeModule]) => {
const fileNameParts = fileName.split('/')
const fileNameWithoutPath = fileNameParts[fileNameParts.length - 1]
const localeName = fileNameWithoutPath.split('.json')[0]
return [localeName, localeModule.default] as const
})
.forEach((localeNameLocaleMessagesTuple) => {
messages[localeNameLocaleMessagesTuple[0]] = localeNameLocaleMessagesTuple[1]
})
export default createI18n({
legacy: false,
locale: 'ru',
fallbackLocale: 'ru',
messages,
})

151
src/i18n/locales/br.json Normal file
View File

@ -0,0 +1,151 @@
{
"auth": {
"agree": "Eu aceito.",
"createAccount": "Criar conta",
"createNewAccount": "Criar uma nova conta",
"email": "Email",
"login": "Entrar",
"password": "Senha",
"recover_password": "Recuperar senha",
"sign_up": "Cadastrar-se",
"keep_logged_in": "Mantenha-me conectado",
"termsOfUse": "Termos de uso.",
"reset_password": "Redefinir senha"
},
"404": {
"title": "Esta página está pescando.",
"text": "Se você acha que isso não está certo, envie-nos uma mensagem em ",
"back_button": "Voltar para o painel"
},
"typography": {
"primary": "Estilos de texto primários",
"secondary": "Estilos de texto secundários"
},
"dashboard": {
"versions": "Versões",
"setupRemoteConnections": "Configurar Conexões Remotas",
"currentVisitors": "Visitantes Atuais",
"charts": {
"trendyTrends": "Tendências na moda",
"showInMoreDetail": "Mostrar em mais detalhes",
"showInLessDetail": "Mostrar em menos detalhes",
"loadingSpeed": "Velocidade de carregamento",
"topContributors": "Principais colaboradores",
"showNextFive": "Mostrar próximos cinco",
"commits": "Commits"
},
"info": {
"componentRichTheme": "tema rico em componentes",
"completedPullRequests": "solicitações de pull concluídas",
"users": "usuários",
"points": "pontos",
"units": "unidades",
"exploreGallery": "Explorar galeria",
"viewLibrary": "Ver Biblioteca",
"commits": "commits",
"components": "componentes",
"teamMembers": "membros da equipe"
},
"tabs": {
"overview": {
"title": "Visão Geral",
"built": "Construído com o framework Vue.js",
"free": "Totalmente gratuito para todos",
"fresh": "Design fresco e nítido",
"mobile": "Responsivo e otimizado para dispositivos móveis",
"components": "Toneladas de componentes úteis",
"nojQuery": "Completamente livre de jQuery"
},
"billingAddress": {
"title": "Endereço de Cobrança",
"personalInfo": "Informação Pessoal",
"firstName": "Nome e Sobrenome",
"email": "Email",
"address": "Endereço",
"companyInfo": "Informação da Empresa",
"city": "Cidade",
"country": "País",
"infiniteConnections": "Conexões infinitas",
"addConnection": "Adicionar Conexão"
},
"bankDetails": {
"title": "Detalhes Bancários",
"detailsFields": "Campos de Detalhes",
"bankName": "Nome do Banco",
"accountName": "Nome da Conta",
"sortCode": "Código de Ordenação",
"accountNumber": "Número da Conta",
"notes": "Notas",
"sendDetails": "Enviar Detalhes"
}
},
"navigationLayout": "layout de navegação",
"topBarButton": "Barra Superior",
"sideBarButton": "Barra Lateral"
},
"language": {
"brazilian_portuguese": "Português",
"english": "Inglês",
"spanish": "Espanhol",
"simplified_chinese": "Chinês Simplificado",
"persian": "Persa"
},
"menu": {
"auth": "Autenticação",
"buttons": "Botões",
"timelines": "Linhas do Tempo",
"dashboard": "Painel de Controle",
"billing": "Cobrança",
"login": "Entrar",
"signUp": "Cadastrar-se",
"preferences": "Preferências",
"payments": "Pagamentos",
"pricing-plans": "Planos de Preços",
"login-singup": "Entrar/Cadastrar",
"404": "Páginas 404",
"faq": "FAQ"
},
"messages": {
"all": "Ver todas as mensagens",
"new": "Novas mensagens de {name}",
"mark_as_read": "Marcar como Lido"
},
"navbar": {
"messageUs": "Perguntas de desenvolvimento web:",
"repository": "Repositório GitHub"
},
"notifications": {
"all": "Ver todas as notificações",
"mark_as_read": "Marcar como lida",
"sentMessage": "enviou uma mensagem",
"uploadedZip": "fez upload de um novo arquivo Zip com {type}",
"startedTopic": "iniciou um novo tópico"
},
"user": {
"language": "Mudar Idioma",
"logout": "Sair",
"profile": "Meu Perfil",
"settings": "Configurações",
"billing": "Faturamento",
"faq": "FAQ",
"helpAndSupport": "Ajuda & Suporte",
"projects": "Projetos",
"account": "Conta",
"explore": "Explorar"
},
"treeView": {
"basic": "Básico",
"icons": "Icones",
"selectable": "Selecionável",
"editable": "Editável",
"advanced": "Advançado"
},
"chat": {
"title": "Gráficos"
},
"helpAndSupport": "Ajuda & Suporte",
"aboutVuesticAdmin": "Sobre Vuestic Admin",
"search": {
"placeholder": "Buscar..."
}
}

184
src/i18n/locales/cn.json Normal file
View File

@ -0,0 +1,184 @@
{
"auth": {
"agree": "我同意",
"createAccount": "创建账号",
"createNewAccount": "创建新账号",
"email": "电子邮箱",
"login": "登录",
"password": "密码",
"recover_password": "恢复密码",
"sign_up": "注册",
"keep_logged_in": "保持登录",
"termsOfUse": "使用条款",
"reset_password": "重置密码"
},
"404": {
"title": "此页面已去钓鱼",
"text": "如果您觉得这不对,请给我们发送消息",
"back_button": "返回仪表板"
},
"typography": {
"primary": "主要文本样式",
"secondary": "次要文本样式"
},
"dashboard": {
"versions": "版本",
"setupRemoteConnections": "设置远程连接",
"currentVisitors": "当前访问者",
"charts": {
"trendyTrends": "流行趋势",
"showInMoreDetail": "显示更多细节",
"showInLessDetail": "显示较少细节",
"loadingSpeed": "加载速度",
"topContributors": "主要贡献者",
"showNextFive": "显示接下来的五个",
"commits": "提交"
},
"info": {
"componentRichTheme": "组件丰富的主题",
"completedPullRequests": "已完成的拉取请求",
"users": "用户",
"points": "点数",
"units": "单位",
"exploreGallery": "探索画廊",
"viewLibrary": "查看库",
"commits": "提交",
"components": "组件",
"teamMembers": "团队成员"
},
"tabs": {
"overview": {
"title": "概述",
"built": "使用 Vue.js 框架构建",
"free": "对所有人完全免费",
"fresh": "新鲜和清新的设计",
"mobile": "响应式且优化移动",
"components": "大量有用的组件",
"nojQuery": "完全不使用 jQuery"
},
"billingAddress": {
"title": "账单地址",
"personalInfo": "个人信息",
"firstName": "名字 & 姓氏",
"email": "电子邮箱",
"address": "地址",
"companyInfo": "公司信息",
"city": "城市",
"country": "国家",
"infiniteConnections": "无限连接",
"addConnection": "添加连接"
},
"bankDetails": {
"title": "银行详情",
"detailsFields": "详情字段",
"bankName": "银行名称",
"accountName": "账户名称",
"sortCode": "排序代码",
"accountNumber": "账号",
"notes": "备注",
"sendDetails": "发送详情"
}
},
"navigationLayout": "导航布局",
"topBarButton": "顶部按钮",
"sideBarButton": "侧边按钮"
},
"language": {
"brazilian_portuguese": "葡萄牙语",
"english": "英语",
"spanish": "西班牙语",
"simplified_chinese": "简体中文",
"persian": "波斯语"
},
"menu": {
"auth": "授权",
"buttons": "按钮",
"timelines": "时间线",
"dashboard": "仪表板",
"billing": "计费",
"login": "登录",
"signUp": "注册",
"preferences": "偏好",
"payments": "支付",
"pricing-plans": "定价计划",
"login-singup": "登录/注册",
"404": "404 页面",
"faq": "常见问题解答"
},
"messages": {
"all": "查看所有消息",
"new": "来自 {name} 的新消息",
"mark_as_read": "标记为已读"
},
"navbar": {
"messageUs": "需要Web开发帮助吗请联系我们。",
"repository": "GitHub 仓库"
},
"notifications": {
"all": "查看所有通知",
"mark_as_read": "标为已读",
"sentMessage": "{name} 给你发了一条消息",
"uploadedZip": "{name} 上传了一个新的 Zip 文件 {type}",
"startedTopic": "{name} 开始了一个新话题"
},
"user": {
"language": "修改语言",
"logout": "登出",
"logout": "登出",
"profile": "我的资料",
"settings": "设置",
"billing": "账单",
"faq": "常见问题",
"helpAndSupport": "帮助与支持",
"projects": "项目",
"account": "账户",
"explore": "探索"
},
"treeView": {
"basic": "基本型",
"icons": "图标",
"selectable": "可选择",
"editable": "可编辑",
"advanced": "高级"
},
"chat": {
"title": "聊天"
},
"cards": {
"cards": "卡片",
"fixed": "固定的",
"floating": "浮动的",
"contentText": "独特的斑马条纹使它们成为人们最熟悉的动物之一。",
"rowHeight": "行高",
"title": {
"dark": "暗色背景",
"bright": "亮色卡片",
"titleOnImageNoOverlay": "图像上的标题,但没有叠加",
"normal": "标准卡",
"overlayAndTextOnImage": "图像上有覆盖和文本的卡片",
"stripeNoImage": "无图像条纹卡"
},
"button": {
"main": "主要",
"cancel": "取消"
},
"link": {
"edit": "编辑",
"setAsDefault": "设为默认",
"delete": "删除",
"traveling": "Traveling",
"france": "法国",
"review": "评论",
"feedback": "反馈信息",
"readFull": "阅读全文",
"secondaryAction": "第二行为",
"action1": "行为 1",
"action2": "行为 2"
}
},
"helpAndSupport": "帮助与支持",
"aboutVuesticAdmin": "关于 Vuestic Admin",
"search": {
"placeholder": "搜索..."
}
}

140
src/i18n/locales/es.json Normal file
View File

@ -0,0 +1,140 @@
{
"auth": {
"agree": "Acepto",
"createAccount": "Crear cuenta",
"createNewAccount": "Crear cuenta nueva",
"email": "Email",
"login": "Iniciar sesión",
"password": "Contraseña",
"recover_password": "Recuperar contraseña",
"sign_up": "Registrar",
"keep_logged_in": "Mantenerme conectado",
"termsOfUse": "Términos de uso",
"reset_password": "Restablecer contraseña"
},
"404": {
"title": "Esta página se ha ido a pescar",
"text": "Si crees que esto no es correcto, por favor envíanos un mensaje a ",
"back_button": "Volver al tablero"
},
"typography": {
"primary": "Estilos de texto primarios",
"secondary": "Estilos de texto secundarios"
},
"dashboard": {
"versions": "Versiones",
"setupRemoteConnections": "Configurar conexiones remotas",
"currentVisitors": "Visitantes actuales",
"charts": {
"trendyTrends": "Tendencias modernas",
"showInMoreDetail": "Mostrar en más detalle",
"showInLessDetail": "Mostrar en menos detalle",
"loadingSpeed": "Velocidad de carga",
"topContributors": "Principales contribuyentes",
"showNextFive": "Mostrar los siguientes cinco",
"commits": "Commit"
},
"info": {
"componentRichTheme": "Tema rico en componentes",
"completedPullRequests": "Solicitudes de pull completadas",
"users": "Usuarios",
"points": "Puntos",
"units": "Unidades",
"exploreGallery": "Explorar galería",
"viewLibrary": "Ver biblioteca",
"commits": "Commits",
"components": "Componentes",
"teamMembers": "Miembros del equipo"
},
"tabs": {
"overview": {
"title": "Visión general",
"built": "Construido con el framework Vue.js",
"free": "Totalmente gratuito para todos",
"fresh": "Diseño fresco y nítido",
"mobile": "Responsivo y optimizado para móviles",
"components": "Toneladas de componentes útiles",
"nojQuery": "Totalmente libre de jQuery"
},
"billingAddress": {
"title": "Dirección de facturación",
"personalInfo": "Información personal",
"firstName": "Nombre y apellido",
"email": "Email",
"address": "Dirección",
"companyInfo": "Información de la empresa",
"city": "Ciudad",
"country": "País",
"infiniteConnections": "Conexiones infinitas",
"addConnection": "Añadir conexión"
},
"bankDetails": {
"title": "Detalles del banco",
"detailsFields": "Campos de detalles",
"bankName": "Nombre del banco",
"accountName": "Nombre de la cuenta",
"sortCode": "Código de ordenación",
"accountNumber": "Número de cuenta",
"notes": "Notas",
"sendDetails": "Enviar detalles"
}
},
"navigationLayout": "Diseño de navegación",
"topBarButton": "Barra superior",
"sideBarButton": "Barra lateral"
},
"language": {
"brazilian_portuguese": "Portugués brasileño",
"english": "Inglés",
"spanish": "Español",
"simplified_chinese": "Chino simplificado",
"persian": "Persa"
},
"menu": {
"auth": "Autenticación",
"buttons": "Botones",
"timelines": "Líneas de tiempo",
"dashboard": "Tablero",
"billing": "Facturación",
"login": "Iniciar sesión",
"preferences": "Preferencias",
"payments": "Pagos",
"pricing-plans": "Planes de precios",
"login-singup": "Iniciar sesión/Registrarse",
"404": "Páginas 404",
"faq": "FAQ"
},
"messages": {
"all": "Ver todos los mensajes",
"new": "Nuevos mensajes de {name}",
"mark_as_read": "Marcar como leído"
},
"navbar": {
"messageUs": "Preguntas sobre desarrollo web:",
"repository": "Repositorio GitHub"
},
"notifications": {
"all": "Ver todas las notificaciones",
"mark_as_read": "Marcar como leída",
"sentMessage": "{name} te envió un mensaje",
"uploadedZip": "{name} subió un archivo Zip con {type}",
"startedTopic": "{name} inició un nuevo tema"
},
"user": {
"language": "Cambiar Idioma",
"logout": "Cerrar sesión",
"profile": "Mi Perfil",
"settings": "Configuración",
"billing": "Facturación",
"faq": "Preguntas Frecuentes",
"helpAndSupport": "Ayuda & Soporte",
"projects": "Proyectos",
"account": "Cuenta",
"explore": "Explorar"
},
"helpAndSupport": "Ayuda y Soporte",
"aboutVuesticAdmin": "Acerca de Vuestic Admin",
"search": {
"placeholder": "Buscar..."
}
}

177
src/i18n/locales/gb.json Normal file
View File

@ -0,0 +1,177 @@
{
"auth": {
"agree": "I agree to",
"createAccount": "Create account",
"createNewAccount": "Create New Account",
"email": "Email",
"login": "Login",
"password": "Password",
"recover_password": "Recover password",
"sign_up": "Sign Up",
"keep_logged_in": "Keep me logged in",
"termsOfUse": "Terms of Use.",
"reset_password": "Reset password"
},
"404": {
"title": "This pages gone fishing.",
"text": "If you feel that its not right, please send us a message at ",
"back_button": "Back to dashboard"
},
"typography": {
"primary": "Primary text styles",
"secondary": "Secondary text styles"
},
"dashboard": {
"versions": "Versions",
"setupRemoteConnections": "Setup Remote Connections",
"currentVisitors": "Current Visitors",
"navigationLayout": "navigation layout",
"topBarButton": "Top Bar",
"sideBarButton": "Side Bar"
},
"language": {
"brazilian_portuguese": "Português",
"english": "English",
"spanish": "Spanish",
"simplified_chinese": "Simplified Chinese",
"persian": "Persian"
},
"menu": {
"auth": "Auth",
"buttons": "Buttons",
"timelines": "Timelines",
"dashboard": "Dashboard",
"billing": "Billing",
"login": "Login",
"preferences": "Account preferences",
"payments": "Payments",
"settings": "Application settings",
"pricing-plans": "Pricing plans",
"payment-methods": "Payment methods",
"signup": "Signup",
"recover-password": "Recover password",
"recover-password-email": "Recover password email",
"404": "404",
"faq": "FAQ",
"users": "Users",
"projects": "Projects"
},
"messages": {
"all": "See all messages",
"new": "New messages from {name}",
"mark_as_read": "Mark As Read"
},
"navbar": {
"messageUs": "Web development inquiries:",
"repository": "GitHub Repo"
},
"notifications": {
"all": "See all notifications",
"less": "See less notifications",
"mark_as_read": "Mark as read",
"sentMessage": "sent you a message",
"uploadedZip": "uploaded a new Zip file with {type}",
"startedTopic": "started a new topic"
},
"user": {
"language": "Change language",
"logout": "Logout",
"profile": "Profile",
"settings": "Settings",
"billing": "Billing",
"faq": "FAQ",
"helpAndSupport": "Help & support",
"projects": "Projects",
"account": "Account",
"explore": "Explore"
},
"treeView": {
"basic": "Basic",
"icons": "Icons",
"selectable": "Selectable",
"editable": "Editable",
"advanced": "Advanced"
},
"chat": {
"title": "Chat",
"sendButton": "Send"
},
"spacingPlayground": {
"value": "Value",
"margin": "Margin",
"padding": "Padding"
},
"spacing": {
"title": "Spacing"
},
"cards": {
"cards": "Cards",
"fixed": "Fixed",
"floating": "Floating",
"contentText": "The unique stripes of zebras make them one of the animals most familiar to people.",
"contentTextLong": "The unique stripes of zebras make them one of the animals most familiar to people. They occur in a variety of habitats, such as grasslands, savannas, woodlands, thorny scrublands, mountains, and coastal hills. Various anthropogenic factors have had a severe impact on zebra populations, in particular hunting for skins and habitat destruction. Grévy's zebra and the mountain zebra are endangered. While plains zebras are much more plentiful, one subspecies, the quagga.",
"rowHeight": "Row height",
"title": {
"default": "Default",
"withControls": "With controls",
"customHeader": "Custom header",
"withoutHeader": "Without header",
"withImage": "With Image",
"withTitleOnImage": "With title on image",
"withCustomTitleOnImage": "With custom title on image",
"withStripe": "With stripe",
"withBackground": "With background"
},
"button": {
"main": "Main",
"cancel": "Cancel",
"showMore": "Show More",
"readMore": "Show More"
},
"link": {
"edit": "Edit",
"setAsDefault": "Set as default",
"delete": "Delete",
"traveling": "Traveling",
"france": "France",
"review": "Review",
"feedback": "Leave feedback",
"readFull": "Read full article",
"secondaryAction": "Secondary action",
"action1": "Action 1",
"action2": "Action 2"
}
},
"colors": {
"themeColors": "Theme Colors",
"extraColors": "Extra Colors",
"gradients": {
"basic": {
"title": "Button Gradients"
},
"hovered": {
"title": "Hovered Button Gradients",
"text": "Lighten 15% applied to an original style (gradient or flat color) for hover state."
},
"pressed": {
"title": "Pressed Button Gradients",
"text": "Darken 15% applied to an original style (gradient or flat color) for pressed state."
}
}
},
"tabs": {
"alignment": "Tabs alignment",
"overflow": "Tabs overflow",
"hidden": "Tabs with hidden slider",
"grow": "Tabs grow"
},
"helpAndSupport": "Help & support",
"aboutVuesticAdmin": "About Vuestic Admin",
"search": {
"placeholder": "Search..."
},
"buttonSelect": {
"dark": "Dark",
"light": "Light"
}
}

192
src/i18n/locales/ir.json Normal file
View File

@ -0,0 +1,192 @@
{
"auth": {
"agree": "با شرایط استفاده موافقم.",
"createAccount": "ساخت حساب کاربری",
"createNewAccount": "ساخت حساب کاربری تازه",
"email": "رایانامه",
"login": "ورود",
"password": "گذرواژه",
"recover_password": "فراموشی رمز عبور",
"sign_up": "ساخت حساب کاربری",
"keep_logged_in": "مرا در این مرورگر بخاطر بسپار",
"termsOfUse": "شرایط استفاده",
"reset_password": "بازنشانی گذرواژه"
},
"404": {
"title": "این صفحه رفته گل بچینه :)",
"text": "اگر فکر میکنید چیزی درست نیست برای ما پیام بفرستید.",
"back_button": "بازگشت به پیشخوان"
},
"typography": {
"primary": "سبک های متن اصلی",
"secondary": "سبکه های متن ثانوی"
},
"dashboard": {
"versions": "ورژن ها",
"setupRemoteConnections": "راه اندازی اتصالات از راه دور",
"currentVisitors": "بازکنندگان فعلی",
"charts": {
"trendyTrends": "ترند های روز",
"showInMoreDetail": "نمایش جزییات بیشتر",
"showInLessDetail": "نمایش جزییات کمتر",
"loadingSpeed": "سرعت بارگزاری",
"topContributors": "مشارکت کنندگان برتر",
"showNextFive": "نمایش پنج تای بعدی",
"commits": "کامیت ها"
},
"info": {
"componentRichTheme": "تم غنی از کامپوننت",
"completedPullRequests": "درخواست های کشیدن کامل شده",
"users": "کاربران",
"points": "امتیازات",
"units": "واحد ها",
"exploreGallery": "گشت و گذار در گالری",
"viewLibrary": "مشاهده کتابخانه",
"commits": "کامیت ها",
"components": "کامپوننت ها",
"teamMembers": "اعضای تیم"
},
"tabs": {
"overview": {
"title": "نمای کلی",
"built": "ساخته شده با فریم ورک Vue.js",
"free": "کاملا رایگان برای همه",
"fresh": "طراحی تازه و باحال",
"mobile": "واکنش گرا و بهینه سازی برای موبایل",
"components": "پر از مولفه های باحال",
"nojQuery": "کاملا بدون jQuery"
},
"billingAddress": {
"title": "نشانی صورتحساب",
"personalInfo": "اطلاعات شخصی",
"firstName": "نام و نام خانوادگی",
"email": "رایانما",
"address": "نشانی",
"companyInfo": "اطلاعات شرکت",
"city": "شهر",
"country": "کشور",
"infiniteConnections": "اتصالات نامحدود",
"addConnection": "افزودن اتصال"
},
"bankDetails": {
"title": "اطلاعات بانکی",
"detailsFields": "فیلد های جزئیات",
"bankName": "نام بانک",
"accountName": "نام حساب",
"sortCode": "کد مرتب سازی",
"accountNumber": "شماره حساب",
"notes": "یادداشت ها",
"sendDetails": "ارسال جزئیات"
}
},
"navigationLayout": "چیدمان ناوبری",
"topBarButton": "دکمه نوار بالا",
"sideBarButton": "دکمه نوار کناری"
},
"language": {
"brazilian_portuguese": "پرتغالی برزیل",
"english": "انگلیسی",
"spanish": "اسپانیایی",
"simplified_chinese": "چینی ساده شده",
"persian": "فارسی"
},
"menu": {
"auth": "احراز هویت",
"buttons": "دکمه ها",
"timelines": "جدول زمانی",
"dashboard": "داشبورد",
"billing": "صورتحساب",
"login": "ورود",
"signUp": "ثبت نام",
"preferences": "ترجیحات",
"payments": "پرداخت ها",
"pricing-plans": "طرح های قیمت گذاری",
"login-singup": "ورود/ثبت نام",
"404": "صفحات 404",
"faq": "سوالات متداول"
},
"messages": {
"all": "مشاهده تمام پیام ها",
"new": "پیام های جدید از {name}",
"mark_as_read": "علامت زدن به عنوان خوانده شده"
},
"navbar": {
"messageUs": "پرسش های مربوط به توسعه وب:",
"repository": "مخزن گیت هاب"
},
"notifications": {
"all": "مشاهده تمام اعلان ها",
"mark_as_read": "علامت زدن به عنوان خوانده شده",
"sentMessage": "پیامی برای شما فرستاد",
"uploadedZip": "یک فایل زیپ جدید با {type} آپلود کرد",
"startedTopic": "یک موضوع جدید را شروع کرد"
},
"user": {
"language": "تغییر زبان",
"logout": "خروج",
"profile": "پروفایل من",
"settings": "تنظیمات",
"billing": "صورتحساب",
"faq": "سؤالات متداول",
"helpAndSupport": "کمک و پشتیبانی",
"projects": "پروژه ها",
"account": "حساب",
"explore": "کاوش"
},
"treeView": {
"basic": "اصلی",
"icons": "آيکون ها",
"selectable": "قابل انتخاب",
"editable": "قابل ویرایش",
"advanced": "پیشرفته"
},
"chat": {
"title": "گفتگو"
},
"spacingPlayground": {
"value": "مقدار",
"margin": "حاشیه",
"padding": "پدینگ"
},
"cards": {
"cards": "کارت ها",
"fixed": "ثابت",
"floating": "شناور",
"contentText": "از قیافت معلومه خیلی وقته روی صندلی نشستی. کپک نزنی! پاشو یه تکونی بده ",
"contentTextLong": "یک برنامه نویس حتما نباید سیگار دستش باشد تا جامعه متوجه بشود که او برنامه نویس است. مشخص نیست این تصور غلط از کجا و به چه شکل به وجود آمد اما اصلا درست نیست. برنامه نویس\u200Cهای بزرگ و موفق غیرسیگاری زیاد هستند. اشخاصی مثل بیل گیتس، ریچارد استالمن یا علی شریفی نیستانی که در دنیای برنامه نویسی کاملا شناخته شده می\u200Cباشند و سیگاری نیستند. در مقابل بعضی از برنامه نویس\u200Cها هم سیگار می\u200Cکشند و این موضوع کاملا به خود شخص برمی\u200Cگردد",
"rowHeight": "ارتفاع سطر",
"title": {
"default": "پیش فرض",
"withControls": "همراه کنترل",
"customHeader": "سربرگ سفارشی",
"withoutHeader": "بدون سربرگ",
"withImage": "همراه تصویر",
"withTitleOnImage": "همراه عنوان و بدون تصویر",
"withCustomTitleOnImage": "بدون تصویر یا عنوان",
"withStripe": "همراه نوار",
"withBackground": "همراه پس زمینه"
},
"button": {
"main": "اصلی",
"cancel": "لغو"
},
"link": {
"edit": "ویرایش",
"setAsDefault": "تنظیم به عنوان پیش فرض",
"delete": "حذف",
"traveling": "مسافرت",
"france": "فرانسیه",
"review": "بررسی",
"feedback": "ترک کردن بازخورد",
"readFull": "خواندن تمام مقالات",
"secondaryAction": "افدام دوم",
"action1": "اقدام یک",
"action2": "اقدام دو"
}
},
"helpAndSupport": "کمک و پشتیبانی",
"aboutVuesticAdmin": "درباره Vuestic Admin",
"search": {
"placeholder": "جستجو..."
}
}

178
src/i18n/locales/ru.json Normal file
View File

@ -0,0 +1,178 @@
{
"auth": {
"agree": "Я согласен",
"createAccount": "Создать аккаунт",
"createNewAccount": "Создать новый аккаунт",
"email": "Email",
"login": "Логин",
"password": "Пароль",
"recover_password": "Восстановить пароль",
"sign_up": "Регистрация",
"keep_logged_in": "Запомнить",
"termsOfUse": "Terms of Use.",
"reset_password": "Сбросить пароль"
},
"404": {
"title": "Эта страница последняя.",
"text": "Если вы счиате, что это ошибка, напишите нам ",
"back_button": "На главную"
},
"typography": {
"primary": "Primary text styles",
"secondary": "Secondary text styles"
},
"dashboard": {
"versions": "Versions",
"setupRemoteConnections": "Setup Remote Connections",
"currentVisitors": "Current Visitors",
"navigationLayout": "navigation layout",
"topBarButton": "Top Bar",
"sideBarButton": "Side Bar"
},
"language": {
"brazilian_portuguese": "Português",
"english": "English",
"spanish": "Spanish",
"simplified_chinese": "Simplified Chinese",
"persian": "Persian"
},
"menu": {
"auth": "Auth",
"buttons": "Buttons",
"timelines": "Timelines",
"dashboard": "Dashboard",
"billing": "Billing",
"login": "Login",
"preferences": "Account preferences",
"payments": "Payments",
"settings": "Application settings",
"pricing-plans": "Pricing plans",
"payment-methods": "Payment methods",
"signup": "Signup",
"recover-password": "Recover password",
"recover-password-email": "Recover password email",
"404": "404",
"faq": "FAQ",
"users": "Users",
"projects": "Projects"
},
"messages": {
"all": "See all messages",
"new": "New messages from {name}",
"mark_as_read": "Mark As Read"
},
"navbar": {
"messageUs": "Web development inquiries:",
"repository": "GitHub Repo"
},
"notifications": {
"all": "See all notifications",
"less": "See less notifications",
"mark_as_read": "Mark as read",
"sentMessage": "sent you a message",
"uploadedZip": "uploaded a new Zip file with {type}",
"startedTopic": "started a new topic"
},
"user": {
"language": "Change language",
"logout": "Logout",
"profile": "Profile",
"settings": "Settings",
"billing": "Billing",
"faq": "FAQ",
"helpAndSupport": "Help & support",
"projects": "Projects",
"account": "Account",
"explore": "Explore"
},
"treeView": {
"basic": "Basic",
"icons": "Icons",
"selectable": "Selectable",
"editable": "Editable",
"advanced": "Advanced"
},
"chat": {
"title": "Chat",
"sendButton": "Send"
},
"spacingPlayground": {
"value": "Value",
"margin": "Margin",
"padding": "Padding"
},
"spacing": {
"title": "Spacing"
},
"cards": {
"cards": "Cards",
"fixed": "Fixed",
"floating": "Floating",
"contentText": "The unique stripes of zebras make them one of the animals most familiar to people.",
"contentTextLong": "The unique stripes of zebras make them one of the animals most familiar to people. They occur in a variety of habitats, such as grasslands, savannas, woodlands, thorny scrublands, mountains, and coastal hills. Various anthropogenic factors have had a severe impact on zebra populations, in particular hunting for skins and habitat destruction. Grévy's zebra and the mountain zebra are endangered. While plains zebras are much more plentiful, one subspecies, the quagga.",
"rowHeight": "Row height",
"title": {
"default": "Default",
"withControls": "With controls",
"customHeader": "Custom header",
"withoutHeader": "Without header",
"withImage": "With Image",
"withTitleOnImage": "With title on image",
"withCustomTitleOnImage": "With custom title on image",
"withStripe": "With stripe",
"withBackground": "With background"
},
"button": {
"main": "Main",
"cancel": "Cancel",
"showMore": "Show More",
"readMore": "Show More"
},
"link": {
"edit": "Edit",
"setAsDefault": "Set as default",
"delete": "Delete",
"traveling": "Traveling",
"france": "France",
"review": "Review",
"feedback": "Leave feedback",
"readFull": "Read full article",
"secondaryAction": "Secondary action",
"action1": "Action 1",
"action2": "Action 2"
}
},
"colors": {
"themeColors": "Theme Colors",
"extraColors": "Extra Colors",
"gradients": {
"basic": {
"title": "Button Gradients"
},
"hovered": {
"title": "Hovered Button Gradients",
"text": "Lighten 15% applied to an original style (gradient or flat color) for hover state."
},
"pressed": {
"title": "Pressed Button Gradients",
"text": "Darken 15% applied to an original style (gradient or flat color) for pressed state."
}
}
},
"tabs": {
"alignment": "Tabs alignment",
"overflow": "Tabs overflow",
"hidden": "Tabs with hidden slider",
"grow": "Tabs grow"
},
"helpAndSupport": "Help & support",
"aboutVuesticAdmin": "About Vuestic Admin",
"search": {
"placeholder": "Search..."
},
"buttonSelect": {
"dark": "Dark",
"light": "Light"
}
}

91
src/layouts/AppLayout.vue Normal file
View File

@ -0,0 +1,91 @@
<template>
<VaLayout
:top="{ fixed: true, order: 2 }"
:left="{ fixed: true, absolute: breakpoints.mdDown, order: 1, overlay: breakpoints.mdDown && !isSidebarMinimized }"
@leftOverlayClick="isSidebarMinimized = true"
>
<template #top>
<AppNavbar :is-mobile="isMobile" />
</template>
<template #left>
<AppSidebar :minimized="isSidebarMinimized" :animated="!isMobile" :mobile="isMobile" />
</template>
<template #content>
<div :class="{ minimized: isSidebarMinimized }" class="app-layout__sidebar-wrapper">
<div v-if="isFullScreenSidebar" class="flex justify-end">
<VaButton class="px-4 py-4" icon="md_close" preset="plain" @click="onCloseSidebarButtonClick" />
</div>
</div>
<AppLayoutNavigation v-if="!isMobile" class="p-4" />
<main class="p-4 pt-0">
<article>
<RouterView />
</article>
</main>
</template>
</VaLayout>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { onBeforeRouteUpdate } from 'vue-router'
import { useBreakpoint } from 'vuestic-ui'
import { useGlobalStore } from '../stores/global-store'
import AppLayoutNavigation from '../components/app-layout-navigation/AppLayoutNavigation.vue'
import AppNavbar from '../components/navbar/AppNavbar.vue'
import AppSidebar from '../components/sidebar/AppSidebar.vue'
const GlobalStore = useGlobalStore()
const breakpoints = useBreakpoint()
const sidebarWidth = ref('16rem')
const sidebarMinimizedWidth = ref(undefined)
const isMobile = ref(false)
const isTablet = ref(false)
const { isSidebarMinimized } = storeToRefs(GlobalStore)
const onResize = () => {
isSidebarMinimized.value = breakpoints.mdDown
isMobile.value = breakpoints.smDown
isTablet.value = breakpoints.mdDown
sidebarMinimizedWidth.value = isMobile.value ? '0' : '4.5rem'
sidebarWidth.value = isTablet.value ? '100%' : '16rem'
}
onMounted(() => {
window.addEventListener('resize', onResize)
onResize()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
})
onBeforeRouteUpdate(() => {
if (breakpoints.mdDown) {
// Collapse sidebar after route change for Mobile
isSidebarMinimized.value = true
}
})
const isFullScreenSidebar = computed(() => isTablet.value && !isSidebarMinimized.value)
const onCloseSidebarButtonClick = () => {
isSidebarMinimized.value = true
}
</script>
<style lang="scss" scoped>
// Prevent icon jump on animation
.va-sidebar {
width: unset !important;
min-width: unset !important;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<VaLayout v-if="breakpoint.lgUp" class="h-screen bg-[var(--va-background-secondary)]">
<template #left>
<RouterLink
class="bg-primary h-full flex items-center justify-center"
style="width: 35vw"
to="/"
aria-label="Visit homepage"
>
<VuesticLogo :height="28" start="#FFF" />
</RouterLink>
</template>
<template #content>
<main class="h-full flex items-center justify-center mx-auto max-w-[420px]">
<RouterView />
</main>
</template>
</VaLayout>
<VaLayout v-else class="h-screen bg-[var(--va-background-secondary)]">
<template #content>
<div class="p-4">
<main class="h-full flex flex-row items-center justify-start mx-auto max-w-[420px]">
<div class="flex flex-col items-start">
<RouterLink class="py-4" to="/" aria-label="Visit homepage">
<VuesticLogo class="mb-2" start="#0E41C9" />
</RouterLink>
<RouterView />
</div>
</main>
</div>
</template>
</VaLayout>
</template>
<script lang="ts" setup>
import { useBreakpoint } from 'vuestic-ui'
import VuesticLogo from '../components/VuesticLogo.vue'
const breakpoint = useBreakpoint()
</script>

View File

@ -0,0 +1,5 @@
<template>
<div class="max-w-7xl mx-auto">
<RouterView></RouterView>
</div>
</template>

28
src/main.ts Normal file
View File

@ -0,0 +1,28 @@
import { createApp } from 'vue'
import i18n from './i18n'
import { createVuestic } from 'vuestic-ui'
import { createGtm } from '@gtm-support/vue-gtm'
import stores from './stores'
import router from './router'
import vuesticGlobalConfig from './services/vuestic-ui/global-config'
import App from './App.vue'
const app = createApp(App)
app.use(stores)
app.use(router)
app.use(i18n)
app.use(createVuestic({ config: vuesticGlobalConfig }))
if (import.meta.env.VITE_APP_GTM_ENABLED) {
app.use(
createGtm({
id: import.meta.env.VITE_APP_GTM_KEY,
debug: false,
vueRouter: router,
}),
)
}
app.mount('#app')

30
src/pages/404.vue Normal file
View File

@ -0,0 +1,30 @@
<script lang="ts" setup>
import VuesticLogo from '../components/VuesticLogo.vue'
import NotFoundImage from '../components/NotFoundImage.vue'
</script>
<template>
<div class="flex flex-col justify-between h-screen items-center bg-[var(--va-background-secondary)]">
<RouterLink to="/">
<VuesticLogo :gradient="false" class="my-8 h-5" />
</RouterLink>
<div class="flex flex-col items-center gap-6 px-4 my-8">
<NotFoundImage />
<h1 class="va-h1 text-center sm:text-5xl text-4xl">Page not found</h1>
<p class="text-center">
The page you are looking for might have been removed had its name changed or is temporarily unavailable.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<VaButton to="/">Go to homepage</VaButton>
<VaButton href="https://github.com/epicmaxco/vuestic-admin/issues/new" preset="secondary" target="_blank"
>Create a GitHub issue
</VaButton>
</div>
</div>
<div />
</div>
</template>

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import RevenueUpdates from './cards/RevenueReport.vue'
import ProjectTable from './cards/ProjectTable.vue'
import RevenueByLocationMap from './cards/RevenueByLocationMap.vue'
import DataSection from './DataSection.vue'
import YearlyBreakup from './cards/YearlyBreakup.vue'
import MonthlyEarnings from './cards/MonthlyEarnings.vue'
import RegionRevenue from './cards/RegionRevenue.vue'
import Timeline from './cards/Timeline.vue'
</script>
<template>
<h1 class="page-title font-bold">Dashboard</h1>
<section class="flex flex-col gap-4">
<div class="flex flex-col sm:flex-row gap-4">
<RevenueUpdates class="w-full sm:w-[70%]" />
<div class="flex flex-col gap-4 w-full sm:w-[30%]">
<YearlyBreakup class="h-full" />
<MonthlyEarnings />
</div>
</div>
<DataSection />
<div class="flex flex-col md:flex-row gap-4">
<RevenueByLocationMap class="w-full md:w-4/6" />
<RegionRevenue class="w-full md:w-2/6" />
</div>
<div class="flex flex-col md:flex-row gap-4">
<ProjectTable class="w-full md:w-1/2" />
<Timeline class="w-full md:w-1/2" />
</div>
</section>
</template>

View File

@ -0,0 +1,80 @@
<template>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<DataSectionItem
v-for="metric in dashboardMetrics"
:key="metric.id"
:title="metric.title"
:value="metric.value"
:change-text="metric.changeText"
:up="metric.changeDirection === 'up'"
:icon-background="metric.iconBackground"
:icon-color="metric.iconColor"
>
<template #icon>
<VaIcon :name="metric.icon" size="large" />
</template>
</DataSectionItem>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useColors } from 'vuestic-ui'
import DataSectionItem from './DataSectionItem.vue'
interface DashboardMetric {
id: string
title: string
value: string
icon: string
changeText: string
changeDirection: 'up' | 'down'
iconBackground: string
iconColor: string
}
const { getColor } = useColors()
const dashboardMetrics = computed<DashboardMetric[]>(() => [
{
id: 'openInvoices',
title: 'Open invoices',
value: '$35,548',
icon: 'mso-attach_money',
changeText: '$1, 450',
changeDirection: 'down',
iconBackground: getColor('success'),
iconColor: getColor('on-success'),
},
{
id: 'ongoingProjects',
title: 'Ongoing project',
value: '15',
icon: 'mso-folder_open',
changeText: '25.36%',
changeDirection: 'up',
iconBackground: getColor('info'),
iconColor: getColor('on-info'),
},
{
id: 'employees',
title: 'Employees',
value: '25',
icon: 'mso-account_circle',
changeText: '2.5%',
changeDirection: 'up',
iconBackground: getColor('danger'),
iconColor: getColor('on-danger'),
},
{
id: 'newProfit',
title: 'New profit',
value: '27%',
icon: 'mso-grade',
changeText: '4%',
changeDirection: 'up',
iconBackground: getColor('warning'),
iconColor: getColor('on-warning'),
},
])
</script>

View File

@ -0,0 +1,50 @@
<template>
<VaCard>
<VaCardContent>
<section>
<header class="flex items-center justify-between">
<div class="text-lg font-semibold grow">{{ value }}</div>
<div
class="p-1 rounded"
:style="{
backgroundColor: iconBackground,
color: iconColor,
}"
>
<slot name="icon"></slot>
</div>
</header>
<div>
<p class="mb-2">{{ title }}</p>
<p class="text-xs text-secondary">
<span :class="changeClass">
<template v-if="up"></template>
<template v-else></template>
{{ changeText }}
</span>
since last month
</p>
</div>
</section>
</VaCardContent>
</VaCard>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { VaCard } from 'vuestic-ui'
const props = defineProps<{
title: string
value: string | number
changeText: string
up: boolean
iconBackground: string
iconColor: string
}>()
const changeClass = computed(() => ({
'text-success': props.up,
'text-red-600': !props.up,
}))
</script>

View File

@ -0,0 +1,65 @@
<template>
<VaCard>
<VaCardTitle>
<h1 class="card-title text-tag text-secondary font-bold uppercase">Monthly Earnings</h1>
</VaCardTitle>
<VaCardContent>
<div class="p-1 bg-black rounded absolute right-4 top-4">
<VaIcon name="mso-attach_money" color="#fff" size="large" />
</div>
<section>
<div class="text-xl font-bold mb-2">$6,820</div>
<p class="text-xs text-success">
<VaIcon name="arrow_upward" />
25.36%
<span class="text-secondary"> last month</span>
</p>
</section>
<div class="w-full flex items-center">
<VaChart :data="chartData" class="h-24" type="line" :options="options" />
</div>
</VaCardContent>
</VaCard>
</template>
<script setup lang="ts">
import { VaCard } from 'vuestic-ui'
import VaChart from '../../../../components/va-charts/VaChart.vue'
import { useChartData } from '../../../../data/charts/composables/useChartData'
import { lineChartData } from '../../../../data/charts/lineChartData'
import { ChartOptions } from 'chart.js'
const chartData = useChartData(lineChartData)
const options: ChartOptions<'line'> = {
scales: {
x: {
display: false,
grid: {
display: false, // Disable X-axis grid lines ("net")
},
},
y: {
display: false,
grid: {
display: false, // Disable Y-axis grid lines ("net")
},
ticks: {
display: false, // Hide Y-axis values
},
},
},
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: true,
},
},
}
</script>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import { defineVaDataTableColumns } from 'vuestic-ui'
import { Project } from '../../../projects/types'
import UserAvatar from '../../../users/widgets/UserAvatar.vue'
import ProjectStatusBadge from '../../../projects/components/ProjectStatusBadge.vue'
import { useProjects } from '../../../projects/composables/useProjects'
import { Pagination } from '../../../../data/pages/projects'
import { ref } from 'vue'
const columns = defineVaDataTableColumns([
{ label: 'Name', key: 'project_name', sortable: true },
{ label: 'Status', key: 'status', sortable: true },
{ label: 'Team', key: 'team', sortable: true },
])
const pagination = ref<Pagination>({ page: 1, perPage: 5, total: 0 })
const { projects, isLoading, sorting } = useProjects({
pagination,
})
const avatarColor = (userName: string) => {
const colors = ['primary', '#FFD43A', '#ADFF00', '#262824', 'danger']
const index = userName.charCodeAt(0) % colors.length
return colors[index]
}
</script>
<template>
<VaCard>
<VaCardTitle class="flex items-start justify-between">
<h1 class="card-title text-secondary font-bold uppercase">Projects</h1>
<VaButton preset="primary" size="small" to="/projects">View all projects</VaButton>
</VaCardTitle>
<VaCardContent>
<div v-if="projects.length > 0">
<VaDataTable
v-model:sort-by="sorting.sortBy"
v-model:sorting-order="sorting.sortingOrder"
:items="projects"
:columns="columns"
:loading="isLoading"
>
<template #cell(project_name)="{ rowData }">
<div class="ellipsis max-w-[230px] lg:max-w-[450px]">
{{ rowData.project_name }}
</div>
</template>
<template #cell(project_owner)="{ rowData }">
<div class="flex items-center gap-2 ellipsis max-w-[230px]">
<UserAvatar :user="rowData.project_owner" size="small" />
{{ rowData.project_owner.fullname }}
</div>
</template>
<template #cell(team)="{ rowData: project }">
<VaAvatarGroup
size="small"
:options="
(project as Project).team.map((user) => ({
label: user.fullname,
src: user.avatar,
fallbackText: user.fullname[0],
color: avatarColor(user.fullname),
}))
"
:max="2"
/>
</template>
<template #cell(status)="{ rowData: project }">
<ProjectStatusBadge :status="project.status" />
</template>
</VaDataTable>
</div>
<div v-else class="p-4 flex justify-center items-center text-[var(--va-secondary)]">No projects</div>
</VaCardContent>
</VaCard>
</template>

View File

@ -0,0 +1,92 @@
<template>
<VaCard>
<VaCardTitle class="flex justify-between">
<h1 class="card-title text-secondary font-bold uppercase">Revenue by Top Regions</h1>
</VaCardTitle>
<VaCardContent class="flex flex-col gap-1">
<div class="flex justify-between">
<VaButtonToggle v-model="selectedPeriod" :options="periods" color="background-element" size="small" />
<VaButton preset="primary" size="small" @click="exportAsCSV"> Export </VaButton>
</div>
<VaDataTable
class="region-revenue-table"
:columns="[
{ key: 'name', label: 'Top Region' },
{ key: 'revenue', label: 'Revenue', align: 'right' },
]"
:items="data"
>
<template #cell(revenue)="{ rowData }"> ${{ rowData[`revenue${selectedPeriod}`] }} </template>
</VaDataTable>
</VaCardContent>
</VaCard>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { downloadAsCSV } from '../../../../services/toCSV'
const selectedPeriod = ref('Today')
const periods = ['Today', 'Week', 'Month'].map((period) => ({ label: period, value: period }))
const data = [
{
name: 'Japan',
revenueToday: '4,748,454',
revenueWeek: '30,000,000',
revenueMonth: '120,000,000',
},
{
name: 'United Kingdom',
revenueToday: '405,748',
revenueWeek: '2,500,000',
revenueMonth: '10,000,000',
},
{
name: 'United States',
revenueToday: '308,536',
revenueWeek: '1,800,000',
revenueMonth: '8,000,000',
},
{
name: 'China',
revenueToday: '250,963',
revenueWeek: '1,600,000',
revenueMonth: '7,000,000',
},
{
name: 'Canada',
revenueToday: '29,415',
revenueWeek: '180,000',
revenueMonth: '800,000',
},
{
name: 'Australia',
revenueToday: '15,000',
revenueWeek: '100,000',
revenueMonth: '500,000',
},
{
name: 'India',
revenueToday: '10,000',
revenueWeek: '50,000',
revenueMonth: '200,000',
},
]
const exportAsCSV = () => {
downloadAsCSV(data, 'region-revenue')
}
</script>
<style lang="scss" scoped>
.region-revenue-table {
::v-deep(tbody) {
tr {
border-top: 1px solid var(--va-background-border);
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<VaCard class="flex flex-col">
<VaCardTitle class="flex items-center justify-between">
<h1 class="card-title text-secondary font-bold uppercase">Revenue by location</h1>
</VaCardTitle>
<VaCardContent class="flex-1 flex overflow-hidden">
<VaAspectRatio class="w-full md:min-h-72 overflow-hidden relative flex items-center">
<Map v-if="geoJson" :data="data" class="dashboard-map flex-1 h-full" />
<VaProgressCircle v-else indeterminate class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</VaAspectRatio>
</VaCardContent>
</VaCard>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { VaCard } from 'vuestic-ui'
import type countriesGeoJSON from '../../../../data/geo.json'
import Map from '../../../../components/va-charts/chart-types/Map.vue'
import type { ChartData } from 'chart.js'
const getRevenue = (countryName: string) => {
if (['United States of America', 'Canada', 'United Kingdom', 'China', 'Japan'].includes(countryName)) {
return 10
}
if (['Antarctica', 'Greenland'].includes(countryName)) {
return 0
}
return Math.random() * 10
}
const geoJson = ref<typeof countriesGeoJSON | null>(null)
onMounted(async () => {
geoJson.value = (await import('../../../../data/geo.json')).default
})
const data = computed<ChartData<'choropleth', { feature: any; value: number }[], string>>(() => {
if (!geoJson.value) {
return {
labels: [],
datasets: [],
}
}
return {
labels: geoJson.value.features.map((d) => d.properties.name),
datasets: [
{
label: 'Countries',
data: geoJson.value.features.map((d) => ({ feature: d, value: getRevenue(d.properties.name) })),
},
],
}
})
</script>
<style lang="scss" scoped>
.va-card--flex {
display: flex;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<VaCard class="flex flex-col">
<VaCardTitle class="flex items-start justify-between">
<h1 class="card-title text-secondary font-bold uppercase">Revenue Report</h1>
<div class="flex gap-2">
<VaSelect v-model="selectedMonth" preset="small" :options="monthsWithCurrentYear" class="w-24" />
<VaButton class="h-2" size="small" preset="primary" @click="exportAsCSV">Export</VaButton>
</div>
</VaCardTitle>
<VaCardContent class="flex flex-col-reverse md:flex-row md:items-center justify-between gap-5 h-full">
<section class="flex flex-col items-start w-full sm:w-1/3 md:w-2/5 lg:w-1/4 gap-2 md:gap-8 pl-4">
<div>
<p class="text-xl font-semibold">{{ formatMoney(totalEarnings) }}</p>
<p class="whitespace-nowrap mt-2">Total earnings</p>
</div>
<div class="flex flex-col sm:flex-col gap-2 md:gap-8 w-full">
<div>
<div class="flex items-center">
<span class="inline-block w-2 h-2 mr-2 -ml-4" :style="{ backgroundColor: earningsColor }"></span>
<span class="text-secondary">Earnings this month</span>
</div>
<div class="mt-2 text-xl font-semibold">{{ formatMoney(earningsForSelectedMonth.earning) }}</div>
</div>
<div>
<div class="flex items-center">
<span class="inline-block w-2 h-2 mr-2 -ml-4" :style="{ backgroundColor: expensesColor }"></span>
<span class="text-secondary">Expense this month</span>
</div>
<div class="mt-2 text-xl font-semibold">{{ formatMoney(earningsForSelectedMonth.expenses) }}</div>
</div>
</div>
</section>
<RevenueReportChart
class="w-2/3 md:w-3/5 lg:w-3/4 h-full min-h-72 sm:min-h-32 pt-4"
:revenues="revenues"
:months="months"
/>
</VaCardContent>
</VaCard>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { VaCard } from 'vuestic-ui'
import RevenueReportChart from './RevenueReportChart.vue'
import { downloadAsCSV } from '../../../../services/toCSV'
import {
earningsColor,
expensesColor,
months,
generateRevenues,
getRevenuePerMonth,
formatMoney,
} from '../../../../data/charts/revenueChartData'
const revenues = generateRevenues(months)
const currentYear = new Date().getFullYear()
const monthsWithCurrentYear = months.map((month) => `${month} ${currentYear}`)
const selectedMonth = ref(monthsWithCurrentYear[0])
const earningsForSelectedMonth = computed(() => getRevenuePerMonth(selectedMonth.value.split(' ')[0], revenues))
const totalEarnings = computed(() => {
return earningsForSelectedMonth.value.earning + earningsForSelectedMonth.value.expenses
})
const exportAsCSV = () => {
downloadAsCSV(revenues, 'revenue-report')
}
</script>

View File

@ -0,0 +1,111 @@
<template>
<div class="flex justify-center w-full h-full overflow-hidden relative">
<canvas ref="canvas" style="max-width: 100%"></canvas>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue'
import { Chart, registerables } from 'chart.js'
import type { Revenues } from '../../../../data/charts/revenueChartData'
import { earningsColor, expensesColor, formatMoney } from '../../../../data/charts/revenueChartData'
const { revenues, months } = defineProps<{
months: string[]
revenues: Revenues[]
}>()
Chart.register(...registerables)
const BR_THICKNESS = 4
Chart.register([
{
id: 'background-color',
beforeDatasetDraw: function (chart) {
const ctx = chart.ctx
const config = chart.config
config.data.datasets.forEach(function (dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex)
if (meta.type === 'bar') {
const bgColor = earningsColor
// Loop through each bar in the dataset
meta.data.forEach(function (bar) {
ctx.fillStyle = bgColor
ctx.fillRect(bar.x - BR_THICKNESS / 2, 0, BR_THICKNESS, chart.chartArea.bottom)
})
}
})
},
},
])
const canvas = ref<HTMLCanvasElement | null>(null)
const doShowChart = ref(false)
onMounted(() => {
if (canvas.value) {
const ctx = canvas.value.getContext('2d')
if (ctx) {
new Chart(ctx, {
type: 'bar',
data: {
labels: months,
datasets: [
{
// Show relative expenses ratio
data: revenues.map(({ earning, expenses }) => (expenses / earning) * 100),
backgroundColor: expensesColor,
barThickness: BR_THICKNESS,
},
],
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
stacked: true,
grid: {
display: false,
},
border: {
width: 0,
},
},
y: {
display: false,
beginAtZero: true,
ticks: {
callback: function (value) {
return formatMoney(Number(value))
},
},
},
},
},
})
}
}
nextTick(() => {
doShowChart.value = true
})
})
</script>
<style lang="scss" scoped>
canvas {
position: absolute;
height: 100%;
width: 100%;
}
</style>

Some files were not shown because too many files have changed in this diff Show More