initial commit (WIP)
This commit is contained in:
commit
9d8f424e1b
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
||||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
69
.eslintrc-auto-import.json
Normal file
69
.eslintrc-auto-import.json
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"Component": true,
|
||||||
|
"ComponentPublicInstance": true,
|
||||||
|
"ComputedRef": true,
|
||||||
|
"EffectScope": true,
|
||||||
|
"ExtractDefaultPropTypes": true,
|
||||||
|
"ExtractPropTypes": true,
|
||||||
|
"ExtractPublicPropTypes": true,
|
||||||
|
"InjectionKey": true,
|
||||||
|
"PropType": true,
|
||||||
|
"Ref": true,
|
||||||
|
"VNode": true,
|
||||||
|
"WritableComputedRef": true,
|
||||||
|
"computed": true,
|
||||||
|
"createApp": true,
|
||||||
|
"customRef": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"h": true,
|
||||||
|
"inject": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"provide": true,
|
||||||
|
"reactive": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"toValue": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"unref": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useRoute": true,
|
||||||
|
"useRouter": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
29
.eslintrc.js
Normal file
29
.eslintrc.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* .eslint.js
|
||||||
|
*
|
||||||
|
* ESLint configuration file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'vue3-recommended',
|
||||||
|
'plugin:prettier/recommend',
|
||||||
|
'@vue/eslint-config-typescript',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/valid-v-slot': 'off',
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Vuetify (Default)
|
||||||
|
|
||||||
|
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
|
||||||
|
|
||||||
|
## ❗️ Important Links
|
||||||
|
|
||||||
|
- 📄 [Docs](https://vuetifyjs.com/)
|
||||||
|
- 🚨 [Issues](https://issues.vuetifyjs.com/)
|
||||||
|
- 🏬 [Store](https://store.vuetifyjs.com/)
|
||||||
|
- 🎮 [Playground](https://play.vuetifyjs.com/)
|
||||||
|
- 💬 [Discord](https://community.vuetifyjs.com)
|
||||||
|
|
||||||
|
## 💿 Install
|
||||||
|
|
||||||
|
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
|
||||||
|
|
||||||
|
| Package Manager | Command |
|
||||||
|
|---------------------------------------------------------------|----------------|
|
||||||
|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
|
||||||
|
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
|
||||||
|
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
|
||||||
|
| [bun](https://bun.sh/#getting-started) | `bun install` |
|
||||||
|
|
||||||
|
After completing the installation, your environment is ready for Vuetify development.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
|
||||||
|
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
|
||||||
|
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
|
||||||
|
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
|
||||||
|
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
|
||||||
|
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
|
||||||
|
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
|
||||||
|
|
||||||
|
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
|
||||||
|
|
||||||
|
## 💡 Usage
|
||||||
|
|
||||||
|
This section covers how to start the development server and build your project for production.
|
||||||
|
|
||||||
|
### Starting the Development Server
|
||||||
|
|
||||||
|
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||||
|
|
||||||
|
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
To build your project for production, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||||
|
|
||||||
|
Once the build process is completed, your application will be ready for deployment in a production environment.
|
||||||
|
|
||||||
|
## 💪 Support Vuetify Development
|
||||||
|
|
||||||
|
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
|
||||||
|
|
||||||
|
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
|
||||||
|
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
|
||||||
|
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
|
||||||
|
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
|
||||||
|
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
|
||||||
|
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
|
||||||
|
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
|
||||||
|
|
||||||
|
## 📑 License
|
||||||
|
[MIT](http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016-present Vuetify, LLC
|
||||||
16
index.html
Normal file
16
index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Peresvet - System Trace</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
6645
package-lock.json
generated
Normal file
6645
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
package.json
Normal file
51
package.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "interface",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "6.2.95",
|
||||||
|
"@vueuse/core": "^10.10.0",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"core-js": "^3.34.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"roboto-fontface": "*",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vuetify": "^3.5.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/types": "^7.24.0",
|
||||||
|
"@types/node": "^20.11.25",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-standard": "^17.1.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-n": "^16.6.2",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"eslint-plugin-vue": "^9.22.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"prettier": "3.3.2",
|
||||||
|
"sass": "^1.71.1",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.4.2",
|
||||||
|
"unplugin-auto-import": "^0.17.5",
|
||||||
|
"unplugin-fonts": "^1.1.1",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"unplugin-vue-router": "^0.8.4",
|
||||||
|
"vite": "^5.1.5",
|
||||||
|
"vite-plugin-pages": "^0.32.2",
|
||||||
|
"vite-plugin-vue-layouts": "^0.11.0",
|
||||||
|
"vite-plugin-vuetify": "^2.0.3",
|
||||||
|
"vue-router": "^4.3.0",
|
||||||
|
"vue-tsc": "^2.0.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
25
public/images/flag_en.svg
Normal file
25
public/images/flag_en.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_270_67366)">
|
||||||
|
<rect width="32" height="24" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="#2E42A5"/>
|
||||||
|
<mask id="mask0_270_67366" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="24">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_270_67366)">
|
||||||
|
<path d="M-3.56311 22.2854L3.47858 25.2635L32.1598 3.23787L35.8741 -1.18761L28.3441 -2.18297L16.6457 7.3085L7.22968 13.7035L-3.56311 22.2854Z" fill="white"/>
|
||||||
|
<path d="M-2.59912 24.3719L0.988295 26.1001L34.5403 -1.59881H29.5032L-2.59912 24.3719Z" fill="#F50100"/>
|
||||||
|
<path d="M35.5631 22.2854L28.5214 25.2635L-0.159817 3.23787L-3.87415 -1.18761L3.65593 -2.18297L15.3543 7.3085L24.7703 13.7035L35.5631 22.2854Z" fill="white"/>
|
||||||
|
<path d="M35.3229 23.7829L31.7355 25.5111L17.4487 13.6518L13.2129 12.3267L-4.23151 -1.17246H0.805637L18.2403 12.0063L22.8713 13.5952L35.3229 23.7829Z" fill="#F50100"/>
|
||||||
|
<mask id="path-7-inside-1_270_67366" fill="white">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7778 -2H12.2222V8H-1.97247V16H12.2222V26H19.7778V16H34.0275V8H19.7778V-2Z"/>
|
||||||
|
</mask>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7778 -2H12.2222V8H-1.97247V16H12.2222V26H19.7778V16H34.0275V8H19.7778V-2Z" fill="#F50100"/>
|
||||||
|
<path d="M12.2222 -2V-4H10.2222V-2H12.2222ZM19.7778 -2H21.7778V-4H19.7778V-2ZM12.2222 8V10H14.2222V8H12.2222ZM-1.97247 8V6H-3.97247V8H-1.97247ZM-1.97247 16H-3.97247V18H-1.97247V16ZM12.2222 16H14.2222V14H12.2222V16ZM12.2222 26H10.2222V28H12.2222V26ZM19.7778 26V28H21.7778V26H19.7778ZM19.7778 16V14H17.7778V16H19.7778ZM34.0275 16V18H36.0275V16H34.0275ZM34.0275 8H36.0275V6H34.0275V8ZM19.7778 8H17.7778V10H19.7778V8ZM12.2222 0H19.7778V-4H12.2222V0ZM14.2222 8V-2H10.2222V8H14.2222ZM-1.97247 10H12.2222V6H-1.97247V10ZM0.0275269 16V8H-3.97247V16H0.0275269ZM12.2222 14H-1.97247V18H12.2222V14ZM14.2222 26V16H10.2222V26H14.2222ZM19.7778 24H12.2222V28H19.7778V24ZM17.7778 16V26H21.7778V16H17.7778ZM34.0275 14H19.7778V18H34.0275V14ZM32.0275 8V16H36.0275V8H32.0275ZM19.7778 10H34.0275V6H19.7778V10ZM17.7778 -2V8H21.7778V-2H17.7778Z" fill="white" mask="url(#path-7-inside-1_270_67366)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_270_67366">
|
||||||
|
<rect width="32" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
18
public/images/flag_ru.svg
Normal file
18
public/images/flag_ru.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_270_67492)">
|
||||||
|
<rect width="32" height="24" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="#3D58DB"/>
|
||||||
|
<mask id="mask0_270_67492" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="24">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V24H32V0H0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_270_67492)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0V8H32V0H0Z" fill="#F7FCFF"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 16V24H32V16H0Z" fill="#C51918"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_270_67492">
|
||||||
|
<rect width="32" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 788 B |
11
src/App.vue
Normal file
11
src/App.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-main>
|
||||||
|
<router-view />
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
16
src/api/auth.ts
Normal file
16
src/api/auth.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { $axios } from '@/plugins/axios'
|
||||||
|
import { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login: async (email: string, password: string): Promise<[AxiosResponse?, string?]> => {
|
||||||
|
try {
|
||||||
|
const data = await $axios.post('v1/auth/login', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
return [data, undefined]
|
||||||
|
} catch (e: any) {
|
||||||
|
return [undefined, e?.response?.data?.error ?? e.message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/api/groups.ts
Normal file
24
src/api/groups.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { $axios } from '@/plugins/axios';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { OrderBy } from '../types/order-by';
|
||||||
|
import { camelToSnake } from '../mixins/case-converter';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAll: async (
|
||||||
|
count: number = 10,
|
||||||
|
page: number = 1,
|
||||||
|
orderBy: OrderBy,
|
||||||
|
): Promise<[AxiosResponse?, string?]> => {
|
||||||
|
try {
|
||||||
|
const order: string = camelToSnake(
|
||||||
|
new URLSearchParams([...Object.entries(orderBy)]).toString(),
|
||||||
|
);
|
||||||
|
const data = await $axios.get(
|
||||||
|
`v1/groups?count=${count}&page=${page}&${order}`,
|
||||||
|
);
|
||||||
|
return [data, undefined];
|
||||||
|
} catch (e: any) {
|
||||||
|
return [undefined, e?.response?.data?.error ?? e.message];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
9
src/api/index.ts
Normal file
9
src/api/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import auth from './auth';
|
||||||
|
import users from './users';
|
||||||
|
import groups from './groups';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
auth,
|
||||||
|
users,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
24
src/api/users.ts
Normal file
24
src/api/users.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { $axios } from '@/plugins/axios';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { OrderBy } from '../types/order-by';
|
||||||
|
import { camelToSnake } from '../mixins/case-converter';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAll: async (
|
||||||
|
count: number = 10,
|
||||||
|
page: number = 1,
|
||||||
|
orderBy: OrderBy,
|
||||||
|
): Promise<[AxiosResponse?, string?]> => {
|
||||||
|
try {
|
||||||
|
const order: string = camelToSnake(
|
||||||
|
new URLSearchParams([...Object.entries(orderBy)]).toString(),
|
||||||
|
);
|
||||||
|
const data = await $axios.get(
|
||||||
|
`v1/users?count=${count}&page=${page}&${order}`,
|
||||||
|
);
|
||||||
|
return [data, undefined];
|
||||||
|
} catch (e: any) {
|
||||||
|
return [undefined, e?.response?.data?.error ?? e.message];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Light.ttf
Normal file
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Regular.ttf
Normal file
BIN
src/assets/fonts/MDI/MaterialSymbolsOutlined-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Normal file
BIN
src/assets/fonts/Roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
8
src/assets/logo.svg
Normal file
8
src/assets/logo.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="81" height="24" viewBox="0 0 81 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="48" y="16.0547" width="32.1081" height="7.94595" fill="black"/>
|
||||||
|
<rect width="32.1081" height="7.94595" fill="black"/>
|
||||||
|
<rect x="40.0547" y="7.94531" width="7.94595" height="8.10811" fill="black"/>
|
||||||
|
<rect x="32.1094" y="7.94531" width="7.94595" height="16.0541" fill="black"/>
|
||||||
|
<rect y="7.94531" width="7.94595" height="16.0541" fill="black"/>
|
||||||
|
<rect x="40.0547" width="40.0541" height="7.94595" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 518 B |
3
src/assets/styles/README.md
Normal file
3
src/assets/styles/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Styles
|
||||||
|
|
||||||
|
This directory is for configuring the styles of the application.
|
||||||
66
src/assets/styles/core.scss
Normal file
66
src/assets/styles/core.scss
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import './fonts.css';
|
||||||
|
@import './table.css';
|
||||||
|
|
||||||
|
.accent-modal {
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
background: rgb(var(--v-theme-code)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-b {
|
||||||
|
border-bottom-width: 1px !important;
|
||||||
|
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-t {
|
||||||
|
border-top-width: 1px !important;
|
||||||
|
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-r {
|
||||||
|
border-right-width: 1px !important;
|
||||||
|
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-l {
|
||||||
|
border-left-width: 1px !important;
|
||||||
|
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-a {
|
||||||
|
border-width: 1px !important;
|
||||||
|
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-h6 {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-mini {
|
||||||
|
min-height: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
/* 14px */
|
||||||
|
line-height: 1.25rem;
|
||||||
|
/* 20px */
|
||||||
|
overscroll-behavior: none;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-theme--light {
|
||||||
|
--v-theme-primary: 11, 87, 208 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-theme--dark {
|
||||||
|
--v-theme-primary: 51, 129, 255 !important;
|
||||||
|
|
||||||
|
.v-overlay__scrim {
|
||||||
|
--v-theme-on-surface: 0, 0, 0;
|
||||||
|
--v-overlay-opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/assets/styles/fonts.css
Normal file
61
src/assets/styles/fonts.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
html {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto/Roboto-Thin.ttf') format('truetype');
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto/Roboto-Light.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto/Roboto-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto/Roboto-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto/Roboto-Bold.ttf') format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto/Roboto-Black.ttf') format('truetype');
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MDI';
|
||||||
|
src: url('../fonts/MDI/MaterialSymbolsOutlined-Light.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MDI';
|
||||||
|
src: url('../fonts/MDI/MaterialSymbolsOutlined-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mso {
|
||||||
|
font-family: 'MDI';
|
||||||
|
font-size: 24px;
|
||||||
|
font-variation-settings:
|
||||||
|
'FILL' 0,
|
||||||
|
'wght' 300,
|
||||||
|
'GRAD' 0,
|
||||||
|
'opsz' 24
|
||||||
|
}
|
||||||
83
src/assets/styles/settings.scss
Normal file
83
src/assets/styles/settings.scss
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* src/styles/settings.scss
|
||||||
|
*
|
||||||
|
* Configures SASS variables and Vuetify overwrites
|
||||||
|
*/
|
||||||
|
|
||||||
|
// https://vuetifyjs.com/features/sass-variables/`
|
||||||
|
@use 'vuetify/settings' with ( // $color-pack: false
|
||||||
|
$card-item-padding: .625rem 0,
|
||||||
|
$selection-control-size: 25px,
|
||||||
|
$utilities: ("align-content": false,
|
||||||
|
"align-items": false,
|
||||||
|
"align-self": false,
|
||||||
|
"border-bottom": false,
|
||||||
|
"border-end": false,
|
||||||
|
"border-opacity": false,
|
||||||
|
"border-start": false,
|
||||||
|
"border-style": false,
|
||||||
|
"border-top": false,
|
||||||
|
"border": false,
|
||||||
|
"display": false,
|
||||||
|
"flex-direction": false,
|
||||||
|
"flex-grow": false,
|
||||||
|
"flex-shrink": false,
|
||||||
|
"flex-wrap": false,
|
||||||
|
"flex": false,
|
||||||
|
"float-ltr": false,
|
||||||
|
"float-rtl": false,
|
||||||
|
"float": false,
|
||||||
|
"font-italic": false,
|
||||||
|
"font-weight": false,
|
||||||
|
"justify-content": false,
|
||||||
|
"margin-bottom": false,
|
||||||
|
"margin-end": false,
|
||||||
|
"margin-left": false,
|
||||||
|
"margin-right": false,
|
||||||
|
"margin-start": false,
|
||||||
|
"margin-top": false,
|
||||||
|
"margin-x": false,
|
||||||
|
"margin-y": false,
|
||||||
|
"margin": false,
|
||||||
|
"negative-margin-bottom": false,
|
||||||
|
"negative-margin-end": false,
|
||||||
|
"negative-margin-left": false,
|
||||||
|
"negative-margin-right": false,
|
||||||
|
"negative-margin-start": false,
|
||||||
|
"negative-margin-top": false,
|
||||||
|
"negative-margin-x": false,
|
||||||
|
"negative-margin-y": false,
|
||||||
|
"negative-margin": false,
|
||||||
|
"order": false,
|
||||||
|
"overflow-wrap": false,
|
||||||
|
"overflow-x": false,
|
||||||
|
"overflow-y": false,
|
||||||
|
"overflow": false,
|
||||||
|
"padding-bottom": false,
|
||||||
|
"padding-end": false,
|
||||||
|
"padding-left": false,
|
||||||
|
"padding-right": false,
|
||||||
|
"padding-start": false,
|
||||||
|
"padding-top": false,
|
||||||
|
"padding-x": false,
|
||||||
|
"padding-y": false,
|
||||||
|
"padding": false,
|
||||||
|
// "rounded-bottom-end": false,
|
||||||
|
// "rounded-bottom-start": false,
|
||||||
|
// "rounded-bottom": false,
|
||||||
|
// "rounded-end": false,
|
||||||
|
// "rounded-start": false,
|
||||||
|
// "rounded-top-end": false,
|
||||||
|
// "rounded-top-start": false,
|
||||||
|
// "rounded-top": false,
|
||||||
|
// "rounded": false,
|
||||||
|
"text-align": false,
|
||||||
|
"text-decoration": false,
|
||||||
|
"text-mono": false,
|
||||||
|
"text-opacity": false,
|
||||||
|
"text-overflow": false,
|
||||||
|
"text-transform": false,
|
||||||
|
// "typography": false,
|
||||||
|
"white-space": false,
|
||||||
|
),
|
||||||
|
);
|
||||||
29
src/assets/styles/table.css
Normal file
29
src/assets/styles/table.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.v-table .v-table__wrapper>table>tbody>tr:not(:last-child)>td,
|
||||||
|
.v-table .v-table__wrapper>table>tbody>tr:not(:last-child)>th {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table .v-table__wrapper>table>tbody>tr>td,
|
||||||
|
.v-table .v-table__wrapper>table>tbody>tr>th {
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
table>tbody>tr>td:last-child,
|
||||||
|
table>thead>tr>th:last-child {
|
||||||
|
position: sticky !important;
|
||||||
|
position: -webkit-sticky !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
right: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none !important;
|
||||||
|
z-index: 9998;
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
border-left-width: 1px !important;
|
||||||
|
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table>thead>tr>th:last-child {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
191
src/auto-imports.d.ts
vendored
Normal file
191
src/auto-imports.d.ts
vendored
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
|
// for vue template auto import
|
||||||
|
import { UnwrapRef } from 'vue'
|
||||||
|
declare module 'vue' {
|
||||||
|
interface GlobalComponents {}
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
|
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||||
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
|
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||||
|
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||||
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
|
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||||
|
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||||
|
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||||
|
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||||
|
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||||
|
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||||
|
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||||
|
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||||
|
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||||
|
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||||
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
|
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||||
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||||
|
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||||
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||||
|
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||||
|
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||||
|
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||||
|
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||||
|
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||||
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
|
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||||
|
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||||
|
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||||
|
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||||
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
|
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||||
|
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||||
|
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||||
|
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
interface GlobalComponents {}
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
|
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||||
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
|
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||||
|
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||||
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
|
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||||
|
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||||
|
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||||
|
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||||
|
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||||
|
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||||
|
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||||
|
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||||
|
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||||
|
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||||
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
|
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||||
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||||
|
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||||
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||||
|
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||||
|
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||||
|
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||||
|
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||||
|
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||||
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
|
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||||
|
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||||
|
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||||
|
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||||
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
|
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||||
|
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||||
|
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||||
|
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/components.d.ts
vendored
Normal file
20
src/components.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
export {}
|
||||||
|
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
AppFooter: typeof import('./components/AppFooter.vue')['default']
|
||||||
|
ErrorPlate: typeof import('./components/ErrorPlate.vue')['default']
|
||||||
|
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||||
|
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||||
|
LogoComponent: typeof import('./components/Icons/LogoComponent.vue')['default']
|
||||||
|
NavigationDrawer: typeof import('./components/NavigationDrawer.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
ThemeSwitcher: typeof import('./components/ThemeSwitcher.vue')['default']
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/components/AppFooter.vue
Normal file
79
src/components/AppFooter.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<v-footer height="40" app>
|
||||||
|
<a
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.title"
|
||||||
|
:href="item.href"
|
||||||
|
:title="item.title"
|
||||||
|
class="d-inline-block mx-2 social-link"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="item.icon"
|
||||||
|
:size="item.icon === '$vuetify' ? 24 : 16"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-caption text-disabled"
|
||||||
|
style="position: absolute; right: 16px;"
|
||||||
|
>
|
||||||
|
© 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
|
||||||
|
—
|
||||||
|
<a
|
||||||
|
class="text-decoration-none on-surface"
|
||||||
|
href="https://vuetifyjs.com/about/licensing/"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
MIT License
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</v-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: 'Vuetify Documentation',
|
||||||
|
icon: `$vuetify`,
|
||||||
|
href: 'https://vuetifyjs.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify Support',
|
||||||
|
icon: 'mdi-shield-star-outline',
|
||||||
|
href: 'https://support.vuetifyjs.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify X',
|
||||||
|
icon: `svg:M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z`,
|
||||||
|
href: 'https://x.com/vuetifyjs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify GitHub',
|
||||||
|
icon: `mdi-github`,
|
||||||
|
href: 'https://github.com/vuetifyjs/vuetify',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify Discord',
|
||||||
|
icon: `mdi-discord`,
|
||||||
|
href: 'https://community.vuetifyjs.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify Reddit',
|
||||||
|
icon: `mdi-reddit`,
|
||||||
|
href: 'https://reddit.com/r/vuetifyjs',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="sass">
|
||||||
|
.social-link :deep(.v-icon)
|
||||||
|
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
|
||||||
|
text-decoration: none
|
||||||
|
transition: .2s ease-in-out
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
color: rgba(25, 118, 210, 1)
|
||||||
|
</style>
|
||||||
67
src/components/ErrorPlate.vue
Normal file
67
src/components/ErrorPlate.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useLocale } from 'vuetify'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
noBorder: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useLocale()
|
||||||
|
|
||||||
|
function parseError(e: string): string {
|
||||||
|
if (e.includes('$vuetify', 0)) {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.includes('validator')) {
|
||||||
|
const errs = e.split(' || ')
|
||||||
|
for (const err of errs) {
|
||||||
|
if (err.includes('validator', 0)) {
|
||||||
|
const splitted = err.split(':')
|
||||||
|
if (splitted?.length) {
|
||||||
|
const right = splitted.pop()
|
||||||
|
if (right && right.includes('/')) {
|
||||||
|
const [field, key] = right.split('/')
|
||||||
|
return t(`$vuetify.validator.field`,
|
||||||
|
field.toLowerCase(),
|
||||||
|
t(`$vuetify.validator.${key.replace(/'/gm, '')}`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-if="error?.length"
|
||||||
|
class="text-sm text-error flex items-center"
|
||||||
|
:class="{
|
||||||
|
'error-block': block,
|
||||||
|
'my-2': !block,
|
||||||
|
'border-b': !noBorder,
|
||||||
|
}"
|
||||||
|
><span class="mso mr-2 text-2xl">error</span> {{ parseError(error) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-block {
|
||||||
|
background-color: rgba(var(--v-theme-error), var(--v-border-opacity));
|
||||||
|
border-color: rgba(var(--v-theme-error), var(--v-border-opacity));
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
157
src/components/HelloWorld.vue
Normal file
157
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="fill-height">
|
||||||
|
<v-responsive
|
||||||
|
class="align-centerfill-height mx-auto"
|
||||||
|
max-width="900"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
class="mb-4"
|
||||||
|
height="150"
|
||||||
|
src="@/assets/logo.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||||
|
|
||||||
|
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-4" />
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||||
|
prepend-icon="mdi-rocket-launch-outline"
|
||||||
|
rounded="lg"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<v-img position="top right" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #subtitle>
|
||||||
|
<div class="text-subtitle-1">
|
||||||
|
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-overlay
|
||||||
|
opacity=".12"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://vuetifyjs.com/"
|
||||||
|
prepend-icon="mdi-text-box-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Learn about all things Vuetify in our documentation."
|
||||||
|
target="_blank"
|
||||||
|
title="Documentation"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||||
|
prepend-icon="mdi-star-circle-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Explore available framework Features."
|
||||||
|
target="_blank"
|
||||||
|
title="Features"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://vuetifyjs.com/components/all"
|
||||||
|
prepend-icon="mdi-widgets-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Discover components in the API Explorer."
|
||||||
|
target="_blank"
|
||||||
|
title="Components"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6">
|
||||||
|
<v-card
|
||||||
|
append-icon="mdi-open-in-new"
|
||||||
|
class="py-4"
|
||||||
|
color="surface-variant"
|
||||||
|
href="https://discord.vuetifyjs.com"
|
||||||
|
prepend-icon="mdi-account-group-outline"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
rounded="lg"
|
||||||
|
subtitle="Connect with Vuetify developers."
|
||||||
|
target="_blank"
|
||||||
|
title="Community"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-overlay
|
||||||
|
opacity=".06"
|
||||||
|
scrim="primary"
|
||||||
|
contained
|
||||||
|
model-value
|
||||||
|
persistent
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-responsive>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
||||||
23
src/components/Icons/LogoComponent.vue
Normal file
23
src/components/Icons/LogoComponent.vue
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, type ComputedRef } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'dark'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fill: ComputedRef<string> = computed(() => props.theme === 'dark' ? '#FFF' : '#000')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg width="81" height="24" viewBox="0 0 81 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="48" y="16.0547" width="32.1081" height="7.94595" :fill="fill"/>
|
||||||
|
<rect width="32.1081" height="7.94595" :fill="fill"/>
|
||||||
|
<rect x="40.0547" y="7.94531" width="7.94595" height="8.10811" :fill="fill"/>
|
||||||
|
<rect x="32.1094" y="7.94531" width="7.94595" height="16.0541" :fill="fill"/>
|
||||||
|
<rect y="7.94531" width="7.94595" height="16.0541" :fill="fill"/>
|
||||||
|
<rect x="40.0547" width="40.0541" height="7.94595" :fill="fill"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
75
src/components/LanguageSwitcher.vue
Normal file
75
src/components/LanguageSwitcher.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useLocale } from 'vuetify';
|
||||||
|
import { $cookie } from '@/plugins/cookie'
|
||||||
|
import { onBeforeMount } from 'vue';
|
||||||
|
import { COOKIE_LOCALE } from '../constants/static';
|
||||||
|
import Moment from '@/plugins/moment'
|
||||||
|
|
||||||
|
const { current } = useLocale()
|
||||||
|
const languages = [
|
||||||
|
{
|
||||||
|
title: 'Русский',
|
||||||
|
locale: 'ru'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'English',
|
||||||
|
locale: 'en'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const key = COOKIE_LOCALE
|
||||||
|
|
||||||
|
const get = (): string => {
|
||||||
|
const v = $cookie.get(key);
|
||||||
|
Moment.$moment.locale(v);
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
const set = (locale: string): void => {
|
||||||
|
current.value = locale;
|
||||||
|
Moment.$moment.locale(locale);
|
||||||
|
$cookie.set(key, locale);
|
||||||
|
}
|
||||||
|
const setLocaleFromExistCookie = (): void => {
|
||||||
|
const v = get()
|
||||||
|
if (v?.length && typeof v === 'string') {
|
||||||
|
set(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBeforeMount(setLocaleFromExistCookie)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-menu
|
||||||
|
transition="slide-y-transition"
|
||||||
|
close-delay="0"
|
||||||
|
open-delay="0"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn icon density="comfortable">
|
||||||
|
<span class="mso" v-bind="props">language</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list density="compact" class="mt-3 accent-modal" elevation="0">
|
||||||
|
<v-list-item
|
||||||
|
v-for="(item, i) in languages"
|
||||||
|
:key="i"
|
||||||
|
class="cm-mini"
|
||||||
|
@click="set(item.locale)"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<img
|
||||||
|
class="flag mr-2"
|
||||||
|
:src="`/images/flag_${item.locale}.svg`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flag {
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
45
src/components/NavigationDrawer.vue
Normal file
45
src/components/NavigationDrawer.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useLocale } from 'vuetify';
|
||||||
|
import { ROUTES } from '@/constants/routes';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
drawer: Boolean
|
||||||
|
})
|
||||||
|
const emits = defineEmits(['change'])
|
||||||
|
|
||||||
|
const { t } = useLocale()
|
||||||
|
|
||||||
|
const modelValue = ref<boolean>(props.drawer)
|
||||||
|
|
||||||
|
watch(props, (v: any, _) => {
|
||||||
|
modelValue.value = v.drawer
|
||||||
|
})
|
||||||
|
|
||||||
|
function change(e: boolean) {
|
||||||
|
emits('change', e)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="modelValue"
|
||||||
|
@update:modelValue="change"
|
||||||
|
class="pt-3 elevation-0 border-r"
|
||||||
|
:scrim="false"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(item, i) in ROUTES"
|
||||||
|
:key="i"
|
||||||
|
:value="item"
|
||||||
|
color="primary"
|
||||||
|
:to="item.value"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<span class="mso mr-3">{{ item.icon }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
35
src/components/README.md
Normal file
35
src/components/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Components
|
||||||
|
|
||||||
|
Vue template files in this folder are automatically imported.
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
|
||||||
|
|
||||||
|
The following example assumes a component located at `src/components/MyComponent.vue`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MyComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
When your template is rendered, the component's import will automatically be inlined, which renders to this:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MyComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import MyComponent from '@/components/MyComponent.vue'
|
||||||
|
</script>
|
||||||
|
```
|
||||||
35
src/components/ThemeSwitcher.vue
Normal file
35
src/components/ThemeSwitcher.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTheme } from 'vuetify'
|
||||||
|
import { $cookie } from '@/plugins/cookie'
|
||||||
|
import { onBeforeMount } from 'vue';
|
||||||
|
import { COOKIE_THEME } from '../constants/static';
|
||||||
|
|
||||||
|
const theme = useTheme()
|
||||||
|
const key = COOKIE_THEME
|
||||||
|
|
||||||
|
const get = (): string => {
|
||||||
|
return $cookie.get(key);
|
||||||
|
}
|
||||||
|
const set = (value: string): void => {
|
||||||
|
theme.global.name.value = value;
|
||||||
|
$cookie.set(key, value);
|
||||||
|
}
|
||||||
|
const toggle = (): void => {
|
||||||
|
const t = theme.global.current?.value?.dark ? 'light' : 'dark'
|
||||||
|
theme.global.name.value = t
|
||||||
|
$cookie.set(key, t);
|
||||||
|
}
|
||||||
|
const setThemeFromExistCookie = (): void => {
|
||||||
|
const v = get()
|
||||||
|
if (v?.length && typeof v === 'string') {
|
||||||
|
set(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBeforeMount(setThemeFromExistCookie)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-btn icon density="comfortable" @click="toggle">
|
||||||
|
<span class="mso">routine</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
11
src/constants/defaults.ts
Normal file
11
src/constants/defaults.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const defaultCursor = JSON.stringify({
|
||||||
|
count: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
totalRows: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultSort = JSON.stringify({
|
||||||
|
key: 'id',
|
||||||
|
order: 'asc',
|
||||||
|
});
|
||||||
32
src/constants/routes.ts
Normal file
32
src/constants/routes.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export const ROUTES = [
|
||||||
|
{
|
||||||
|
title: '$vuetify.navigation.dashboards',
|
||||||
|
value: '/',
|
||||||
|
icon: 'dashboard'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '$vuetify.navigation.tasks',
|
||||||
|
value: '/tasks',
|
||||||
|
icon: 'web_traffic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '$vuetify.navigation.servers',
|
||||||
|
value: '/servers',
|
||||||
|
icon: 'dns'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '$vuetify.navigation.users',
|
||||||
|
value: '/users',
|
||||||
|
icon: 'group'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '$vuetify.navigation.patterns',
|
||||||
|
value: '/patterns',
|
||||||
|
icon: 'layers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '$vuetify.navigation.logs',
|
||||||
|
value: '/logs',
|
||||||
|
icon: 'menu'
|
||||||
|
},
|
||||||
|
]
|
||||||
4
src/constants/static.ts
Normal file
4
src/constants/static.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const ITEMS_PER_PAGE = [10, 25, 50, 100]
|
||||||
|
|
||||||
|
export const COOKIE_LOCALE = 'user_locale'
|
||||||
|
export const COOKIE_THEME = 'user_theme'
|
||||||
7
src/enums/user-actions.enum.ts
Normal file
7
src/enums/user-actions.enum.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export enum UserActions {
|
||||||
|
EDIT,
|
||||||
|
BLOCK,
|
||||||
|
UNBLOCK,
|
||||||
|
RESET_PASSWORD,
|
||||||
|
DELETE,
|
||||||
|
}
|
||||||
4
src/enums/user-tab.enum.ts
Normal file
4
src/enums/user-tab.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum UsersPageTabs {
|
||||||
|
USERS,
|
||||||
|
GROUPS,
|
||||||
|
}
|
||||||
5
src/layouts/README.md
Normal file
5
src/layouts/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Layouts
|
||||||
|
|
||||||
|
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages.
|
||||||
|
|
||||||
|
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.
|
||||||
13
src/layouts/centered.vue
Normal file
13
src/layouts/centered.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-main>
|
||||||
|
<div class="flex flex-col items-center justify-center h-full w-full">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
39
src/layouts/default.vue
Normal file
39
src/layouts/default.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useTheme } from 'vuetify'
|
||||||
|
import NavigationDrawer from '../components/NavigationDrawer.vue'
|
||||||
|
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||||
|
import ThemeSwitcher from '../components/ThemeSwitcher.vue'
|
||||||
|
|
||||||
|
const drawer = ref(true)
|
||||||
|
const theme = useTheme()
|
||||||
|
const currentTheme = computed(() => theme.global.name.value)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-app :style="`background-color: ${currentTheme === 'light' ? '#f0f4f9' : '#1c1c1c'}`">
|
||||||
|
<v-app-bar
|
||||||
|
prominent
|
||||||
|
class="elevation-0 border-b px-2"
|
||||||
|
>
|
||||||
|
<v-app-bar-nav-icon density="comfortable" @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
|
<v-app-bar-title>System Trace</v-app-bar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<!-- Theme switcher -->
|
||||||
|
<ThemeSwitcher class="mr-1" />
|
||||||
|
<!-- Language switcher -->
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<NavigationDrawer
|
||||||
|
:drawer="drawer"
|
||||||
|
@change="(e) => drawer = e"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<router-view></router-view>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
13
src/locales/en/actions.json
Normal file
13
src/locales/en/actions.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"actions": {
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"create-group": "Create group",
|
||||||
|
"create-user": "Create user",
|
||||||
|
"reset-password": "Reset password",
|
||||||
|
"edit": "Edit",
|
||||||
|
"block": "Block",
|
||||||
|
"unblock": "Unblock",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/locales/en/auth.json
Normal file
11
src/locales/en/auth.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"copyright": "Product of Peresvet LLC",
|
||||||
|
"title": "Authentication",
|
||||||
|
"email": "E-Mail address",
|
||||||
|
"password": "Password",
|
||||||
|
"login": "Log in",
|
||||||
|
"help": "Any problems? Contact ",
|
||||||
|
"help-link": "administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/locales/en/errors.json
Normal file
12
src/locales/en/errors.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"User with specified E-Mail and password not found": "User with specified email and password was not found",
|
||||||
|
"validator": {
|
||||||
|
"field": "Field {0} is not compliant: {1}",
|
||||||
|
"required": "required",
|
||||||
|
"min": "min. value",
|
||||||
|
"max": "max. value",
|
||||||
|
"password": "password rules",
|
||||||
|
"email": "E-Mail rules",
|
||||||
|
"endswith": "invalid value"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/locales/en/index.ts
Normal file
15
src/locales/en/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import errors from './errors.json'
|
||||||
|
import navigation from './navigation.json'
|
||||||
|
import users from './users.json'
|
||||||
|
import actions from './actions.json'
|
||||||
|
import auth from './auth.json'
|
||||||
|
import misc from './misc.json'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...errors,
|
||||||
|
...navigation,
|
||||||
|
...users,
|
||||||
|
...actions,
|
||||||
|
...auth,
|
||||||
|
...misc,
|
||||||
|
}
|
||||||
5
src/locales/en/misc.json
Normal file
5
src/locales/en/misc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"misc": {
|
||||||
|
"actions": "Actions"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/locales/en/navigation.json
Normal file
10
src/locales/en/navigation.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"navigation": {
|
||||||
|
"dashboards": "Dashboards",
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"servers": "Servers",
|
||||||
|
"users": "Users",
|
||||||
|
"patterns": "Patterns",
|
||||||
|
"logs": "Logs"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/locales/en/users.json
Normal file
31
src/locales/en/users.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"users": {
|
||||||
|
"title": "Users",
|
||||||
|
"search": "Search",
|
||||||
|
"tabs": {
|
||||||
|
"users": "Users",
|
||||||
|
"groups": "Groups"
|
||||||
|
},
|
||||||
|
"all": "Users in group",
|
||||||
|
"heads": {
|
||||||
|
"id": "ID",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"group": "Group",
|
||||||
|
"real-name": "Real name",
|
||||||
|
"password": "Password",
|
||||||
|
"status": "Status",
|
||||||
|
"last-visit": "Last visit",
|
||||||
|
"created-at": "Created at",
|
||||||
|
"updated-at": "Last update",
|
||||||
|
"name": "Name",
|
||||||
|
"permissions": "Permissions",
|
||||||
|
"issuer": "Issuer"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"active": "Active",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"set": "Set",
|
||||||
|
"unset": "Not set"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/locales/ru/actions.json
Normal file
13
src/locales/ru/actions.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"actions": {
|
||||||
|
"refresh": "Обновить",
|
||||||
|
"create-group": "Создать группу",
|
||||||
|
"create-user": "Создать пользователя",
|
||||||
|
"reset-password": "Сбросить пароль",
|
||||||
|
"edit": "Редактировать",
|
||||||
|
"block": "Заблокировать",
|
||||||
|
"unblock": "Разблокировать",
|
||||||
|
"confirm": "Применить",
|
||||||
|
"cancel": "Отменить"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/locales/ru/auth.json
Normal file
11
src/locales/ru/auth.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"copyright": "Продукт компании Пересвет",
|
||||||
|
"title": "Авторизация",
|
||||||
|
"email": "Адрес эл. почты",
|
||||||
|
"password": "Пароль",
|
||||||
|
"login": "Войти",
|
||||||
|
"help": "Проблемы? Сообщите ",
|
||||||
|
"help-link": "администратору"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/locales/ru/errors.json
Normal file
12
src/locales/ru/errors.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"User with specified E-Mail and password not found": "Пользователь с указанными E-Mail и паролем не найден",
|
||||||
|
"validator": {
|
||||||
|
"field": "Поле {0} не соответствует требованиям: {1}",
|
||||||
|
"required": "обязательное поле",
|
||||||
|
"min": "мин. значение",
|
||||||
|
"max": "макс. значение",
|
||||||
|
"password": "требования к паролю",
|
||||||
|
"email": "требования к E-Mail",
|
||||||
|
"endswith": "недействительное значение"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/locales/ru/index.ts
Normal file
15
src/locales/ru/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import errors from './errors.json'
|
||||||
|
import navigation from './navigation.json'
|
||||||
|
import users from './users.json'
|
||||||
|
import actions from './actions.json'
|
||||||
|
import auth from './auth.json'
|
||||||
|
import misc from './misc.json'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...errors,
|
||||||
|
...navigation,
|
||||||
|
...users,
|
||||||
|
...actions,
|
||||||
|
...auth,
|
||||||
|
...misc,
|
||||||
|
}
|
||||||
5
src/locales/ru/misc.json
Normal file
5
src/locales/ru/misc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"misc": {
|
||||||
|
"actions": "Действия"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/locales/ru/navigation.json
Normal file
10
src/locales/ru/navigation.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"navigation": {
|
||||||
|
"dashboards": "Дашборды",
|
||||||
|
"tasks": "Задачи",
|
||||||
|
"servers": "Серверы",
|
||||||
|
"users": "Пользователи",
|
||||||
|
"patterns": "Паттерны",
|
||||||
|
"logs": "Логи"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/locales/ru/users.json
Normal file
34
src/locales/ru/users.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"users": {
|
||||||
|
"title": "Пользователи",
|
||||||
|
"search": "Поиск",
|
||||||
|
"tabs": {
|
||||||
|
"users": "Пользователи",
|
||||||
|
"groups": "Группы"
|
||||||
|
},
|
||||||
|
"all": "Пользователи в группе",
|
||||||
|
"heads": {
|
||||||
|
"id": "ID",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"group": "Группа",
|
||||||
|
"real-name": "Наст. имя",
|
||||||
|
"password": "Пароль",
|
||||||
|
"status": "Статус",
|
||||||
|
"last-visit": "Посл. вход",
|
||||||
|
"created-at": "Создан",
|
||||||
|
"updated-at": "Посл. изменения",
|
||||||
|
"name": "Название",
|
||||||
|
"permissions": "Права",
|
||||||
|
"issuer": "Издатель"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"active": "Активный",
|
||||||
|
"blocked": "Заблокирован",
|
||||||
|
"set": "Установлен",
|
||||||
|
"unset": "Не установлен"
|
||||||
|
},
|
||||||
|
"creation": {
|
||||||
|
"create": "Создание пользователя"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main.ts
Normal file
23
src/main.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* main.ts
|
||||||
|
*
|
||||||
|
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import { registerPlugins } from '@/plugins'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
import './assets/styles/core.scss'
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
registerPlugins(app)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
3
src/mixins/case-converter.ts
Normal file
3
src/mixins/case-converter.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function camelToSnake(str: string): string {
|
||||||
|
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||||
|
}
|
||||||
12
src/mixins/format.ts
Normal file
12
src/mixins/format.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export function secondsToHms(seconds: any): string {
|
||||||
|
const d = Math.floor(seconds / (3600 * 24));
|
||||||
|
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
const dDisplay = d > 0 ? d + 'd ' : '';
|
||||||
|
const hDisplay = h > 0 ? h + 'h ' : '';
|
||||||
|
const mDisplay = m > 0 ? m + 'm ' : '';
|
||||||
|
const sDisplay = s > 0 ? s + 's ' : '';
|
||||||
|
return dDisplay + hDisplay + mDisplay + sDisplay;
|
||||||
|
}
|
||||||
5
src/pages/README.md
Normal file
5
src/pages/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Pages
|
||||||
|
|
||||||
|
Vue components created in this folder will automatically be converted to navigatable routes.
|
||||||
|
|
||||||
|
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.
|
||||||
100
src/pages/auth.vue
Normal file
100
src/pages/auth.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { type Ref, ref, computed } from 'vue'
|
||||||
|
import ErrorPlate from '../components/ErrorPlate.vue'
|
||||||
|
import LogoComponent from '../components/Icons/LogoComponent.vue'
|
||||||
|
import api from '../api/index'
|
||||||
|
import { useLocale, useTheme } from 'vuetify';
|
||||||
|
|
||||||
|
type AuthForm = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useLocale()
|
||||||
|
const theme = useTheme()
|
||||||
|
const currentTheme = computed(() => theme.global.name.value)
|
||||||
|
|
||||||
|
const err: Ref<string> = ref('')
|
||||||
|
const form: Ref<AuthForm> = ref({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const doAuth = async () => {
|
||||||
|
const [_, e] = await api.auth.login(form.value.email, form.value.password)
|
||||||
|
if (e && typeof e === 'string') {
|
||||||
|
err.value = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex relative flex-col items-center justify-center h-full">
|
||||||
|
<!-- Card -->
|
||||||
|
<v-card
|
||||||
|
width="800px"
|
||||||
|
class="p-12 rounded-3xl"
|
||||||
|
elevation="0"
|
||||||
|
>
|
||||||
|
<!-- Logo -->
|
||||||
|
<LogoComponent :theme="currentTheme" />
|
||||||
|
<div class="grid grid-cols-5 mt-8">
|
||||||
|
<!-- Left side -->
|
||||||
|
<div class="items-start flex flex-col col-span-2">
|
||||||
|
<span class="text-3xl font-medium">{{ t('$vuetify.auth.title') }}</span>
|
||||||
|
<!-- Copyright -->
|
||||||
|
<span class="text-sm mt-2 opacity-75">{{ t('$vuetify.auth.copyright') }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- Right side -->
|
||||||
|
<div class="flex flex-col col-span-3">
|
||||||
|
<!-- Login -->
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.email"
|
||||||
|
:label="t('$vuetify.auth.email')"
|
||||||
|
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||||
|
flat
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
<!-- <template v-slot:prepend-inner>
|
||||||
|
<span class="mso mr-1 text-2xl">email</span>
|
||||||
|
</template> -->
|
||||||
|
</v-text-field>
|
||||||
|
<!-- Password -->
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.password"
|
||||||
|
:label="t('$vuetify.auth.password')"
|
||||||
|
:variant="currentTheme === 'dark' ? 'solo-filled' : 'outlined'"
|
||||||
|
hide-details
|
||||||
|
flat
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<!-- <template v-slot:prepend-inner>
|
||||||
|
<span class="mso mr-1 text-2xl">password</span>
|
||||||
|
</template> -->
|
||||||
|
</v-text-field>
|
||||||
|
<!-- Error -->
|
||||||
|
<ErrorPlate :error="err" no-border />
|
||||||
|
<!-- Help -->
|
||||||
|
<span class="mt-5">{{ t('$vuetify.auth.help') }}<a href="#" class="text-primary font-bold">{{ t('$vuetify.auth.help-link') }}</a></span>
|
||||||
|
<!-- Buttons -->
|
||||||
|
<v-card-actions class="flex justify-end px-0 mt-8">
|
||||||
|
<v-btn
|
||||||
|
:variant="currentTheme === 'dark' ? 'flat' : 'flat'"
|
||||||
|
color="primary"
|
||||||
|
class="px-8 rounded-3xl"
|
||||||
|
@click="doAuth"
|
||||||
|
>
|
||||||
|
<i class="mi-log-in"/>
|
||||||
|
<span>{{ t('$vuetify.auth.login') }}</span>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <route lang="yaml">
|
||||||
|
meta:
|
||||||
|
layout: centered
|
||||||
|
</route> -->
|
||||||
7
src/pages/index.vue
Normal file
7
src/pages/index.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<HelloWorld />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
44
src/pages/users/components/AddUser.vue
Normal file
44
src/pages/users/components/AddUser.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, type Ref } from 'vue';
|
||||||
|
import { useLocale, useTheme } from 'vuetify';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
}>()
|
||||||
|
defineEmits(['close'])
|
||||||
|
|
||||||
|
const { t } = useLocale()
|
||||||
|
const theme = useTheme()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog persistent :model-value="modelValue" width="450">
|
||||||
|
<v-card
|
||||||
|
text="When using the activator slot, you must bind the slot props to the activator element."
|
||||||
|
title="Slot Activator"
|
||||||
|
>
|
||||||
|
<template v-slot:title>
|
||||||
|
<v-card-title class="p-0 w-full flex items-center">
|
||||||
|
<span class="mso mr-1 text-2xl">person</span>
|
||||||
|
<span>{{ t('$vuetify.users.creation.create') }}</span>
|
||||||
|
</v-card-title>
|
||||||
|
</template>
|
||||||
|
<v-card-actions class="flex justify-end p-5 gap-1">
|
||||||
|
<!-- Cancel -->
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
class="px-4"
|
||||||
|
:text="t('$vuetify.actions.cancel')"
|
||||||
|
@click="$emit('close')"
|
||||||
|
></v-btn>
|
||||||
|
<!-- Confirm -->
|
||||||
|
<v-btn
|
||||||
|
variant="flat"
|
||||||
|
color="primary"
|
||||||
|
class="px-4"
|
||||||
|
:text="t('$vuetify.actions.confirm')"
|
||||||
|
></v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
198
src/pages/users/components/GroupsTab.vue
Normal file
198
src/pages/users/components/GroupsTab.vue
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
onBeforeMount,
|
||||||
|
type Ref,
|
||||||
|
type ComputedRef,
|
||||||
|
} from 'vue';
|
||||||
|
import { useLocale } from 'vuetify';
|
||||||
|
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||||
|
import { useGroupsStore } from '@/stores/groups';
|
||||||
|
import { UserActions } from '@/enums/user-actions.enum';
|
||||||
|
import Moment from '@/plugins/moment';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tableHeight: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const $groups = useGroupsStore();
|
||||||
|
|
||||||
|
const headers: ComputedRef<any[]> = computed(() => [
|
||||||
|
{ title: t('$vuetify.users.heads.id'), key: 'ID' },
|
||||||
|
{ title: t('$vuetify.users.heads.email'), key: 'email' },
|
||||||
|
{ title: t('$vuetify.users.heads.group'), key: 'group' },
|
||||||
|
{ title: t('$vuetify.users.heads.real-name'), key: 'realName' },
|
||||||
|
{ title: t('$vuetify.users.heads.password'), key: 'isRequiredToSetPassword' },
|
||||||
|
{ title: t('$vuetify.users.heads.status'), key: 'isActive' },
|
||||||
|
{ title: t('$vuetify.users.heads.last-visit'), key: 'lastLogin' },
|
||||||
|
{ title: t('$vuetify.users.heads.created-at'), key: 'createdAt' },
|
||||||
|
{
|
||||||
|
title: t('$vuetify.misc.actions'),
|
||||||
|
key: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const actions = computed(() => [
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.edit'),
|
||||||
|
value: UserActions.EDIT,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.block'),
|
||||||
|
value: UserActions.BLOCK,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.reset-password'),
|
||||||
|
value: UserActions.RESET_PASSWORD,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.unblock'),
|
||||||
|
value: UserActions.UNBLOCK,
|
||||||
|
blocked: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedGroups: Ref<number[]> = ref([]);
|
||||||
|
|
||||||
|
onBeforeMount($groups.getAll);
|
||||||
|
watch(() => [$groups.page, $groups.perPage, $groups.orderBy], $groups.getAll);
|
||||||
|
watch(selectedGroups, (n, o) => console.log(n, o));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-data-table-server
|
||||||
|
v-model="selectedGroups"
|
||||||
|
:search="$groups.search"
|
||||||
|
:header-props="{ nowrap: true }"
|
||||||
|
:headers="headers"
|
||||||
|
:items="$groups.all"
|
||||||
|
:items-per-page-options="ITEMS_PER_PAGE"
|
||||||
|
:items-length="$groups.cursor?.totalRows"
|
||||||
|
:loading="$groups.loading"
|
||||||
|
:height="tableHeight - 62"
|
||||||
|
fixed-footer
|
||||||
|
fixed-header
|
||||||
|
show-select
|
||||||
|
density="compact"
|
||||||
|
style="--fixed: 10"
|
||||||
|
@update:sort-by="$groups.setSort"
|
||||||
|
@update:page="$groups.setPage"
|
||||||
|
@update:itemsPerPage="$groups.setPerPage"
|
||||||
|
>
|
||||||
|
<!-- Fields -->
|
||||||
|
<!-- Group -->
|
||||||
|
<template v-slot:item.group="{ item }">
|
||||||
|
<template v-if="item.group">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-nowrap">{{ item.group.name }}</span>
|
||||||
|
<!-- Group users -->
|
||||||
|
<v-tooltip
|
||||||
|
:text="t('$vuetify.users.all')"
|
||||||
|
:close-delay="0"
|
||||||
|
:open-delay="0"
|
||||||
|
transition="no"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
<span class="mso text-base">group</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-nowrap">—</span>
|
||||||
|
</template>
|
||||||
|
<!-- Real name -->
|
||||||
|
<template v-slot:item.realName="{ item }">
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{ item.realName?.length ? item.realName : '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Required to set password -->
|
||||||
|
<template v-slot:item.isRequiredToSetPassword="{ item }">
|
||||||
|
<span
|
||||||
|
:class="!item.isRequiredToSetPassword ? '' : 'text-error'"
|
||||||
|
class="text-nowrap"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
item.isRequiredToSetPassword
|
||||||
|
? t('$vuetify.users.statuses.unset')
|
||||||
|
: t('$vuetify.users.statuses.set')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- User status -->
|
||||||
|
<template v-slot:item.isActive="{ item }">
|
||||||
|
<span :class="item.isActive ? '' : 'text-error'">
|
||||||
|
{{
|
||||||
|
item.isActive
|
||||||
|
? t('$vuetify.users.statuses.active')
|
||||||
|
: t('$vuetify.users.statuses.blocked')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Last visit -->
|
||||||
|
<template v-slot:item.lastLogin="{ item }">
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{ Moment.$moment(item.lastLogin).format('DD MMM YYYY HH:mm') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Created at -->
|
||||||
|
<template v-slot:item.createdAt="{ item }">
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{ Moment.$moment(item.createdAt).format('DD MMM YYYY HH:mm') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Actions -->
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-menu transition="slide-y-transition">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
density="comfortable"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<span class="mso text-xl">more_vert</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list class="mt-2 accent-modal" elevation="0" density="compact">
|
||||||
|
<template v-for="(act, index) of actions">
|
||||||
|
<v-list-item
|
||||||
|
v-if="
|
||||||
|
(act?.blocked && !item.isActive) ||
|
||||||
|
(!act?.blocked && item.isActive)
|
||||||
|
"
|
||||||
|
:key="index"
|
||||||
|
:value="act.value"
|
||||||
|
lines="one"
|
||||||
|
class="cm-mini"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ act.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
<!-- Delete -->
|
||||||
|
<v-btn icon size="small" variant="text" density="comfortable">
|
||||||
|
<span class="mso text-xl">close</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table-server>
|
||||||
|
</template>
|
||||||
198
src/pages/users/components/UsersTab.vue
Normal file
198
src/pages/users/components/UsersTab.vue
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
onBeforeMount,
|
||||||
|
type Ref,
|
||||||
|
type ComputedRef,
|
||||||
|
} from 'vue';
|
||||||
|
import { useLocale } from 'vuetify';
|
||||||
|
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||||
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
import { UserActions } from '@/enums/user-actions.enum';
|
||||||
|
import Moment from '@/plugins/moment';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tableHeight: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const $users = useUsersStore();
|
||||||
|
|
||||||
|
const headers: ComputedRef<any[]> = computed(() => [
|
||||||
|
{ title: t('$vuetify.users.heads.id'), key: 'ID' },
|
||||||
|
{ title: t('$vuetify.users.heads.email'), key: 'email' },
|
||||||
|
{ title: t('$vuetify.users.heads.group'), key: 'group' },
|
||||||
|
{ title: t('$vuetify.users.heads.real-name'), key: 'realName' },
|
||||||
|
{ title: t('$vuetify.users.heads.password'), key: 'isRequiredToSetPassword' },
|
||||||
|
{ title: t('$vuetify.users.heads.status'), key: 'isActive' },
|
||||||
|
{ title: t('$vuetify.users.heads.last-visit'), key: 'lastLogin' },
|
||||||
|
{ title: t('$vuetify.users.heads.created-at'), key: 'createdAt' },
|
||||||
|
{
|
||||||
|
title: t('$vuetify.misc.actions'),
|
||||||
|
key: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const actions = computed(() => [
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.edit'),
|
||||||
|
value: UserActions.EDIT,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.block'),
|
||||||
|
value: UserActions.BLOCK,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.reset-password'),
|
||||||
|
value: UserActions.RESET_PASSWORD,
|
||||||
|
blocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('$vuetify.actions.unblock'),
|
||||||
|
value: UserActions.UNBLOCK,
|
||||||
|
blocked: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedUsers: Ref<number[]> = ref([]);
|
||||||
|
|
||||||
|
onBeforeMount($users.getAll);
|
||||||
|
watch(() => [$users.page, $users.perPage, $users.orderBy], $users.getAll);
|
||||||
|
watch(selectedUsers, (n, o) => console.log(n, o));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-data-table-server
|
||||||
|
v-model="selectedUsers"
|
||||||
|
:search="$users.search"
|
||||||
|
:header-props="{ nowrap: true }"
|
||||||
|
:headers="headers"
|
||||||
|
:items="$users.all"
|
||||||
|
:items-per-page-options="ITEMS_PER_PAGE"
|
||||||
|
:items-length="$users.cursor?.totalRows"
|
||||||
|
:loading="$users.loading"
|
||||||
|
:height="tableHeight - 62"
|
||||||
|
fixed-footer
|
||||||
|
fixed-header
|
||||||
|
show-select
|
||||||
|
density="compact"
|
||||||
|
style="--fixed: 10"
|
||||||
|
@update:sort-by="$users.setSort"
|
||||||
|
@update:page="$users.setPage"
|
||||||
|
@update:itemsPerPage="$users.setPerPage"
|
||||||
|
>
|
||||||
|
<!-- Fields -->
|
||||||
|
<!-- Group -->
|
||||||
|
<template v-slot:item.group="{ item }">
|
||||||
|
<template v-if="item.group">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-nowrap">{{ item.group.name }}</span>
|
||||||
|
<!-- Group users -->
|
||||||
|
<v-tooltip
|
||||||
|
:text="t('$vuetify.users.all')"
|
||||||
|
:close-delay="0"
|
||||||
|
:open-delay="0"
|
||||||
|
transition="no"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
<span class="mso text-base">group</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-nowrap">—</span>
|
||||||
|
</template>
|
||||||
|
<!-- Real name -->
|
||||||
|
<template v-slot:item.realName="{ item }">
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{ item.realName?.length ? item.realName : '—' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Required to set password -->
|
||||||
|
<template v-slot:item.isRequiredToSetPassword="{ item }">
|
||||||
|
<span
|
||||||
|
:class="!item.isRequiredToSetPassword ? '' : 'text-error'"
|
||||||
|
class="text-nowrap"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
item.isRequiredToSetPassword
|
||||||
|
? t('$vuetify.users.statuses.unset')
|
||||||
|
: t('$vuetify.users.statuses.set')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- User status -->
|
||||||
|
<template v-slot:item.isActive="{ item }">
|
||||||
|
<span :class="item.isActive ? '' : 'text-error'">
|
||||||
|
{{
|
||||||
|
item.isActive
|
||||||
|
? t('$vuetify.users.statuses.active')
|
||||||
|
: t('$vuetify.users.statuses.blocked')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Last visit -->
|
||||||
|
<template v-slot:item.lastLogin="{ item }">
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{ Moment.$moment(item.lastLogin).format('DD MMM YYYY HH:mm') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Created at -->
|
||||||
|
<template v-slot:item.createdAt="{ item }">
|
||||||
|
<span class="text-nowrap">
|
||||||
|
{{ Moment.$moment(item.createdAt).format('DD MMM YYYY HH:mm') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Actions -->
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-menu transition="slide-y-transition">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
density="comfortable"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<span class="mso text-xl">more_vert</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list class="mt-2 accent-modal" elevation="0" density="compact">
|
||||||
|
<template v-for="(act, index) of actions">
|
||||||
|
<v-list-item
|
||||||
|
v-if="
|
||||||
|
(act?.blocked && !item.isActive) ||
|
||||||
|
(!act?.blocked && item.isActive)
|
||||||
|
"
|
||||||
|
:key="index"
|
||||||
|
:value="act.value"
|
||||||
|
lines="one"
|
||||||
|
class="cm-mini"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ act.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
<!-- Delete -->
|
||||||
|
<v-btn icon size="small" variant="text" density="comfortable">
|
||||||
|
<span class="mso text-xl">close</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table-server>
|
||||||
|
</template>
|
||||||
117
src/pages/users/index.vue
Normal file
117
src/pages/users/index.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, Ref } from 'vue';
|
||||||
|
import { useLocale } from 'vuetify';
|
||||||
|
import { UsersPageTabs } from '@/enums/user-tab.enum';
|
||||||
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
import { useGroupsStore } from '@/stores/groups';
|
||||||
|
import ErrorPlate from '@/components/ErrorPlate.vue';
|
||||||
|
import AddUser from './components/AddUser.vue';
|
||||||
|
import UsersTab from './components/UsersTab.vue';
|
||||||
|
import GroupsTab from './components/GroupsTab.vue';
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const $users = useUsersStore();
|
||||||
|
const $groups = useGroupsStore();
|
||||||
|
|
||||||
|
const tableHeight: Ref<number> = ref(500);
|
||||||
|
setInterval(() => {
|
||||||
|
const wY = window.innerHeight;
|
||||||
|
const el = document.getElementById('table');
|
||||||
|
if (el) {
|
||||||
|
const bb = el.getBoundingClientRect();
|
||||||
|
tableHeight.value = wY - bb.top;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const tab: Ref<UsersPageTabs> = ref(UsersPageTabs.USERS);
|
||||||
|
|
||||||
|
const userModal: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
|
const getAll = () => {
|
||||||
|
tab.value === UsersPageTabs.USERS ? $users.getAll() : $groups.getAll();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<v-app-bar prominent class="elevation-0 border-b">
|
||||||
|
<!-- Title -->
|
||||||
|
<span class="text-h6 ml-5 mr-10">{{ t('$vuetify.users.title') }}</span>
|
||||||
|
|
||||||
|
<!-- Create user -->
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="text"
|
||||||
|
class="mr-3"
|
||||||
|
@click="userModal = true"
|
||||||
|
>
|
||||||
|
<span class="mso mr-1 text-xl">person_add</span>
|
||||||
|
<span>{{ t('$vuetify.actions.create-user') }}</span>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<AddUser v-model="userModal" @close="userModal = false" />
|
||||||
|
|
||||||
|
<!-- Refresh users -->
|
||||||
|
<v-btn color="primary" variant="text" @click="getAll()">
|
||||||
|
<span class="mso mr-1 text-xl">refresh</span>
|
||||||
|
<span>{{ t('$vuetify.actions.refresh') }}</span>
|
||||||
|
</v-btn>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<v-tabs
|
||||||
|
v-model="tab"
|
||||||
|
color="primary"
|
||||||
|
class="border-b"
|
||||||
|
density="comfortable"
|
||||||
|
bg-color="surface"
|
||||||
|
>
|
||||||
|
<v-tab :value="UsersPageTabs.USERS">{{
|
||||||
|
t('$vuetify.users.tabs.users')
|
||||||
|
}}</v-tab>
|
||||||
|
<v-tab :value="UsersPageTabs.GROUPS">{{
|
||||||
|
t('$vuetify.users.tabs.groups')
|
||||||
|
}}</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<v-card-title class="p-0 w-full">
|
||||||
|
<v-text-field
|
||||||
|
v-model="$users.search"
|
||||||
|
density="compact"
|
||||||
|
:label="t('$vuetify.users.search')"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
variant="solo-filled"
|
||||||
|
class="border-b"
|
||||||
|
:rounded="false"
|
||||||
|
flat
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
></v-text-field>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<ErrorPlate :error="$users.error" block />
|
||||||
|
|
||||||
|
<div class="h-0" id="table"></div>
|
||||||
|
|
||||||
|
<v-tabs-window v-model="tab">
|
||||||
|
<!-- USERS -->
|
||||||
|
<v-tabs-window-item :value="UsersPageTabs.USERS">
|
||||||
|
<!-- Data -->
|
||||||
|
<v-card flat rounded="0" :height="`${tableHeight}px`">
|
||||||
|
<users-tab :tableHeight="tableHeight" />
|
||||||
|
</v-card>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<!-- GROUPS -->
|
||||||
|
<v-tabs-window-item :value="UsersPageTabs.GROUPS">
|
||||||
|
<!-- Data -->
|
||||||
|
<v-card flat rounded="0" :height="`${tableHeight}px`">
|
||||||
|
<groups-tab :tableHeight="tableHeight" />
|
||||||
|
</v-card>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
</v-tabs-window>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
3
src/plugins/README.md
Normal file
3
src/plugins/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Plugins
|
||||||
|
|
||||||
|
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
||||||
12
src/plugins/axios.ts
Normal file
12
src/plugins/axios.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import axios, { Axios } from 'axios';
|
||||||
|
import type { App } from 'vue';
|
||||||
|
|
||||||
|
export const $axios: Axios = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: (app: App) => {
|
||||||
|
app.config.globalProperties.$axios = $axios;
|
||||||
|
},
|
||||||
|
};
|
||||||
43
src/plugins/cookie.ts
Normal file
43
src/plugins/cookie.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { ref, type Ref, type App } from 'vue';
|
||||||
|
|
||||||
|
class Cookie {
|
||||||
|
private items: Ref<Map<any, any>> = ref(new Map());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const stringified: string = document.cookie;
|
||||||
|
if (stringified?.length) {
|
||||||
|
const splitted = stringified.split('; ')
|
||||||
|
for (const cookie of splitted) {
|
||||||
|
const [key, value] = cookie.split('=')
|
||||||
|
this.items.value.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(key: string): boolean {
|
||||||
|
return this.items.value.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): any {
|
||||||
|
return this.items.value.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(key: string, value: any): void {
|
||||||
|
this.items.value.set(key, value);
|
||||||
|
document.cookie = `${key}=${value}; Secure`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(key: string): void {
|
||||||
|
this.items.value.delete(key);
|
||||||
|
document.cookie =
|
||||||
|
`${key}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $cookie: Cookie = new Cookie()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: (app: App) => {
|
||||||
|
app.config.globalProperties.$cookie = $cookie;
|
||||||
|
},
|
||||||
|
};
|
||||||
20
src/plugins/index.ts
Normal file
20
src/plugins/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* plugins/index.ts
|
||||||
|
*
|
||||||
|
* Automatically included in `./src/main.ts`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import vuetify from './vuetify'
|
||||||
|
import pinia from '../stores'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import type { App } from 'vue'
|
||||||
|
|
||||||
|
export function registerPlugins(app: App) {
|
||||||
|
app
|
||||||
|
.use(vuetify)
|
||||||
|
.use(router)
|
||||||
|
.use(pinia)
|
||||||
|
}
|
||||||
9
src/plugins/moment.ts
Normal file
9
src/plugins/moment.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import moment from 'moment/min/moment-with-locales';
|
||||||
|
|
||||||
|
class Moment {
|
||||||
|
public $moment = moment
|
||||||
|
public locale(locale: string) {
|
||||||
|
this.$moment.locale(locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new Moment()
|
||||||
38
src/plugins/vuetify.ts
Normal file
38
src/plugins/vuetify.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* plugins/vuetify.ts
|
||||||
|
*
|
||||||
|
* Framework documentation: https://vuetifyjs.com`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
import 'vuetify/styles'
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
|
||||||
|
// Locales
|
||||||
|
import { ru, en } from 'vuetify/locale'
|
||||||
|
import ruLocal from '../locales/ru'
|
||||||
|
import enLocal from '../locales/en'
|
||||||
|
|
||||||
|
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||||
|
export default createVuetify({
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'dark',
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
locale: 'ru',
|
||||||
|
fallback: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
...en,
|
||||||
|
...enLocal,
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
...ru,
|
||||||
|
...ruLocal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
16
src/router/index.ts
Normal file
16
src/router/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* router/index.ts
|
||||||
|
*
|
||||||
|
* Automatic routes for `./src/pages/*.vue`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||||
|
import { setupLayouts } from 'virtual:generated-layouts'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
extendRoutes: setupLayouts,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
5
src/stores/README.md
Normal file
5
src/stores/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Store
|
||||||
|
|
||||||
|
Pinia stores are used to store reactive state and expose actions to mutate it.
|
||||||
|
|
||||||
|
Full documentation for this feature can be found in the Official [Pinia](https://pinia.esm.dev/) repository.
|
||||||
8
src/stores/app.ts
Normal file
8
src/stores/app.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Utilities
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', {
|
||||||
|
state: () => ({
|
||||||
|
//
|
||||||
|
}),
|
||||||
|
})
|
||||||
72
src/stores/groups.ts
Normal file
72
src/stores/groups.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, type Ref } from 'vue';
|
||||||
|
import { User } from '@/types/user';
|
||||||
|
import { Cursor } from '@/types/cursor';
|
||||||
|
import { OrderBy } from '@/types/order-by';
|
||||||
|
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||||
|
import { defaultCursor, defaultSort } from '@/constants/defaults';
|
||||||
|
import api from '@/api';
|
||||||
|
|
||||||
|
export const useGroupsStore = defineStore('groups', () => {
|
||||||
|
const all: Ref<User[]> = ref([]);
|
||||||
|
const cursor: Ref<Cursor> = ref(JSON.parse(defaultCursor));
|
||||||
|
|
||||||
|
const search: Ref<string> = ref('');
|
||||||
|
const page: Ref<number> = ref(1);
|
||||||
|
const perPage: Ref<number> = ref(ITEMS_PER_PAGE[0]);
|
||||||
|
const orderBy: Ref<OrderBy> = ref(JSON.parse(defaultSort));
|
||||||
|
|
||||||
|
const loading: Ref<boolean> = ref(false);
|
||||||
|
const error: Ref<string> = ref('');
|
||||||
|
|
||||||
|
const getAll = async () => {
|
||||||
|
if (loading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const [res, e] = await api.groups.getAll(
|
||||||
|
perPage.value,
|
||||||
|
page.value,
|
||||||
|
orderBy.value,
|
||||||
|
);
|
||||||
|
if (e && typeof e === 'string') {
|
||||||
|
error.value = e;
|
||||||
|
} else {
|
||||||
|
error.value = '';
|
||||||
|
all.value = res?.data?.data;
|
||||||
|
cursor.value = res?.data?.cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPage = (v: number) => (page.value = v);
|
||||||
|
const setPerPage = (v: number) => (perPage.value = v);
|
||||||
|
const setSort = (v: OrderBy[]) => {
|
||||||
|
if (!v || !v.length) {
|
||||||
|
orderBy.value = JSON.parse(defaultSort);
|
||||||
|
} else {
|
||||||
|
orderBy.value = v[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
all,
|
||||||
|
cursor,
|
||||||
|
|
||||||
|
search,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
orderBy,
|
||||||
|
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
getAll,
|
||||||
|
setPage,
|
||||||
|
setPerPage,
|
||||||
|
setSort,
|
||||||
|
};
|
||||||
|
});
|
||||||
4
src/stores/index.ts
Normal file
4
src/stores/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Utilities
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
export default createPinia()
|
||||||
72
src/stores/users.ts
Normal file
72
src/stores/users.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, type Ref } from 'vue';
|
||||||
|
import { User } from '@/types/user';
|
||||||
|
import { Cursor } from '@/types/cursor';
|
||||||
|
import { OrderBy } from '@/types/order-by';
|
||||||
|
import { ITEMS_PER_PAGE } from '@/constants/static';
|
||||||
|
import { defaultCursor, defaultSort } from '@/constants/defaults';
|
||||||
|
import api from '@/api';
|
||||||
|
|
||||||
|
export const useUsersStore = defineStore('users', () => {
|
||||||
|
const all: Ref<User[]> = ref([]);
|
||||||
|
const cursor: Ref<Cursor> = ref(JSON.parse(defaultCursor));
|
||||||
|
|
||||||
|
const search: Ref<string> = ref('');
|
||||||
|
const page: Ref<number> = ref(1);
|
||||||
|
const perPage: Ref<number> = ref(ITEMS_PER_PAGE[0]);
|
||||||
|
const orderBy: Ref<OrderBy> = ref(JSON.parse(defaultSort));
|
||||||
|
|
||||||
|
const loading: Ref<boolean> = ref(false);
|
||||||
|
const error: Ref<string> = ref('');
|
||||||
|
|
||||||
|
const getAll = async () => {
|
||||||
|
if (loading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const [res, e] = await api.users.getAll(
|
||||||
|
perPage.value,
|
||||||
|
page.value,
|
||||||
|
orderBy.value,
|
||||||
|
);
|
||||||
|
if (e && typeof e === 'string') {
|
||||||
|
error.value = e;
|
||||||
|
} else {
|
||||||
|
error.value = '';
|
||||||
|
all.value = res?.data?.data;
|
||||||
|
cursor.value = res?.data?.cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPage = (v: number) => (page.value = v);
|
||||||
|
const setPerPage = (v: number) => (perPage.value = v);
|
||||||
|
const setSort = (v: OrderBy[]) => {
|
||||||
|
if (!v || !v.length) {
|
||||||
|
orderBy.value = JSON.parse(defaultSort);
|
||||||
|
} else {
|
||||||
|
orderBy.value = v[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
all,
|
||||||
|
cursor,
|
||||||
|
|
||||||
|
search,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
orderBy,
|
||||||
|
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
getAll,
|
||||||
|
setPage,
|
||||||
|
setPerPage,
|
||||||
|
setSort,
|
||||||
|
};
|
||||||
|
});
|
||||||
28
src/typed-router.d.ts
vendored
Normal file
28
src/typed-router.d.ts
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||||
|
// It's recommended to commit this file.
|
||||||
|
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||||
|
|
||||||
|
declare module 'vue-router/auto-routes' {
|
||||||
|
import type {
|
||||||
|
RouteRecordInfo,
|
||||||
|
ParamValue,
|
||||||
|
ParamValueOneOrMore,
|
||||||
|
ParamValueZeroOrMore,
|
||||||
|
ParamValueZeroOrOne,
|
||||||
|
} from 'unplugin-vue-router/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route name map generated by unplugin-vue-router
|
||||||
|
*/
|
||||||
|
export interface RouteNamedMap {
|
||||||
|
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||||
|
'/auth': RouteRecordInfo<'/auth', '/auth', Record<never, never>, Record<never, never>>,
|
||||||
|
'/users/': RouteRecordInfo<'/users/', '/users', Record<never, never>, Record<never, never>>,
|
||||||
|
'/users/components/AddUser': RouteRecordInfo<'/users/components/AddUser', '/users/components/AddUser', Record<never, never>, Record<never, never>>,
|
||||||
|
'/users/components/GroupsTab': RouteRecordInfo<'/users/components/GroupsTab', '/users/components/GroupsTab', Record<never, never>, Record<never, never>>,
|
||||||
|
'/users/components/UsersTab': RouteRecordInfo<'/users/components/UsersTab', '/users/components/UsersTab', Record<never, never>, Record<never, never>>,
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/types/cursor.d.ts
vendored
Normal file
6
src/types/cursor.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface Cursor {
|
||||||
|
count: number;
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalRows: number;
|
||||||
|
}
|
||||||
4
src/types/group-permission.d.ts
vendored
Normal file
4
src/types/group-permission.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface GroupPermission {
|
||||||
|
groupID: number;
|
||||||
|
value: number
|
||||||
|
}
|
||||||
14
src/types/group.d.ts
vendored
Normal file
14
src/types/group.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { GroupPermission } from "./group-permission";
|
||||||
|
import { User } from "./user";
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
ID: number;
|
||||||
|
issuerID: number;
|
||||||
|
issuer: User;
|
||||||
|
name: string;
|
||||||
|
users: User[];
|
||||||
|
permissions: GroupPermission[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
deletedAt: Date;
|
||||||
|
}
|
||||||
4
src/types/order-by.d.ts
vendored
Normal file
4
src/types/order-by.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface OrderBy {
|
||||||
|
key: string;
|
||||||
|
order?: boolean | 'asc' | 'desc';
|
||||||
|
}
|
||||||
17
src/types/user.d.ts
vendored
Normal file
17
src/types/user.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Group } from "./group";
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
ID: number;
|
||||||
|
email: string;
|
||||||
|
passwordHash: string;
|
||||||
|
passwordLength: number;
|
||||||
|
realName: string;
|
||||||
|
groupID: number;
|
||||||
|
group: Group;
|
||||||
|
isRequiredToSetPassword: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
lastLogin: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
deletedAt: Date;
|
||||||
|
}
|
||||||
8
src/vite-env.d.ts
vendored
Normal file
8
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
6
src/vue.d.ts
vendored
Normal file
6
src/vue.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const component: DefineComponent<object, object, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
13
tailwind.config.js
Normal file
13
tailwind.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx,vue}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
important: true,
|
||||||
|
}
|
||||||
|
|
||||||
48
tsconfig.json
Normal file
48
tsconfig.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"vite-plugin-vue-layouts/client",
|
||||||
|
"unplugin-vue-router/client"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*.d.ts",
|
||||||
|
"./src/**/*",
|
||||||
|
"./src/**/*.{vue,ts,js}",
|
||||||
|
"./src/**/**/*.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"cypress"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.mts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user