merge web project

This commit is contained in:
Sphereso 2024-10-08 11:44:24 +02:00
parent ec3a84b533
commit 2e32e24f4a
38 changed files with 3481 additions and 0 deletions

4
web/.browserslistrc Normal file
View file

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

5
web/.editorconfig Normal file
View file

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

1
web/.env.example Normal file
View file

@ -0,0 +1 @@
VITE_BACKEND_URL=

24
web/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
components.d.ts
# local env files
.env.local
.env.*.local
.env
# 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?

81
web/README.md Normal file
View 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
web/index.html Normal file
View 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>From TURBOMANN™ & Konsorten</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

32
web/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "web",
"version": "0.0.0",
"scripts": {
"dev": "vite --host",
"build": "vue-tsc --noEmit && vite build",
"buildNoCheck": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@mdi/font": "6.2.95",
"@tanstack/vue-query": "^5.51.0",
"@vueuse/core": "^10.11.0",
"axios": "^1.7.2",
"lucide-vue-next": "^0.402.0",
"minisearch": "^7.0.0",
"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",
"typescript": "^5.4.2",
"unplugin-fonts": "^1.1.1",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.5",
"vite-plugin-vuetify": "^2.0.3",
"vue-tsc": "^2.0.6"
}
}

1473
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

26
web/src/App.vue Normal file
View file

@ -0,0 +1,26 @@
<template>
<v-app>
<v-main>
<div v-if="store.token === null">
<loginPage />
</div>
<div v-else>
<MainPage />
</div>
</v-main>
</v-app>
</template>
<script setup lang="ts">
import MainPage from "./components/MainPage.vue";
import loginPage from "./components/loginPage.vue";
import { store } from "./store";
</script>
<style scoped>
html,
body,
main {
background-color: #e3e3e3;
}
</style>

View file

@ -0,0 +1,16 @@
<svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.56704 17.8622C2.1947 18.2468 1.98546 18.7685 1.98535 19.3125V21.5404C1.98535 21.8125 2.08993 22.0734 2.27609 22.2657C2.46225 22.4581 2.71473 22.5662 2.978 22.5662H5.95594C6.21921 22.5662 6.47169 22.4581 6.65785 22.2657C6.844 22.0734 6.94859 21.8125 6.94859 21.5404V20.5147C6.94859 20.2427 7.05317 19.9818 7.23933 19.7894C7.42548 19.597 7.67797 19.489 7.94123 19.489H8.93388C9.19715 19.489 9.44963 19.3809 9.63579 19.1885C9.82195 18.9962 9.92653 18.7353 9.92653 18.4632V17.4375C9.92653 17.1655 10.0311 16.9046 10.2173 16.7122C10.4034 16.5198 10.6559 16.4118 10.9192 16.4118H11.0899C11.6164 16.4116 12.1213 16.1954 12.4935 15.8107L13.3015 14.9757C14.6811 15.4723 16.183 15.4704 17.5615 14.9703C18.9399 14.4703 20.1133 13.5016 20.8897 12.2228C21.6661 10.944 21.9995 9.43079 21.8354 7.93071C21.6713 6.43064 21.0195 5.03251 19.9864 3.96505C18.9534 2.89758 17.6004 2.22398 16.1487 2.05443C14.697 1.88488 13.2326 2.22943 11.9951 3.0317C10.7575 3.83398 9.82011 5.04648 9.33615 6.47087C8.85219 7.89525 8.85035 9.44719 9.33094 10.8728L2.56704 17.8622Z" fill="white"/>
<path d="M2.56704 17.8622C2.1947 18.2468 1.98546 18.7685 1.98535 19.3125V21.5404C1.98535 21.8125 2.08993 22.0734 2.27609 22.2657C2.46225 22.4581 2.71473 22.5662 2.978 22.5662H5.95594C6.21921 22.5662 6.47169 22.4581 6.65785 22.2657C6.844 22.0734 6.94859 21.8125 6.94859 21.5404V20.5147C6.94859 20.2427 7.05317 19.9818 7.23933 19.7894C7.42548 19.597 7.67797 19.489 7.94123 19.489H8.93388C9.19715 19.489 9.44963 19.3809 9.63579 19.1885C9.82195 18.9962 9.92653 18.7353 9.92653 18.4632V17.4375C9.92653 17.1655 10.0311 16.9046 10.2173 16.7122C10.4034 16.5198 10.6559 16.4118 10.9192 16.4118H11.0899C11.6164 16.4116 12.1213 16.1954 12.4935 15.8107L13.3015 14.9757C14.6811 15.4723 16.183 15.4704 17.5615 14.9703C18.9399 14.4703 20.1133 13.5016 20.8897 12.2228C21.6661 10.944 21.9995 9.43079 21.8354 7.93071C21.6713 6.43064 21.0195 5.03251 19.9864 3.96505C18.9534 2.89758 17.6004 2.22398 16.1487 2.05443C14.697 1.88488 13.2326 2.22943 11.9951 3.0317C10.7575 3.83398 9.82011 5.04648 9.33615 6.47087C8.85219 7.89525 8.85035 9.44719 9.33094 10.8728L2.56704 17.8622Z" fill="white"/>
<path d="M2.56704 17.8622C2.1947 18.2468 1.98546 18.7685 1.98535 19.3125V21.5404C1.98535 21.8125 2.08993 22.0734 2.27609 22.2657C2.46225 22.4581 2.71473 22.5662 2.978 22.5662H5.95594C6.21921 22.5662 6.47169 22.4581 6.65785 22.2657C6.844 22.0734 6.94859 21.8125 6.94859 21.5404V20.5147C6.94859 20.2427 7.05317 19.9818 7.23933 19.7894C7.42548 19.597 7.67797 19.489 7.94123 19.489H8.93388C9.19715 19.489 9.44963 19.3809 9.63579 19.1885C9.82195 18.9962 9.92653 18.7353 9.92653 18.4632V17.4375C9.92653 17.1655 10.0311 16.9046 10.2173 16.7122C10.4034 16.5198 10.6559 16.4118 10.9192 16.4118H11.0899C11.6164 16.4116 12.1213 16.1954 12.4935 15.8107L13.3015 14.9757C14.6811 15.4723 16.183 15.4704 17.5615 14.9703C18.9399 14.4703 20.1133 13.5016 20.8897 12.2228C21.6661 10.944 21.9995 9.43079 21.8354 7.93071C21.6713 6.43064 21.0195 5.03251 19.9864 3.96505C18.9534 2.89758 17.6004 2.22398 16.1487 2.05443C14.697 1.88488 13.2326 2.22943 11.9951 3.0317C10.7575 3.83398 9.82011 5.04648 9.33615 6.47087C8.85219 7.89525 8.85035 9.44719 9.33094 10.8728L2.56704 17.8622Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.56704 17.8622C2.1947 18.2468 1.98546 18.7685 1.98535 19.3125V21.5404C1.98535 21.8125 2.08993 22.0734 2.27609 22.2657C2.46225 22.4581 2.71473 22.5662 2.978 22.5662H5.95594C6.21921 22.5662 6.47169 22.4581 6.65785 22.2657C6.844 22.0734 6.94859 21.8125 6.94859 21.5404V20.5147C6.94859 20.2427 7.05317 19.9818 7.23933 19.7894C7.42548 19.597 7.67797 19.489 7.94123 19.489H8.93388C9.19715 19.489 9.44963 19.3809 9.63579 19.1885C9.82195 18.9962 9.92653 18.7353 9.92653 18.4632V17.4375C9.92653 17.1655 10.0311 16.9046 10.2173 16.7122C10.4034 16.5198 10.6559 16.4118 10.9192 16.4118H11.0899C11.6164 16.4116 12.1213 16.1954 12.4935 15.8107L13.3015 14.9757C14.6811 15.4723 16.183 15.4704 17.5615 14.9703C18.9399 14.4703 20.1133 13.5016 20.8897 12.2228C21.6661 10.944 21.9995 9.43079 21.8354 7.93071C21.6713 6.43064 21.0195 5.03251 19.9864 3.96505C18.9534 2.89758 17.6004 2.22398 16.1487 2.05443C14.697 1.88488 13.2326 2.22943 11.9951 3.0317C10.7575 3.83398 9.82011 5.04648 9.33615 6.47087C8.85219 7.89525 8.85035 9.44719 9.33094 10.8728L2.56704 17.8622Z" fill="white"/>
<path d="M2.56704 17.8622C2.1947 18.2468 1.98546 18.7685 1.98535 19.3125V21.5404C1.98535 21.8125 2.08993 22.0734 2.27609 22.2657C2.46225 22.4581 2.71473 22.5662 2.978 22.5662H5.95594C6.21921 22.5662 6.47169 22.4581 6.65785 22.2657C6.844 22.0734 6.94859 21.8125 6.94859 21.5404V20.5147C6.94859 20.2427 7.05317 19.9818 7.23933 19.7894C7.42548 19.597 7.67797 19.489 7.94123 19.489H8.93388C9.19715 19.489 9.44963 19.3809 9.63579 19.1885C9.82195 18.9962 9.92653 18.7353 9.92653 18.4632V17.4375C9.92653 17.1655 10.0311 16.9046 10.2173 16.7122C10.4034 16.5198 10.6559 16.4118 10.9192 16.4118H11.0899C11.6164 16.4116 12.1213 16.1954 12.4935 15.8107L13.3015 14.9757C14.6811 15.4723 16.183 15.4704 17.5615 14.9703C18.9399 14.4703 20.1133 13.5016 20.8897 12.2228C21.6661 10.944 21.9995 9.43079 21.8354 7.93071C21.6713 6.43064 21.0195 5.03251 19.9864 3.96505C18.9534 2.89758 17.6004 2.22398 16.1487 2.05443C14.697 1.88488 13.2326 2.22943 11.9951 3.0317C10.7575 3.83398 9.82011 5.04648 9.33615 6.47087C8.85219 7.89525 8.85035 9.44719 9.33094 10.8728L2.56704 17.8622Z" fill="white"/>
<path d="M2.56704 17.8622C2.1947 18.2468 1.98546 18.7685 1.98535 19.3125V21.5404C1.98535 21.8125 2.08993 22.0734 2.27609 22.2657C2.46225 22.4581 2.71473 22.5662 2.978 22.5662H5.95594C6.21921 22.5662 6.47169 22.4581 6.65785 22.2657C6.844 22.0734 6.94859 21.8125 6.94859 21.5404V20.5147C6.94859 20.2427 7.05317 19.9818 7.23933 19.7894C7.42548 19.597 7.67797 19.489 7.94123 19.489H8.93388C9.19715 19.489 9.44963 19.3809 9.63579 19.1885C9.82195 18.9962 9.92653 18.7353 9.92653 18.4632V17.4375C9.92653 17.1655 10.0311 16.9046 10.2173 16.7122C10.4034 16.5198 10.6559 16.4118 10.9192 16.4118H11.0899C11.6164 16.4116 12.1213 16.1954 12.4935 15.8107L13.3015 14.9757C14.6811 15.4723 16.183 15.4704 17.5615 14.9703C18.9399 14.4703 20.1133 13.5016 20.8897 12.2228C21.6661 10.944 21.9995 9.43079 21.8354 7.93071C21.6713 6.43064 21.0195 5.03251 19.9864 3.96505C18.9534 2.89758 17.6004 2.22398 16.1487 2.05443C14.697 1.88488 13.2326 2.22943 11.9951 3.0317C10.7575 3.83398 9.82011 5.04648 9.33615 6.47087C8.85219 7.89525 8.85035 9.44719 9.33094 10.8728L2.56704 17.8622Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.3786 8.20588C16.6528 8.20588 16.875 7.97626 16.875 7.69301C16.875 7.40976 16.6528 7.18015 16.3786 7.18015C16.1045 7.18015 15.8823 7.40976 15.8823 7.69301C15.8823 7.97626 16.1045 8.20588 16.3786 8.20588Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.74356 20.2445C5.37122 20.6291 5.16198 21.1509 5.16187 21.6949V23.9228C5.16187 24.1948 5.26645 24.4557 5.4526 24.6481C5.63876 24.8405 5.89125 24.9485 6.15451 24.9485H9.13245C9.39572 24.9485 9.6482 24.8405 9.83436 24.6481C10.0205 24.4557 10.1251 24.1948 10.1251 23.9228V22.8971C10.1251 22.625 10.2297 22.3641 10.4158 22.1718C10.602 21.9794 10.8545 21.8713 11.1177 21.8713H12.1104C12.3737 21.8713 12.6261 21.7633 12.8123 21.5709C12.9985 21.3785 13.103 21.1176 13.103 20.8456V19.8199C13.103 19.5478 13.2076 19.2869 13.3938 19.0945C13.5799 18.9022 13.8324 18.7941 14.0957 18.7941H14.2664C14.7929 18.794 15.2978 18.5778 15.67 18.193L16.478 17.3581C17.8577 17.8547 19.3595 17.8528 20.738 17.3527C22.1164 16.8526 23.2898 15.8839 24.0662 14.6051C24.8426 13.3263 25.176 11.8131 25.0119 10.3131C24.8479 8.813 24.196 7.41487 23.163 6.3474C22.1299 5.27994 20.7769 4.60633 19.3252 4.43679C17.8735 4.26724 16.4091 4.61178 15.1716 5.41406C13.9341 6.21633 12.9966 7.42884 12.5127 8.85322C12.0287 10.2776 12.0269 11.8295 12.5075 13.2551L5.74356 20.2445Z" fill="white"/>
<path d="M5.74356 20.2445C5.37122 20.6291 5.16198 21.1509 5.16187 21.6949V23.9228C5.16187 24.1948 5.26645 24.4557 5.4526 24.6481C5.63876 24.8405 5.89125 24.9485 6.15451 24.9485H9.13245C9.39572 24.9485 9.6482 24.8405 9.83436 24.6481C10.0205 24.4557 10.1251 24.1948 10.1251 23.9228V22.8971C10.1251 22.625 10.2297 22.3641 10.4158 22.1718C10.602 21.9794 10.8545 21.8713 11.1177 21.8713H12.1104C12.3737 21.8713 12.6261 21.7633 12.8123 21.5709C12.9985 21.3785 13.103 21.1176 13.103 20.8456V19.8199C13.103 19.5478 13.2076 19.2869 13.3938 19.0945C13.5799 18.9022 13.8324 18.7941 14.0957 18.7941H14.2664C14.7929 18.794 15.2978 18.5778 15.67 18.193L16.478 17.3581C17.8577 17.8547 19.3595 17.8528 20.738 17.3527C22.1164 16.8526 23.2898 15.8839 24.0662 14.6051C24.8426 13.3263 25.176 11.8131 25.0119 10.3131C24.8479 8.813 24.196 7.41487 23.163 6.3474C22.1299 5.27994 20.7769 4.60633 19.3252 4.43679C17.8735 4.26724 16.4091 4.61178 15.1716 5.41406C13.9341 6.21633 12.9966 7.42884 12.5127 8.85322C12.0287 10.2776 12.0269 11.8295 12.5075 13.2551L5.74356 20.2445Z" fill="white"/>
<path d="M5.74356 20.2445C5.37122 20.6291 5.16198 21.1509 5.16187 21.6949V23.9228C5.16187 24.1948 5.26645 24.4557 5.4526 24.6481C5.63876 24.8405 5.89125 24.9485 6.15451 24.9485H9.13245C9.39572 24.9485 9.6482 24.8405 9.83436 24.6481C10.0205 24.4557 10.1251 24.1948 10.1251 23.9228V22.8971C10.1251 22.625 10.2297 22.3641 10.4158 22.1718C10.602 21.9794 10.8545 21.8713 11.1177 21.8713H12.1104C12.3737 21.8713 12.6261 21.7633 12.8123 21.5709C12.9985 21.3785 13.103 21.1176 13.103 20.8456V19.8199C13.103 19.5478 13.2076 19.2869 13.3938 19.0945C13.5799 18.9022 13.8324 18.7941 14.0957 18.7941H14.2664C14.7929 18.794 15.2978 18.5778 15.67 18.193L16.478 17.3581C17.8577 17.8547 19.3595 17.8528 20.738 17.3527C22.1164 16.8526 23.2898 15.8839 24.0662 14.6051C24.8426 13.3263 25.176 11.8131 25.0119 10.3131C24.8479 8.813 24.196 7.41487 23.163 6.3474C22.1299 5.27994 20.7769 4.60633 19.3252 4.43679C17.8735 4.26724 16.4091 4.61178 15.1716 5.41406C13.9341 6.21633 12.9966 7.42884 12.5127 8.85322C12.0287 10.2776 12.0269 11.8295 12.5075 13.2551L5.74356 20.2445Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.74356 20.2445C5.37122 20.6291 5.16198 21.1509 5.16187 21.6949V23.9228C5.16187 24.1948 5.26645 24.4557 5.4526 24.6481C5.63876 24.8405 5.89125 24.9485 6.15451 24.9485H9.13245C9.39572 24.9485 9.6482 24.8405 9.83436 24.6481C10.0205 24.4557 10.1251 24.1948 10.1251 23.9228V22.8971C10.1251 22.625 10.2297 22.3641 10.4158 22.1718C10.602 21.9794 10.8545 21.8713 11.1177 21.8713H12.1104C12.3737 21.8713 12.6261 21.7633 12.8123 21.5709C12.9985 21.3785 13.103 21.1176 13.103 20.8456V19.8199C13.103 19.5478 13.2076 19.2869 13.3938 19.0945C13.5799 18.9022 13.8324 18.7941 14.0957 18.7941H14.2664C14.7929 18.794 15.2978 18.5778 15.67 18.193L16.478 17.3581C17.8577 17.8547 19.3595 17.8528 20.738 17.3527C22.1164 16.8526 23.2898 15.8839 24.0662 14.6051C24.8426 13.3263 25.176 11.8131 25.0119 10.3131C24.8479 8.813 24.196 7.41487 23.163 6.3474C22.1299 5.27994 20.7769 4.60633 19.3252 4.43679C17.8735 4.26724 16.4091 4.61178 15.1716 5.41406C13.9341 6.21633 12.9966 7.42884 12.5127 8.85322C12.0287 10.2776 12.0269 11.8295 12.5075 13.2551L5.74356 20.2445Z" fill="white"/>
<path d="M5.74356 20.2445C5.37122 20.6291 5.16198 21.1509 5.16187 21.6949V23.9228C5.16187 24.1948 5.26645 24.4557 5.4526 24.6481C5.63876 24.8405 5.89125 24.9485 6.15451 24.9485H9.13245C9.39572 24.9485 9.6482 24.8405 9.83436 24.6481C10.0205 24.4557 10.1251 24.1948 10.1251 23.9228V22.8971C10.1251 22.625 10.2297 22.3641 10.4158 22.1718C10.602 21.9794 10.8545 21.8713 11.1177 21.8713H12.1104C12.3737 21.8713 12.6261 21.7633 12.8123 21.5709C12.9985 21.3785 13.103 21.1176 13.103 20.8456V19.8199C13.103 19.5478 13.2076 19.2869 13.3938 19.0945C13.5799 18.9022 13.8324 18.7941 14.0957 18.7941H14.2664C14.7929 18.794 15.2978 18.5778 15.67 18.193L16.478 17.3581C17.8577 17.8547 19.3595 17.8528 20.738 17.3527C22.1164 16.8526 23.2898 15.8839 24.0662 14.6051C24.8426 13.3263 25.176 11.8131 25.0119 10.3131C24.8479 8.813 24.196 7.41487 23.163 6.3474C22.1299 5.27994 20.7769 4.60633 19.3252 4.43679C17.8735 4.26724 16.4091 4.61178 15.1716 5.41406C13.9341 6.21633 12.9966 7.42884 12.5127 8.85322C12.0287 10.2776 12.0269 11.8295 12.5075 13.2551L5.74356 20.2445Z" fill="white"/>
<path d="M5.74356 20.2445C5.37122 20.6291 5.16198 21.1509 5.16187 21.6949V23.9228C5.16187 24.1948 5.26645 24.4557 5.4526 24.6481C5.63876 24.8405 5.89125 24.9485 6.15451 24.9485H9.13245C9.39572 24.9485 9.6482 24.8405 9.83436 24.6481C10.0205 24.4557 10.1251 24.1948 10.1251 23.9228V22.8971C10.1251 22.625 10.2297 22.3641 10.4158 22.1718C10.602 21.9794 10.8545 21.8713 11.1177 21.8713H12.1104C12.3737 21.8713 12.6261 21.7633 12.8123 21.5709C12.9985 21.3785 13.103 21.1176 13.103 20.8456V19.8199C13.103 19.5478 13.2076 19.2869 13.3938 19.0945C13.5799 18.9022 13.8324 18.7941 14.0957 18.7941H14.2664C14.7929 18.794 15.2978 18.5778 15.67 18.193L16.478 17.3581C17.8577 17.8547 19.3595 17.8528 20.738 17.3527C22.1164 16.8526 23.2898 15.8839 24.0662 14.6051C24.8426 13.3263 25.176 11.8131 25.0119 10.3131C24.8479 8.813 24.196 7.41487 23.163 6.3474C22.1299 5.27994 20.7769 4.60633 19.3252 4.43679C17.8735 4.26724 16.4091 4.61178 15.1716 5.41406C13.9341 6.21633 12.9966 7.42884 12.5127 8.85322C12.0287 10.2776 12.0269 11.8295 12.5075 13.2551L5.74356 20.2445Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.5552 10.5882C19.8293 10.5882 20.0515 10.3586 20.0515 10.0754C20.0515 9.79212 19.8293 9.5625 19.5552 9.5625C19.281 9.5625 19.0588 9.79212 19.0588 10.0754C19.0588 10.3586 19.281 10.5882 19.5552 10.5882Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

BIN
web/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

6
web/src/assets/logo.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View file

@ -0,0 +1,19 @@
<svg width="138" height="63" viewBox="0 0 138 63" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_17_1565)">
<mask id="mask0_17_1565" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="138" height="63">
<path d="M138 0H0V63H138V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_17_1565)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.7102 20.392C14.7102 18.4791 16.4493 16.9284 18.5946 16.9284H57.9444C60.0898 16.9284 61.8288 18.4791 61.8288 20.392C61.8288 22.3049 60.0898 23.8555 57.9444 23.8555H18.5946C16.4493 23.8555 14.7102 22.3049 14.7102 20.392Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.1845 47.6365H55.5218L56.1281 47.691H62.1961V52.6454C62.1961 54.3563 62.573 55.6884 63.3269 56.6414C64.0808 57.5944 65.3211 58.0711 67.0486 58.0711H69.1457V62.8373H65.4447C63.4432 62.8373 61.7093 62.4766 60.2424 61.7556C58.776 61.0346 57.6382 59.9775 56.8292 58.5843C56.4251 57.8879 56.1217 57.1181 55.9194 56.2749L55.7282 54.5407L55.5218 54.5596H16.1845C14.5763 54.5596 13.1965 53.6877 12.607 52.4454L12.302 51.098L12.607 49.7507C13.1965 48.5084 14.5763 47.6365 16.1845 47.6365ZM25.0124 39.0686H26.0568C28.2013 39.0686 29.9394 40.6184 29.9394 42.5297C29.9394 44.4414 28.2013 45.9912 26.0568 45.9912H25.0124C23.4039 45.9912 22.0239 45.1194 21.4346 43.877L21.1298 42.5297L21.4346 41.1824C22.0239 39.9404 23.4039 39.0686 25.0124 39.0686ZM8.55526 16.9277C10.5226 16.9277 12.1174 18.3495 12.1174 20.1034C12.1174 20.2938 12.1174 20.4843 12.1174 20.6747C12.1174 22.4287 10.5226 23.8502 8.55526 23.8502C6.58796 23.8502 4.99315 22.4287 4.99315 20.6747V20.1034C4.99315 18.3495 6.58796 16.9277 8.55526 16.9277ZM45.2118 11.8929H56.5529C57.6024 11.8929 58.4537 12.6516 58.4537 13.5875C58.4537 14.5235 57.6024 15.2822 56.5529 15.2822H45.2118C44.1618 15.2822 43.3109 14.5235 43.3109 13.5875C43.3109 12.6516 44.1618 11.8929 45.2118 11.8929ZM39.6578 11.8929C40.6948 11.8929 41.5351 12.6422 41.5351 13.5666V13.6085C41.5351 14.5329 40.6948 15.2822 39.6578 15.2822C38.6211 15.2822 37.7804 14.5329 37.7804 13.6085V13.5666C37.7804 12.6422 38.6211 11.8929 39.6578 11.8929ZM25.0124 3.18718H27.3596C29.5036 3.18718 31.2422 4.73685 31.2422 6.64849C31.2422 8.56013 29.5036 10.1098 27.3596 10.1098H25.0124C23.4039 10.1098 22.0239 9.23809 21.4346 7.9958L21.1298 6.64849L21.4346 5.30118C22.0239 4.05885 23.4039 3.18718 25.0124 3.18718ZM116.534 0.16289H120.276C122.277 0.16289 124.018 0.51728 125.499 1.22606C126.979 1.93484 128.123 2.97358 128.933 4.34229C129.741 5.71095 130.146 7.40957 130.146 9.43815V19.4833C130.146 21.8787 130.379 23.7789 130.844 25.184C131.311 26.5895 132.113 27.5974 133.25 28.2086C134.388 28.8197 135.971 29.1251 138 29.1251V34.111C135.971 34.111 134.388 34.3983 133.25 34.9726C132.113 35.5468 131.311 36.549 130.844 37.9787C130.379 39.4084 130.146 41.382 130.146 43.8996V53.6881C130.146 55.6679 129.741 57.3542 128.933 58.7474C128.123 60.1406 126.979 61.1973 125.499 61.9183C124.018 62.6393 122.277 63 120.276 63H116.534V58.2342H118.672C120.372 58.2342 121.599 57.7575 122.352 56.8041C123.107 55.8511 123.484 54.519 123.484 52.8081V42.3231C123.484 40.0011 123.751 38.1255 124.285 36.6958C124.82 35.2661 125.574 34.1356 126.547 33.3043C127.52 32.4735 128.638 31.8382 129.899 31.3979C128.665 31.0315 127.562 30.4695 126.589 29.7117C125.615 28.9542 124.854 27.8663 124.306 26.4489C123.758 25.0315 123.484 23.1493 123.484 20.8031V10.3547C123.484 8.66826 123.107 7.33624 122.352 6.35861C121.599 5.38099 120.372 4.89215 118.672 4.89215H116.534V0.16289ZM65.4447 0H69.1457V4.72926H67.0486C65.3211 4.72926 64.0808 5.2181 63.3269 6.19572C62.573 7.17335 62.1961 8.50537 62.1961 10.1918V20.6402C62.1961 22.9866 61.9216 24.8684 61.3732 26.2858C60.8253 27.7036 60.0714 28.791 59.1115 29.5489C58.1521 30.3064 57.042 30.8688 55.7806 31.2352C57.0691 31.675 58.1931 32.3108 59.1529 33.1416C59.6324 33.5572 60.0594 34.0475 60.4327 34.6127L60.9053 35.5567H53.3925L52.4703 34.8099C51.9012 34.5225 51.2246 34.3073 50.4399 34.1635L48.0554 33.9716H2.52818C1.1319 33.9716 0 32.9625 0 31.7181C0 30.4732 1.1319 29.4641 2.52818 29.4641H49.8437L49.9029 29.4747H53.9345C55.8164 29.4747 57.3417 28.1147 57.3417 26.437C57.3417 25.1791 56.4835 24.0994 55.2607 23.6383L55.2515 23.6359L55.4363 22.5419C55.5563 21.5918 55.616 20.518 55.616 19.3204V17.0245H56.6191C58.7635 17.0245 60.5016 15.4748 60.5016 13.5631C60.5016 12.1294 59.5239 10.8993 58.1305 10.3738L57.555 10.2145H37.8466C36.2381 10.2145 34.8586 9.34277 34.2692 8.10048L33.964 6.75317L34.2692 5.40587C34.8586 4.16357 36.2381 3.29186 37.8466 3.29186H57.4442L57.4975 3.21472C58.2275 2.31194 59.1423 1.59476 60.2424 1.06317C61.7093 0.35439 63.4432 0 65.4447 0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.6269 42.526C31.6269 40.6131 33.3659 39.0624 35.5113 39.0624H84.6066C86.752 39.0624 88.491 40.6131 88.491 42.526C88.491 44.439 86.752 45.9896 84.6066 45.9896H35.5113C33.3659 45.9896 31.6269 44.439 31.6269 42.526Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.3314 8.40271H114.188L112.913 13.9391H97.1241L86.6389 57.7534H79.3197L83.7741 39.0661H85.8013C87.3068 39.0661 88.5273 37.9779 88.5273 36.6355C88.5273 35.2931 87.3068 34.2048 85.8013 34.2048H84.933L89.7639 13.9391H74.0566L75.3314 8.40271Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.0679 31.8894C75.0679 30.554 76.2824 29.4711 77.7801 29.4711H86.2385C87.7366 29.4711 88.9507 30.554 88.9507 31.8894C88.9507 33.2248 87.7362 34.3078 86.2385 34.3078H77.7801C76.2824 34.3078 75.0679 33.2248 75.0679 31.8894Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_17_1565">
<rect width="138" height="63" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

11
web/src/client.ts Normal file
View file

@ -0,0 +1,11 @@
import axios from "axios";
import { store } from "@/store";
let axiosInstance = axios.create({
baseURL: import.meta.env.VITE_BACKEND_URL,
});
axiosInstance.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${store.token}`;
return config;
});
export { axiosInstance };

View file

@ -0,0 +1,97 @@
<template>
<div class="mt-3">
<v-row no-gutters>
<v-col cols="11">
<h2>{{ licenseGroup.name }}:</h2>
</v-col>
<v-col>
<v-btn
block
class="ml-7"
flat
size="small"
color="isHovering ? 'red' : undefined"
variant="text"
v-model="deleteDialog"
@click="deleteDialog = true"
>
<Trash />
</v-btn>
<v-dialog v-model="deleteDialog" width="600" persistent>
<v-card max-width="600">
<v-card-title>
<span class="headline">Delete group</span>
</v-card-title>
<v-card-subtitle>
<span>Delete a group inside the database</span>
</v-card-subtitle>
<v-card-text>
<h4>This action is irreversible!</h4>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="8" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="deleteDialog = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Confirm Delete"
type="submit"
color="red darken-1"
@click="deleteMutate"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</v-col>
</v-row>
<li v-for="license in licenseGroup.licenses" :key="license.id">
<ListViewElement :license="license" />
</li>
</div>
</template>
<script setup lang="ts">
import ListViewElement from "./ListViewElement.vue";
import { LicenseGroup } from "@/types";
import { Trash } from "lucide-vue-next";
import { ref } from "vue";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
const { licenseGroup } = defineProps<{ licenseGroup: LicenseGroup }>();
const deleteDialog = ref(false);
const queryClient = useQueryClient();
const id = licenseGroup.id;
const { mutate: deleteMutate } = useMutation({
mutationFn: async () => {
await axiosInstance.delete(`/groups/${id}`);
},
onError: (error) => {
console.log(error);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["licenses"] });
deleteDialog.value = false;
},
});
</script>
<style scoped>
li {
list-style-type: none;
}
</style>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import { useClipboard } from "@vueuse/core";
const source = ref("Hello");
const { text, copy, copied, isSupported } = useClipboard({ source });
</script>
<template>
<div v-if="isSupported">
<button @click="copy(source)">
<!-- by default, `copied` will be reset in 1.5s -->
<span v-if="!copied">Copy</span>
<span v-else>Copied!</span>
</button>
<p>
Current copied: <code>{{ text || "none" }}</code>
</p>
</div>
<p v-else>Your browser does not support Clipboard API</p>
</template>

View file

@ -0,0 +1,81 @@
<template>
<v-btn
class="mr-3"
flat
size="small"
color="red"
variant="text"
@click="deleteDialog = true"
>
<Trash />
</v-btn>
<v-dialog v-model="deleteDialog" width="600" persistent>
<v-card max-width="600">
<v-card-title>
<span class="headline">Delete License</span>
</v-card-title>
<v-card-subtitle>
<span>Delete a License inside the database</span>
</v-card-subtitle>
<v-card-text>
<h4>This action is irreversible!</h4>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="8" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="deleteDialog = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Confirm Delete"
type="submit"
color="red darken-1"
@click="deleteMutate"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { Trash } from "lucide-vue-next";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
import { License } from "@/types";
import { ref } from "vue";
const { license } = defineProps<{
license: License;
}>();
// EDIT SECTION
const deleteDialog = ref(false);
const queryClient = useQueryClient();
const id = license.id;
const { mutate: deleteMutate } = useMutation({
mutationFn: async () => {
await axiosInstance.delete(`/licenses/${id}`);
},
onError: (error) => {
console.log(error);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["licenses"] });
deleteDialog.value = false;
},
});
</script>
<style scoped></style>

View file

@ -0,0 +1,375 @@
<template>
<div>
<v-toolbar color="main" dark prominent>
<img src="../assets/turbologo.svg" alt="logo" class="logo" width="75" />
<v-spacer></v-spacer>
<!-- Users, Groups, Licenses -->
<div>
<!-- USER MANAGEMENT -->
<template v-if="!user" />
<UsersDialog v-else-if="user.admin" />
<UsersEditActions v-else :admin-menu="false" :user="user!" />
<!-- -->
<!-- GROUP SECTION -->
<!-- -->
<v-btn icon class="mr-3" @click="group = true">
<FolderPlus />
</v-btn>
<v-dialog v-model="group" width="600" persistent>
<v-card max-width="600">
<v-form @submit.prevent="submitGroup">
<v-card-title>
<span class="headline">Add Group</span>
</v-card-title>
<v-card-subtitle>
<span> Add a Group to the Database</span>
</v-card-subtitle>
<v-card-text>
<div>
<v-text-field
label="Group Name *"
v-model="groupGroupName"
variant="outlined"
required
clearable
:rules="licenseGroupRules"
class="mb-3"
></v-text-field>
</div>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="10" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
@click="group = false"
color="blue darken-1"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Add"
type="submit"
color="blue darken-1"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
<!-- -->
<!-- ADD SECTION -->
<!-- -->
<v-btn icon class="mr-3" @click="add = true">
<Plus />
</v-btn>
<v-dialog v-model="add" width="600" persistent>
<v-card max-width="600">
<v-form @submit.prevent="submit">
<v-card-title>
<span class="headline">Add License</span>
</v-card-title>
<v-card-subtitle>
<span> Add an extra License to the database</span>
</v-card-subtitle>
<v-card-text>
<div>
<v-text-field
label="License Name *"
v-model="addLicenseName"
variant="outlined"
required
clearable
:rules="licenseNameRules"
class="mb-3"
></v-text-field>
<v-text-field
label="License Key *"
v-model="addLicenseKey"
variant="outlined"
required
clearable
:rules="licenseKeyRules"
class="mb-3"
></v-text-field>
<v-autocomplete
clearable
label="Select Group *"
:items="data"
variant="outlined"
:rules="selectGroupRules"
class="mb-1"
v-model="addLicenseGroup"
></v-autocomplete>
<!-- Divider maybe remove -->
<v-divider
class="border-opacity-50"
:thickness="2"
></v-divider>
<div>
<div class="mb-3 mt-3">
<v-date-input
label="Start Date (optional)"
variant="outlined"
prepend-icon=""
prepend-inner-icon="mdi-calendar-check"
clearable
v-model="addStartDate"
hide-details
@click:clear="addStartDate = undefined"
></v-date-input>
</div>
<div class="mb-6">
<v-date-input
label="Stop Date (optional)"
variant="outlined"
prepend-icon=""
prepend-inner-icon="mdi-calendar-remove"
clearable
v-model="addStopDate"
hide-details
@click:clear="addStopDate = undefined"
></v-date-input>
</div>
</div>
<v-number-input
label="Amount (optional)"
users
variant="outlined"
clearable
:min="0"
v-model="addAmount"
>
</v-number-input>
<v-text-field
label="Notes (optional)"
v-model="addNotes"
variant="outlined"
clearable
></v-text-field>
<span class="dialogNote">
all fields marked with * are required
</span>
</div>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="10" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="add = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Add"
type="submit"
color="blue darken-1"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
<!-- Search -->
<v-text-field
class="compact-form mr-2"
label="Search"
variant="solo"
density="compact"
prepend-inner-icon="mdi-magnify"
hide-details
single-line
clearable
v-model="search"
rounded="pill"
></v-text-field>
<!-- Logout -->
<v-btn icon variant="outlined" @click="handlelogout">
<LogOut />
</v-btn>
</v-toolbar>
</div>
<v-snackbar v-model="snackbar" :timeout="2000" elevation="24" location="top">
<span> Who am I??? </span>
</v-snackbar>
</template>
<script setup lang="ts">
import { store } from "@/store";
import { SubmitEventPromise } from "vuetify";
import { ref } from "vue";
import { Plus, LogOut, FolderPlus } from "lucide-vue-next";
import { useQuery, useMutation, useQueryClient } from "@tanstack/vue-query";
import UsersDialog from "./NavBarIcons/UsersDialog.vue";
import UsersEditActions from "./NavBarIcons/UsersEditActions.vue";
import { axiosInstance } from "@/client";
import { LicenseGroup, CreateLicenseDto, CreateGroupDto, User } from "@/types";
import { search } from "@/store";
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ["licenses"],
queryFn: async () => {
const res = await axiosInstance.get<LicenseGroup[]>("/licenses");
return res.data;
},
select: (data) => {
return data.map((group) => {
return {
title: group.name,
value: group.id,
};
});
},
});
const { data: user } = useQuery({
queryKey: ["user"],
queryFn: async () => {
const res = await axiosInstance.get<User>("/users/me");
return res.data;
},
});
const { mutate: licenseMutate } = useMutation({
mutationFn: async (newLicense: CreateLicenseDto) => {
await axiosInstance.post("/licenses", newLicense);
},
onError: (error) => {
snackbar.value = true;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["licenses"] });
add.value = false;
},
});
const { mutate: groupsMutate } = useMutation({
mutationFn: async (newGroup: CreateGroupDto) => {
await axiosInstance.post("/groups", newGroup);
},
onError: (error) => {
snackbar.value = true;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["licenses"] });
group.value = false;
},
});
function handlelogout() {
store.setToken(null);
}
async function submit(event: SubmitEventPromise) {
const result = await event;
if (result.valid && addLicenseGroup.value) {
const data = {
name: addLicenseName.value,
group_id: addLicenseGroup.value,
amount: addAmount.value,
key: addLicenseKey.value,
start: addStartDate.value ?? null,
end: addStopDate.value ?? null,
note: addNotes.value,
};
console.log(data);
licenseMutate(data);
} else {
console.log("Invalid");
}
}
async function submitGroup(event: SubmitEventPromise) {
const result = await event;
if (result.valid) {
const groupsData = {
name: groupGroupName.value,
};
console.log(groupsData);
groupsMutate(groupsData);
} else {
console.log("Invalid");
}
}
const snackbar = ref(false);
//
// GROUP SECTION
//
const group = ref(false); // Dialog for Group
const groupGroupName = ref<string>("");
// Rules for Group Dialog
const licenseGroupRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A GROUP NAME)";
},
];
//
// ADD SECTION
//
const add = ref(false);
// References for Add License Dialog
const addLicenseName = ref<string>("");
const addLicenseKey = ref<string>("");
const addLicenseGroup = ref<string>();
// Rules for Add License Dialog
const licenseNameRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A LICENSE NAME)";
},
];
const selectGroupRules = [
(value: string) => {
if (value) return true;
return "YOU MUST SELECT (A LICENSE GROUP)";
},
];
const licenseKeyRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A LICENSE KEY)";
},
];
// Optional Fields
const addAmount = ref<number | undefined>();
const addNotes = ref<string | undefined>("");
// Date Picker
const addStartDate = ref<Date | undefined>();
const addStopDate = ref<Date | undefined>();
</script>
<style scoped>
.logo {
margin-right: 20%;
margin-left: 10px;
}
.dialogNote {
font-size: 13px;
color: grey;
}
</style>

View file

@ -0,0 +1,135 @@
<template>
<template v-if="visible">
<v-sheet elevation="4" width="96vw" rounded="lg" class="mt-3">
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>
<div class="mr-3">
{{ license.name }}
</div>
<div
class="mr-3 d-flex align-self-center"
style="align-items: center"
>
<KeyRound class="mr-1" />
{{ license.key }}
</div>
<div
class="mr-3 d-flex align-self-center"
style="align-items: center"
>
<CalendarCheck />
<span>
{{
license.start
? new Date(license.start).toLocaleDateString()
: "Indefinitely"
}}
</span>
</div>
<div
class="mr-3 d-flex align-self-center"
style="align-items: center"
>
<CalendarX />
<span>
{{
license.end
? new Date(license.end).toLocaleDateString()
: "Indefinitely"
}}
</span>
</div>
<div
class="mr-3 d-flex align-self-center"
style="align-items: center"
>
<img
src="../assets/doublekey.svg"
alt="logo"
class="logo mr-1"
width="24"
/>
<span
v-if="license.amount == null || license.amount == undefined"
>
<Infinity />
</span>
<span v-else>
{{ license.amount }}
</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
Notes:
<div class="flex-nowrap d-flex" no-gutters>
<div
class="flex-grow-1 overflow-x-auto border-e-md align-self-end"
cols="10"
>
{{ license.note }}
</div>
<div align="end">
<!-- -->
<!-- EDIT SECTION -->
<!-- -->
<UpdateDialog :license="license" />
<!-- -->
<!-- DELETE BTN -->
<!-- -->
<DeleteDialog :license="license" />
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-sheet>
</template>
</template>
<script setup lang="ts">
import { License } from "@/types";
import { Infinity, KeyRound, CalendarCheck, CalendarX } from "lucide-vue-next";
import UpdateDialog from "./UpdateDialog.vue";
import DeleteDialog from "./DeleteDialog.vue";
import { ComputedRef, inject, ref, watch } from "vue";
import { key } from "@/store";
const { license } = defineProps<{
license: License;
/*id: string;
licenseName: string;
licenseKey: string;
startDate: Date | null;
endDate: Date | null;
amount?: number;
notes?: string;*/
}>();
const visible = ref(true);
const { searching, visibleIds } = inject(key) as {
searching: ComputedRef<boolean>;
visibleIds: ComputedRef<string[]>;
};
watch([searching, visibleIds], ([searching, visibleIds]) => {
if (!searching) {
visible.value = true;
} else {
visible.value = visibleIds.includes(license.id);
}
});
</script>
<style scoped>
.values {
display: flex;
align-items: center;
}
</style>

View file

@ -0,0 +1,72 @@
<template>
<div>
<HeaderBar />
<div class="ma-8">
<div v-if="isPending">Loading...</div>
<div v-else-if="isError">Error: {{ error?.message }}</div>
<ul v-else-if="data">
<li v-for="group in data" :key="group.id">
<CategoryContainer :licenseGroup="group" />
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import HeaderBar from "./HeaderBar.vue";
import CategoryContainer from "./CategoryContainer.vue";
import { useQuery } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
import { LicenseGroup, License } from "@/types";
import { search, key } from "@/store";
import MiniSearch from "minisearch";
import { computed, provide } from "vue";
const { isPending, isError, data, error } = useQuery({
queryKey: ["licenses"],
queryFn: async () => {
const res = await axiosInstance.get<LicenseGroup[]>("/licenses");
console.log(res.data);
return res.data;
},
refetchInterval: 60 * 1000,
});
const searchEngine = computed(() => {
let minisearch = new MiniSearch({
fields: ["name", "description", "id"],
searchOptions: {
boost: { name: 2 },
prefix: true,
},
});
let licenses: License[] = [];
data.value?.forEach((group) => {
group.licenses.forEach((license) => licenses.push(license));
});
console.log(licenses);
minisearch.addAll(licenses);
return minisearch;
});
const searching = computed(() => search.value !== "");
const visibleIds = computed(() => {
return searchEngine.value.search(search.value).map((searchResult) => {
return searchResult.id;
});
});
provide(key, {
visibleIds,
searching,
});
</script>
<style scoped>
li,
ul {
list-style-type: none;
}
</style>

View file

@ -0,0 +1,149 @@
<template>
<div>
<v-btn prepend-icon="mdi-plus" variant="outlined" block @click="add = true">
Add User
</v-btn>
<v-dialog v-model="add" width="600" persistent>
<v-card max-width="600">
<v-form @submit.prevent="submit">
<v-card-title>
<span class="headline">Add User</span>
</v-card-title>
<v-card-subtitle>
<span> Add an User to the database</span>
</v-card-subtitle>
<v-card-text>
<div>
<v-text-field
label="User Name *"
v-model="addUserName"
variant="outlined"
required
clearable
:rules="userNameRules"
class="mb-3"
></v-text-field>
<v-text-field
label="User Email *"
v-model="addUserEmail"
variant="outlined"
required
clearable
:rules="userNameEmail"
class="mb-3"
></v-text-field>
<v-text-field
label="User Password *"
v-model="addUserPassword"
variant="outlined"
required
clearable
:rules="userNamePassword"
class="mb-3"
></v-text-field>
<v-switch
label="Admin (Optional)"
inset
v-model="addUserAdmin"
color="primary"
></v-switch>
<span class="dialogNote">
all fields marked with * are required
</span>
</div>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="10" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="add = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Add"
type="submit"
color="blue darken-1"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
import { axiosInstance } from "@/client";
import { CreateUserDto } from "@/types";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { ref } from "vue";
import { SubmitEventPromise } from "vuetify";
const add = ref(false);
const addUserName = ref("");
const addUserEmail = ref("");
const addUserPassword = ref("");
const addUserAdmin = ref(false);
const userNameRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A USER NAME)";
},
];
const userNameEmail = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A USER EMAIL)";
},
];
const userNamePassword = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A USER PASSWORD)";
},
];
const queryClient = useQueryClient();
const { mutate: userMutate } = useMutation({
mutationFn: async (newUser: CreateUserDto) => {
await axiosInstance.post("/users", newUser);
},
onError: (error) => {
console.log(error);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
add.value = false;
},
});
async function submit(event: SubmitEventPromise) {
const result = await event;
if (result.valid) {
const data = {
name: addUserName.value,
email: addUserEmail.value,
password: addUserPassword.value,
admin: addUserAdmin.value,
};
console.log(data);
userMutate(data);
} else {
console.log("Invalid");
}
}
</script>
<style scoped></style>

View file

@ -0,0 +1,70 @@
<template>
<v-icon size="small" @click="deleteDialog = true"> mdi-delete </v-icon>
<v-dialog v-model="deleteDialog" width="600" persistent>
<v-card max-width="600">
<v-card-title>
<span class="headline">Delete License</span>
</v-card-title>
<v-card-subtitle>
<span>Delete a License inside the database</span>
</v-card-subtitle>
<v-card-text>
<h4>This action is irreversible!</h4>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="8" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="deleteDialog = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Confirm Delete"
color="red darken-1"
@click="deleteMutate()"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { useQuery, useMutation, useQueryClient } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
import { SubmitEventPromise } from "vuetify";
import { LicenseGroup, UpdateLicenseDto, License } from "@/types";
import { User } from "@/types";
import { ref } from "vue";
const { user } = defineProps<{ user: User }>();
const queryClient = useQueryClient();
const gyros = user.id;
const { mutate: deleteMutate } = useMutation({
mutationFn: async () => {
console.log(gyros);
console.log(user.id);
await axiosInstance.delete(`/users/${gyros}`);
},
onError: (error) => {
console.log(error);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
deleteDialog.value = false;
},
});
const deleteDialog = ref(false);
</script>
<style scoped></style>

View file

@ -0,0 +1,69 @@
<template>
<v-btn icon class="mr-3" @click="users = true">
<Users />
</v-btn>
<v-dialog v-model="users" width="1200" persistent>
<v-card max-width="1200">
<v-card-title>
<span class="headline">User Management Menu</span>
</v-card-title>
<v-card-subtitle>
<span>Add/Delete/Manage users who have access to this tool</span>
</v-card-subtitle>
<v-card-text>
<v-data-table
:headers="headers"
:items="data ?? []"
:items-per-page="10"
item-key="name"
class="elevation-5 mb-5"
>
<template v-slot:item.actions="{ item }">
<UsersEditAction :user="item" :admin-menu="true" />
<UsersDeleteAction :user="item" />
</template>
</v-data-table>
<UsersAddDialog />
<v-divider class="border-opacity-50 mt-5" :thickness="2"></v-divider>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" @click="users = false">Close</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import UsersDeleteAction from "./UsersDeleteActions.vue";
import UsersEditAction from "./UsersEditActions.vue";
import UsersAddDialog from "./UsersAddDialog.vue";
import { Users } from "lucide-vue-next";
import { ref } from "vue";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
import { User } from "@/types";
// Data Table values
const headers = ref([
{ title: "Name", value: "name" },
{ title: "Email", value: "email" },
{ title: "isAdmin", value: "admin" },
{ title: "Actions", value: "actions", sortable: false },
]);
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ["users"],
queryFn: async () => {
const res = await axiosInstance.get<User[]>("/users");
return res.data;
},
});
// Dialog open state
const users = ref(false);
</script>
<style scoped></style>

View file

@ -0,0 +1,145 @@
<template>
<v-icon v-if="adminMenu" class="me-2" size="small" @click="edit = true"
>mdi-pencil</v-icon
>
<v-btn v-else icon class="mr-3" @click="edit = true">
<Users />
</v-btn>
<v-dialog v-model="edit" width="600" persistent>
<v-card max-width="600">
<v-form @submit.prevent="submit">
<v-card-title>
<span class="headline">Edit User</span>
</v-card-title>
<v-card-subtitle>
<span>Edit a User inside the database</span>
</v-card-subtitle>
<v-card-text>
<div>
<v-text-field
label="User Name *"
v-model="editUserName"
variant="outlined"
clearable
:rules="editUserNameRules"
class="mb-3"
></v-text-field>
<v-text-field
label="User Email *"
v-model="editUserEmail"
variant="outlined"
clearable
:rules="editUserEmailRules"
class="mb-3"
></v-text-field>
<v-text-field
label="User Password (Optional)"
v-model="editUserPassword"
variant="outlined"
clearable
class="mb-3"
></v-text-field>
<v-switch
label="Admin (Optional)"
inset
v-model="editUserAdmin"
color="primary"
v-if="adminMenu"
></v-switch>
<span class="dialogNote">
all fields marked with * are required
</span>
</div>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="10" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="edit = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Update"
type="submit"
color="blue darken-1"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { SubmitEventPromise } from "vuetify";
import { axiosInstance } from "@/client";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { CreateLicenseDto, UpdateUserDto, User } from "@/types";
import { Users } from "lucide-vue-next";
const { user, adminMenu } = defineProps<{ user: User; adminMenu: boolean }>();
const queryClient = useQueryClient();
const editUserNameRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A USER NAME)";
},
];
const editUserEmailRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A EMAIL NAME)";
},
];
const id = user.id;
async function submit(event: SubmitEventPromise) {
const result = await event;
if (result.valid) {
const userData = adminMenu ? {
name: editUserName.value,
email: editUserEmail.value,
password: !editUserPassword.value || editUserPassword.value === "" ? undefined : editUserPassword.value,
admin: editUserAdmin.value
} : {
name: editUserName.value,
email: editUserEmail.value,
password: !editUserPassword.value || editUserPassword.value === "" ? undefined : editUserPassword.value
}
console.log(userData);
userMutate(userData);
} else {
console.log("Invalid");
}
}
const { mutate: userMutate } = useMutation({
mutationFn: async (editUser: UpdateUserDto) => {
await axiosInstance.put(`/users/${id}`, editUser);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
edit.value = false;
},
});
const edit = ref(false);
const editUserName = ref(user.name);
const editUserEmail = ref(user.email);
const editUserAdmin = ref(user.admin);
const editUserPassword = ref<string | undefined>();
</script>
<style scoped></style>

View 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>
```

View file

@ -0,0 +1,207 @@
<template>
<v-btn class="mr-3" @click="edit = true" flat size="small">
<Pencil />
</v-btn>
<v-dialog v-model="edit" width="600" persistent>
<v-card max-width="600">
<v-form @submit.prevent="submitEdit">
<v-card-title>
<span class="headline">Edit License</span>
</v-card-title>
<v-card-subtitle>
<span>Edit a License inside the database</span>
</v-card-subtitle>
<v-card-text>
<div>
<v-text-field
label="License Name *"
v-model="editLicenseName"
variant="outlined"
required
clearable
:rules="editNameRules"
class="mb-3"
></v-text-field>
<v-text-field
label="License Key *"
v-model="editLicenseKey"
variant="outlined"
required
clearable
:rules="editNameRules"
class="mb-3"
></v-text-field>
<v-autocomplete
clearable
label="Select Group *"
:items="data"
variant="outlined"
:rules="editNameRules"
class="mb-1"
v-model="editLicenseGroup"
></v-autocomplete>
<!-- Divider maybe remove -->
<v-divider class="border-opacity-50" :thickness="2"></v-divider>
<div>
<div class="mb-3 mt-3">
<v-date-input
label="Start Date (optional)"
variant="outlined"
prepend-icon=""
prepend-inner-icon="mdi-calendar-check"
clearable
v-model="editStartDate"
hide-details
@click:clear="editStartDate = null"
></v-date-input>
</div>
<div class="mb-6">
<v-date-input
label="Stop Date (optional)"
variant="outlined"
prepend-icon=""
prepend-inner-icon="mdi-calendar-remove"
clearable
v-model="editStopDate"
hide-details
@click:clear="editStopDate = null"
></v-date-input>
</div>
</div>
<v-number-input
label="Amount (optional)"
users
variant="outlined"
clearable
:min="0"
v-model="editAmount"
>
</v-number-input>
<v-text-field
label="Notes (optional)"
v-model="editNotes"
variant="outlined"
clearable
></v-text-field>
<span class="dialogNote">
all fields marked with * are required
</span>
</div>
</v-card-text>
<v-card-actions>
<v-row>
<v-col cols="10" align="right" no-gutters>
<v-btn
class="ms-auto"
text="Cancel"
color="blue darken-1"
@click="edit = false"
></v-btn>
</v-col>
<v-col>
<v-btn
class="ms-auto"
text="Update"
type="submit"
color="blue darken-1"
></v-btn>
</v-col>
</v-row>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { Pencil } from "lucide-vue-next";
import { useQuery, useMutation, useQueryClient } from "@tanstack/vue-query";
import { axiosInstance } from "@/client";
import { SubmitEventPromise } from "vuetify";
import { LicenseGroup, UpdateLicenseDto, License } from "@/types";
import { ref } from "vue";
const { license } = defineProps<{
license: License;
}>();
// EDIT SECTION
const edit = ref(false);
const editLicenseName = ref(license.name);
const editLicenseGroup = ref(license.group_id);
const editAmount = ref(license.amount);
const editLicenseKey = ref(license.key);
const editStartDate = ref<Date | null>(
license.start ? new Date(license.start) : null
);
const editStopDate = ref<Date | null>(
license.end ? new Date(license.end) : null
);
const editNotes = ref<string | undefined>(license.note);
const editNameRules = [
(value: string) => {
if (value) return true;
return "YOU MUST ENTER (A GROUP NAME)";
},
];
async function submitEdit(event: SubmitEventPromise) {
console.log(editStartDate.value)
const result = await event;
if (result.valid) {
const editData = {
name: editLicenseName.value,
group_id: editLicenseGroup.value,
amount: editAmount.value,
key: editLicenseKey.value,
start: editStartDate.value
? editStartDate.value.toISOString()
: undefined,
end: editStopDate.value ? editStopDate.value.toISOString() : undefined,
note: editNotes.value,
};
console.log(editData);
editMutate(editData);
} else {
console.log("Invalid");
}
}
const queryClient = useQueryClient();
const gyros = license.id;
const { mutate: editMutate } = useMutation({
mutationFn: async (newEdit: UpdateLicenseDto) => {
await axiosInstance.put(`/licenses/${gyros}`, newEdit);
},
onError: (error) => {
console.log(error);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["licenses"] });
edit.value = false;
},
});
const { data } = useQuery({
queryKey: ["licenses"],
queryFn: async () => {
const res = await axiosInstance.get<LicenseGroup[]>("/licenses");
return res.data;
},
select: (data) => {
return data.map((group) => {
return {
title: group.name,
value: group.id,
};
});
},
});
</script>
<style scoped></style>

View file

@ -0,0 +1,65 @@
<template>
<v-container>
<v-row justify="center">
<v-col cols="12" sm="8" md="4">
<v-card>
<v-card-title class="text-h5">Login</v-card-title>
<v-card-text>
<v-form>
<v-text-field
v-model="email"
label="Email"
required
variant="outlined"
></v-text-field>
<v-text-field
v-model="password"
label="Password"
type="password"
required
variant="outlined"
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn @click="login" color="primary">Login</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
import { ref } from "vue";
import axios from "axios";
import { store } from "@/store";
const email = ref("");
const password = ref("");
async function login() {
try {
const response = await axios.post<string>(
import.meta.env.VITE_BACKEND_URL + "/auth/login",
{ email: email.value, password: password.value }
);
if (response.status === 200) {
store.setToken(response.data);
} else {
alert("Invalid Credentials");
}
} catch (err) {
alert("Invalid Credentials");
}
}
/*
setTimeout(() => {
console.log(email.value)
console.log(password.value)
}, 3000);
*/
</script>
<style scoped></style>

22
web/src/main.ts Normal file
View file

@ -0,0 +1,22 @@
/**
* main.ts
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
const app = createApp(App)
registerPlugins(app)
app.mount('#app')

View 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.

17
web/src/plugins/index.ts Normal file
View file

@ -0,0 +1,17 @@
/**
* plugins/index.ts
*
* Automatically included in `./src/main.ts`
*/
// Plugins
import { VueQueryPlugin } from "@tanstack/vue-query";
import vuetify from "./vuetify";
// Types
import type { App } from "vue";
export function registerPlugins(app: App) {
app.use(vuetify);
app.use(VueQueryPlugin);
}

View file

@ -0,0 +1,36 @@
/**
* plugins/vuetify.ts
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import "@mdi/font/css/materialdesignicons.css";
import "vuetify/styles";
// Composables
import { createVuetify } from "vuetify";
import { VNumberInput, VDateInput } from "vuetify/labs/components";
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
themes: {
light: {
dark: false,
colors: {
main: "#024950",
darker: "#003135",
contrast: "#964734",
accent: "#0FA4AF",
accentLigher: "#AFDDE5",
backdrop: "#e3e3e3",
},
},
},
},
components: {
VNumberInput,
VDateInput,
},
});

23
web/src/store.ts Normal file
View file

@ -0,0 +1,23 @@
import { ComputedRef, InjectionKey, reactive, ref } from "vue";
export const store = reactive<{
token: string | null;
setToken: (token: string | null) => void;
}>({
token: localStorage.getItem("token") || null,
setToken: (token: string | null) => {
store.token = token;
if (token) {
localStorage.setItem("token", token);
} else {
localStorage.removeItem("token");
}
},
});
export const search = ref("");
export const key = Symbol() as InjectionKey<{
searching: ComputedRef<boolean>;
visibleIds: ComputedRef<string[]>;
}>;

51
web/src/types.ts Normal file
View file

@ -0,0 +1,51 @@
export interface LicenseGroup {
id: string;
name: string;
licenses: License[];
}
export interface License {
name: string;
id: string;
start?: string;
end?: string;
key: string;
amount?: number;
note?: string;
group_id: string;
}
export interface CreateLicenseDto {
name: string;
start: Date | null;
end: Date | null;
key: string;
amount?: number;
group_id: string;
note?: string;
}
export interface User {
id: string;
name: string;
email: string;
admin: boolean;
}
export interface CreateUserDto {
name: string;
email: string;
password: string;
admin: boolean;
}
export interface UpdateUserDto {
name: string;
email: string;
password?: string;
admin?: boolean;
}
export type CreateGroupDto = Omit<LicenseGroup, "id" | "licenses">;
export type UpdateLicenseDto = Omit<License, "id">;

7
web/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

32
web/tsconfig.json Normal file
View file

@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": [
"vite/client"
],
"allowJs": true,
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": [
"src/**/*",
"src/**/*.vue",
"src/**/*/*.vue"
],
"exclude": ["dist", "node_modules", "cypress"],
"references": [{ "path": "./tsconfig.node.json" }],
}

9
web/tsconfig.node.json Normal file
View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.mts"]
}

47
web/vite.config.mts Normal file
View file

@ -0,0 +1,47 @@
// Plugins
import Components from 'unplugin-vue-components/vite'
import Vue from '@vitejs/plugin-vue'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import ViteFonts from 'unplugin-fonts/vite'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
Vue({
template: { transformAssetUrls },
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify(),
Components(),
ViteFonts({
google: {
families: [{
name: 'Roboto',
styles: 'wght@100;300;400;500;700;900',
}],
},
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
})