mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add dashboard
This commit is contained in:
20
dashboard/.env.example
Normal file
20
dashboard/.env.example
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
MONGO_CONNECTION_STRING=
|
||||||
|
|
||||||
|
REDIS_URL=
|
||||||
|
REDIS_USERNAME=
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
AI_ORG=
|
||||||
|
AI_PROJECT=
|
||||||
|
AI_KEY=
|
||||||
|
|
||||||
|
EMAIL_SERVICE=
|
||||||
|
EMAIL_HOST=
|
||||||
|
EMAIL_USER=
|
||||||
|
EMAIL_PASS=
|
||||||
|
|
||||||
|
AUTH_JWT_SECRET=
|
||||||
|
|
||||||
|
GOOGLE_AUTH_CLIENT_ID=
|
||||||
|
GOOGLE_AUTH_CLIENT_SECRET=
|
||||||
34
dashboard/.gitignore
vendored
Normal file
34
dashboard/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
|
||||||
|
# Test reports
|
||||||
|
*.report.txt
|
||||||
|
|
||||||
|
# PDF
|
||||||
|
out.pdf
|
||||||
|
|
||||||
|
# TESTS - TO REMOVE
|
||||||
|
tests
|
||||||
75
dashboard/README.md
Normal file
75
dashboard/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Nuxt 3 Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm run dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm run preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
30
dashboard/app.vue
Normal file
30
dashboard/app.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { Lit } from 'litlyx';
|
||||||
|
|
||||||
|
Lit.init('6643cd08a1854e3b81722ab5');
|
||||||
|
|
||||||
|
const debugMode = process.dev;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="w-dvw h-dvh bg-[#151517] relative">
|
||||||
|
|
||||||
|
<div v-if="debugMode"
|
||||||
|
class="absolute bottom-8 left-4 bg-red-400 text-white text-[.9rem] font-bold px-4 py-[.2rem] rounded-lg z-[100]">
|
||||||
|
<div class="poppins flex sm:hidden"> XS </div>
|
||||||
|
<div class="poppins hidden sm:max-md:flex"> SM - MOBILE </div>
|
||||||
|
<div class="poppins hidden md:max-lg:flex"> MD - TABLET </div>
|
||||||
|
<div class="poppins hidden lg:max-xl:flex"> LG - LARGE </div>
|
||||||
|
<div class="poppins hidden xl:max-2xl:flex"> XL - EXTRA LARGE </div>
|
||||||
|
<div class="poppins hidden 2xl:flex"> 2XL - WIDE SCREEN </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage></NuxtPage>
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
27846
dashboard/assets/font-awesome/css/all.css
vendored
Normal file
27846
dashboard/assets/font-awesome/css/all.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
12
dashboard/assets/font-awesome/css/all.min.css
vendored
Normal file
12
dashboard/assets/font-awesome/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1573
dashboard/assets/font-awesome/css/brands.css
vendored
Normal file
1573
dashboard/assets/font-awesome/css/brands.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
dashboard/assets/font-awesome/css/brands.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12841
dashboard/assets/font-awesome/css/duotone.css
vendored
Normal file
12841
dashboard/assets/font-awesome/css/duotone.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8
dashboard/assets/font-awesome/css/duotone.min.css
vendored
Normal file
8
dashboard/assets/font-awesome/css/duotone.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13336
dashboard/assets/font-awesome/css/fontawesome.css
vendored
Normal file
13336
dashboard/assets/font-awesome/css/fontawesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
dashboard/assets/font-awesome/css/fontawesome.min.css
vendored
Normal file
9
dashboard/assets/font-awesome/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
19
dashboard/assets/font-awesome/css/light.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/light.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Pro';
|
||||||
|
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Pro'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-light-300.woff2") format("woff2"), url("../webfonts/fa-light-300.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fal,
|
||||||
|
.fa-light {
|
||||||
|
font-weight: 300; }
|
||||||
6
dashboard/assets/font-awesome/css/light.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/light.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Pro";--fa-font-light:normal 300 1em/1 "Font Awesome 6 Pro"}@font-face{font-family:"Font Awesome 6 Pro";font-style:normal;font-weight:300;font-display:block;src:url(../webfonts/fa-light-300.woff2) format("woff2"),url(../webfonts/fa-light-300.ttf) format("truetype")}.fa-light,.fal{font-weight:300}
|
||||||
19
dashboard/assets/font-awesome/css/regular.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/regular.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Pro';
|
||||||
|
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Pro'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.far,
|
||||||
|
.fa-regular {
|
||||||
|
font-weight: 400; }
|
||||||
6
dashboard/assets/font-awesome/css/regular.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/regular.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Pro";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Pro"}@font-face{font-family:"Font Awesome 6 Pro";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
|
||||||
19
dashboard/assets/font-awesome/css/sharp-light.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/sharp-light.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Sharp';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-sharp-light-300.woff2") format("woff2"), url("../webfonts/fa-sharp-light-300.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fasl,
|
||||||
|
.fa-light {
|
||||||
|
font-weight: 300; }
|
||||||
6
dashboard/assets/font-awesome/css/sharp-light.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/sharp-light.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-sharp:"Font Awesome 6 Sharp";--fa-font-sharp-light:normal 300 1em/1 "Font Awesome 6 Sharp"}@font-face{font-family:"Font Awesome 6 Sharp";font-style:normal;font-weight:300;font-display:block;src:url(../webfonts/fa-sharp-light-300.woff2) format("woff2"),url(../webfonts/fa-sharp-light-300.ttf) format("truetype")}.fa-light,.fasl{font-weight:300}
|
||||||
19
dashboard/assets/font-awesome/css/sharp-regular.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/sharp-regular.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Sharp';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-sharp-regular-400.woff2") format("woff2"), url("../webfonts/fa-sharp-regular-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fasr,
|
||||||
|
.fa-regular {
|
||||||
|
font-weight: 400; }
|
||||||
6
dashboard/assets/font-awesome/css/sharp-regular.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/sharp-regular.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-sharp:"Font Awesome 6 Sharp";--fa-font-sharp-regular:normal 400 1em/1 "Font Awesome 6 Sharp"}@font-face{font-family:"Font Awesome 6 Sharp";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-sharp-regular-400.woff2) format("woff2"),url(../webfonts/fa-sharp-regular-400.ttf) format("truetype")}.fa-regular,.fasr{font-weight:400}
|
||||||
19
dashboard/assets/font-awesome/css/sharp-solid.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/sharp-solid.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Sharp';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-sharp-solid-900.woff2") format("woff2"), url("../webfonts/fa-sharp-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fass,
|
||||||
|
.fa-solid {
|
||||||
|
font-weight: 900; }
|
||||||
6
dashboard/assets/font-awesome/css/sharp-solid.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/sharp-solid.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-sharp:"Font Awesome 6 Sharp";--fa-font-sharp-solid:normal 900 1em/1 "Font Awesome 6 Sharp"}@font-face{font-family:"Font Awesome 6 Sharp";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-sharp-solid-900.woff2) format("woff2"),url(../webfonts/fa-sharp-solid-900.ttf) format("truetype")}.fa-solid,.fass{font-weight:900}
|
||||||
19
dashboard/assets/font-awesome/css/sharp-thin.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/sharp-thin.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-sharp: 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Sharp';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-sharp-thin-100.woff2") format("woff2"), url("../webfonts/fa-sharp-thin-100.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fast,
|
||||||
|
.fa-thin {
|
||||||
|
font-weight: 100; }
|
||||||
6
dashboard/assets/font-awesome/css/sharp-thin.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/sharp-thin.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-sharp:"Font Awesome 6 Sharp";--fa-font-sharp-thin:normal 100 1em/1 "Font Awesome 6 Sharp"}@font-face{font-family:"Font Awesome 6 Sharp";font-style:normal;font-weight:100;font-display:block;src:url(../webfonts/fa-sharp-thin-100.woff2) format("woff2"),url(../webfonts/fa-sharp-thin-100.ttf) format("truetype")}.fa-thin,.fast{font-weight:100}
|
||||||
19
dashboard/assets/font-awesome/css/solid.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/solid.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Pro';
|
||||||
|
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Pro'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fas,
|
||||||
|
.fa-solid {
|
||||||
|
font-weight: 900; }
|
||||||
6
dashboard/assets/font-awesome/css/solid.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/solid.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Pro";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Pro"}@font-face{font-family:"Font Awesome 6 Pro";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
||||||
640
dashboard/assets/font-awesome/css/svg-with-js.css
vendored
Normal file
640
dashboard/assets/font-awesome/css/svg-with-js.css
vendored
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Solid';
|
||||||
|
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Regular';
|
||||||
|
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Light';
|
||||||
|
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Thin';
|
||||||
|
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
|
||||||
|
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
|
||||||
|
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }
|
||||||
|
|
||||||
|
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: content-box; }
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
display: var(--fa-display, inline-block);
|
||||||
|
height: 1em;
|
||||||
|
overflow: visible;
|
||||||
|
vertical-align: -.125em; }
|
||||||
|
.svg-inline--fa.fa-2xs {
|
||||||
|
vertical-align: 0.1em; }
|
||||||
|
.svg-inline--fa.fa-xs {
|
||||||
|
vertical-align: 0em; }
|
||||||
|
.svg-inline--fa.fa-sm {
|
||||||
|
vertical-align: -0.07143em; }
|
||||||
|
.svg-inline--fa.fa-lg {
|
||||||
|
vertical-align: -0.2em; }
|
||||||
|
.svg-inline--fa.fa-xl {
|
||||||
|
vertical-align: -0.25em; }
|
||||||
|
.svg-inline--fa.fa-2xl {
|
||||||
|
vertical-align: -0.3125em; }
|
||||||
|
.svg-inline--fa.fa-pull-left {
|
||||||
|
margin-right: var(--fa-pull-margin, 0.3em);
|
||||||
|
width: auto; }
|
||||||
|
.svg-inline--fa.fa-pull-right {
|
||||||
|
margin-left: var(--fa-pull-margin, 0.3em);
|
||||||
|
width: auto; }
|
||||||
|
.svg-inline--fa.fa-li {
|
||||||
|
width: var(--fa-li-width, 2em);
|
||||||
|
top: 0.25em; }
|
||||||
|
.svg-inline--fa.fa-fw {
|
||||||
|
width: var(--fa-fw-width, 1.25em); }
|
||||||
|
|
||||||
|
.fa-layers svg.svg-inline--fa {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0; }
|
||||||
|
|
||||||
|
.fa-layers-text, .fa-layers-counter {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center; }
|
||||||
|
|
||||||
|
.fa-layers {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: -.125em;
|
||||||
|
width: 1em; }
|
||||||
|
.fa-layers svg.svg-inline--fa {
|
||||||
|
-webkit-transform-origin: center center;
|
||||||
|
transform-origin: center center; }
|
||||||
|
|
||||||
|
.fa-layers-text {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
-webkit-transform-origin: center center;
|
||||||
|
transform-origin: center center; }
|
||||||
|
|
||||||
|
.fa-layers-counter {
|
||||||
|
background-color: var(--fa-counter-background-color, #ff253a);
|
||||||
|
border-radius: var(--fa-counter-border-radius, 1em);
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--fa-inverse, #fff);
|
||||||
|
line-height: var(--fa-counter-line-height, 1);
|
||||||
|
max-width: var(--fa-counter-max-width, 5em);
|
||||||
|
min-width: var(--fa-counter-min-width, 1.5em);
|
||||||
|
overflow: hidden;
|
||||||
|
padding: var(--fa-counter-padding, 0.25em 0.5em);
|
||||||
|
right: var(--fa-right, 0);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
top: var(--fa-top, 0);
|
||||||
|
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
|
||||||
|
transform: scale(var(--fa-counter-scale, 0.25));
|
||||||
|
-webkit-transform-origin: top right;
|
||||||
|
transform-origin: top right; }
|
||||||
|
|
||||||
|
.fa-layers-bottom-right {
|
||||||
|
bottom: var(--fa-bottom, 0);
|
||||||
|
right: var(--fa-right, 0);
|
||||||
|
top: auto;
|
||||||
|
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
-webkit-transform-origin: bottom right;
|
||||||
|
transform-origin: bottom right; }
|
||||||
|
|
||||||
|
.fa-layers-bottom-left {
|
||||||
|
bottom: var(--fa-bottom, 0);
|
||||||
|
left: var(--fa-left, 0);
|
||||||
|
right: auto;
|
||||||
|
top: auto;
|
||||||
|
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
-webkit-transform-origin: bottom left;
|
||||||
|
transform-origin: bottom left; }
|
||||||
|
|
||||||
|
.fa-layers-top-right {
|
||||||
|
top: var(--fa-top, 0);
|
||||||
|
right: var(--fa-right, 0);
|
||||||
|
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
-webkit-transform-origin: top right;
|
||||||
|
transform-origin: top right; }
|
||||||
|
|
||||||
|
.fa-layers-top-left {
|
||||||
|
left: var(--fa-left, 0);
|
||||||
|
right: auto;
|
||||||
|
top: var(--fa-top, 0);
|
||||||
|
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
transform: scale(var(--fa-layers-scale, 0.25));
|
||||||
|
-webkit-transform-origin: top left;
|
||||||
|
transform-origin: top left; }
|
||||||
|
|
||||||
|
.fa-1x {
|
||||||
|
font-size: 1em; }
|
||||||
|
|
||||||
|
.fa-2x {
|
||||||
|
font-size: 2em; }
|
||||||
|
|
||||||
|
.fa-3x {
|
||||||
|
font-size: 3em; }
|
||||||
|
|
||||||
|
.fa-4x {
|
||||||
|
font-size: 4em; }
|
||||||
|
|
||||||
|
.fa-5x {
|
||||||
|
font-size: 5em; }
|
||||||
|
|
||||||
|
.fa-6x {
|
||||||
|
font-size: 6em; }
|
||||||
|
|
||||||
|
.fa-7x {
|
||||||
|
font-size: 7em; }
|
||||||
|
|
||||||
|
.fa-8x {
|
||||||
|
font-size: 8em; }
|
||||||
|
|
||||||
|
.fa-9x {
|
||||||
|
font-size: 9em; }
|
||||||
|
|
||||||
|
.fa-10x {
|
||||||
|
font-size: 10em; }
|
||||||
|
|
||||||
|
.fa-2xs {
|
||||||
|
font-size: 0.625em;
|
||||||
|
line-height: 0.1em;
|
||||||
|
vertical-align: 0.225em; }
|
||||||
|
|
||||||
|
.fa-xs {
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0.08333em;
|
||||||
|
vertical-align: 0.125em; }
|
||||||
|
|
||||||
|
.fa-sm {
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 0.07143em;
|
||||||
|
vertical-align: 0.05357em; }
|
||||||
|
|
||||||
|
.fa-lg {
|
||||||
|
font-size: 1.25em;
|
||||||
|
line-height: 0.05em;
|
||||||
|
vertical-align: -0.075em; }
|
||||||
|
|
||||||
|
.fa-xl {
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 0.04167em;
|
||||||
|
vertical-align: -0.125em; }
|
||||||
|
|
||||||
|
.fa-2xl {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 0.03125em;
|
||||||
|
vertical-align: -0.1875em; }
|
||||||
|
|
||||||
|
.fa-fw {
|
||||||
|
text-align: center;
|
||||||
|
width: 1.25em; }
|
||||||
|
|
||||||
|
.fa-ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-left: var(--fa-li-margin, 2.5em);
|
||||||
|
padding-left: 0; }
|
||||||
|
.fa-ul > li {
|
||||||
|
position: relative; }
|
||||||
|
|
||||||
|
.fa-li {
|
||||||
|
left: calc(var(--fa-li-width, 2em) * -1);
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
width: var(--fa-li-width, 2em);
|
||||||
|
line-height: inherit; }
|
||||||
|
|
||||||
|
.fa-border {
|
||||||
|
border-color: var(--fa-border-color, #eee);
|
||||||
|
border-radius: var(--fa-border-radius, 0.1em);
|
||||||
|
border-style: var(--fa-border-style, solid);
|
||||||
|
border-width: var(--fa-border-width, 0.08em);
|
||||||
|
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
|
||||||
|
|
||||||
|
.fa-pull-left {
|
||||||
|
float: left;
|
||||||
|
margin-right: var(--fa-pull-margin, 0.3em); }
|
||||||
|
|
||||||
|
.fa-pull-right {
|
||||||
|
float: right;
|
||||||
|
margin-left: var(--fa-pull-margin, 0.3em); }
|
||||||
|
|
||||||
|
.fa-beat {
|
||||||
|
-webkit-animation-name: fa-beat;
|
||||||
|
animation-name: fa-beat;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
|
||||||
|
|
||||||
|
.fa-bounce {
|
||||||
|
-webkit-animation-name: fa-bounce;
|
||||||
|
animation-name: fa-bounce;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
|
||||||
|
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
|
||||||
|
|
||||||
|
.fa-fade {
|
||||||
|
-webkit-animation-name: fa-fade;
|
||||||
|
animation-name: fa-fade;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
|
||||||
|
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
|
||||||
|
|
||||||
|
.fa-beat-fade {
|
||||||
|
-webkit-animation-name: fa-beat-fade;
|
||||||
|
animation-name: fa-beat-fade;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
|
||||||
|
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
|
||||||
|
|
||||||
|
.fa-flip {
|
||||||
|
-webkit-animation-name: fa-flip;
|
||||||
|
animation-name: fa-flip;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
|
||||||
|
|
||||||
|
.fa-shake {
|
||||||
|
-webkit-animation-name: fa-shake;
|
||||||
|
animation-name: fa-shake;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, linear); }
|
||||||
|
|
||||||
|
.fa-spin {
|
||||||
|
-webkit-animation-name: fa-spin;
|
||||||
|
animation-name: fa-spin;
|
||||||
|
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
animation-delay: var(--fa-animation-delay, 0s);
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 2s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 2s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
|
||||||
|
animation-timing-function: var(--fa-animation-timing, linear); }
|
||||||
|
|
||||||
|
.fa-spin-reverse {
|
||||||
|
--fa-animation-direction: reverse; }
|
||||||
|
|
||||||
|
.fa-pulse,
|
||||||
|
.fa-spin-pulse {
|
||||||
|
-webkit-animation-name: fa-spin;
|
||||||
|
animation-name: fa-spin;
|
||||||
|
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
animation-direction: var(--fa-animation-direction, normal);
|
||||||
|
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
animation-duration: var(--fa-animation-duration, 1s);
|
||||||
|
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||||
|
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
|
||||||
|
animation-timing-function: var(--fa-animation-timing, steps(8)); }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.fa-beat,
|
||||||
|
.fa-bounce,
|
||||||
|
.fa-fade,
|
||||||
|
.fa-beat-fade,
|
||||||
|
.fa-flip,
|
||||||
|
.fa-pulse,
|
||||||
|
.fa-shake,
|
||||||
|
.fa-spin,
|
||||||
|
.fa-spin-pulse {
|
||||||
|
-webkit-animation-delay: -1ms;
|
||||||
|
animation-delay: -1ms;
|
||||||
|
-webkit-animation-duration: 1ms;
|
||||||
|
animation-duration: 1ms;
|
||||||
|
-webkit-animation-iteration-count: 1;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
-webkit-transition-delay: 0s;
|
||||||
|
transition-delay: 0s;
|
||||||
|
-webkit-transition-duration: 0s;
|
||||||
|
transition-duration: 0s; } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-beat {
|
||||||
|
0%, 90% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
45% {
|
||||||
|
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
|
||||||
|
transform: scale(var(--fa-beat-scale, 1.25)); } }
|
||||||
|
|
||||||
|
@keyframes fa-beat {
|
||||||
|
0%, 90% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
45% {
|
||||||
|
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
|
||||||
|
transform: scale(var(--fa-beat-scale, 1.25)); } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-bounce {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(0);
|
||||||
|
transform: scale(1, 1) translateY(0); }
|
||||||
|
10% {
|
||||||
|
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
|
||||||
|
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
|
||||||
|
30% {
|
||||||
|
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
|
||||||
|
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
|
||||||
|
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
|
||||||
|
57% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
|
||||||
|
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
|
||||||
|
64% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(0);
|
||||||
|
transform: scale(1, 1) translateY(0); }
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(0);
|
||||||
|
transform: scale(1, 1) translateY(0); } }
|
||||||
|
|
||||||
|
@keyframes fa-bounce {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(0);
|
||||||
|
transform: scale(1, 1) translateY(0); }
|
||||||
|
10% {
|
||||||
|
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
|
||||||
|
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
|
||||||
|
30% {
|
||||||
|
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
|
||||||
|
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
|
||||||
|
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
|
||||||
|
57% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
|
||||||
|
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
|
||||||
|
64% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(0);
|
||||||
|
transform: scale(1, 1) translateY(0); }
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(1, 1) translateY(0);
|
||||||
|
transform: scale(1, 1) translateY(0); } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-fade {
|
||||||
|
50% {
|
||||||
|
opacity: var(--fa-fade-opacity, 0.4); } }
|
||||||
|
|
||||||
|
@keyframes fa-fade {
|
||||||
|
50% {
|
||||||
|
opacity: var(--fa-fade-opacity, 0.4); } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-beat-fade {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: var(--fa-beat-fade-opacity, 0.4);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
|
||||||
|
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
|
||||||
|
|
||||||
|
@keyframes fa-beat-fade {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: var(--fa-beat-fade-opacity, 0.4);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1); }
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
|
||||||
|
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-flip {
|
||||||
|
50% {
|
||||||
|
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
|
||||||
|
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
|
||||||
|
|
||||||
|
@keyframes fa-flip {
|
||||||
|
50% {
|
||||||
|
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
|
||||||
|
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-shake {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(-15deg);
|
||||||
|
transform: rotate(-15deg); }
|
||||||
|
4% {
|
||||||
|
-webkit-transform: rotate(15deg);
|
||||||
|
transform: rotate(15deg); }
|
||||||
|
8%, 24% {
|
||||||
|
-webkit-transform: rotate(-18deg);
|
||||||
|
transform: rotate(-18deg); }
|
||||||
|
12%, 28% {
|
||||||
|
-webkit-transform: rotate(18deg);
|
||||||
|
transform: rotate(18deg); }
|
||||||
|
16% {
|
||||||
|
-webkit-transform: rotate(-22deg);
|
||||||
|
transform: rotate(-22deg); }
|
||||||
|
20% {
|
||||||
|
-webkit-transform: rotate(22deg);
|
||||||
|
transform: rotate(22deg); }
|
||||||
|
32% {
|
||||||
|
-webkit-transform: rotate(-12deg);
|
||||||
|
transform: rotate(-12deg); }
|
||||||
|
36% {
|
||||||
|
-webkit-transform: rotate(12deg);
|
||||||
|
transform: rotate(12deg); }
|
||||||
|
40%, 100% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg); } }
|
||||||
|
|
||||||
|
@keyframes fa-shake {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(-15deg);
|
||||||
|
transform: rotate(-15deg); }
|
||||||
|
4% {
|
||||||
|
-webkit-transform: rotate(15deg);
|
||||||
|
transform: rotate(15deg); }
|
||||||
|
8%, 24% {
|
||||||
|
-webkit-transform: rotate(-18deg);
|
||||||
|
transform: rotate(-18deg); }
|
||||||
|
12%, 28% {
|
||||||
|
-webkit-transform: rotate(18deg);
|
||||||
|
transform: rotate(18deg); }
|
||||||
|
16% {
|
||||||
|
-webkit-transform: rotate(-22deg);
|
||||||
|
transform: rotate(-22deg); }
|
||||||
|
20% {
|
||||||
|
-webkit-transform: rotate(22deg);
|
||||||
|
transform: rotate(22deg); }
|
||||||
|
32% {
|
||||||
|
-webkit-transform: rotate(-12deg);
|
||||||
|
transform: rotate(-12deg); }
|
||||||
|
36% {
|
||||||
|
-webkit-transform: rotate(12deg);
|
||||||
|
transform: rotate(12deg); }
|
||||||
|
40%, 100% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg); } }
|
||||||
|
|
||||||
|
@-webkit-keyframes fa-spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg); }
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
@keyframes fa-spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg); }
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
.fa-rotate-90 {
|
||||||
|
-webkit-transform: rotate(90deg);
|
||||||
|
transform: rotate(90deg); }
|
||||||
|
|
||||||
|
.fa-rotate-180 {
|
||||||
|
-webkit-transform: rotate(180deg);
|
||||||
|
transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.fa-rotate-270 {
|
||||||
|
-webkit-transform: rotate(270deg);
|
||||||
|
transform: rotate(270deg); }
|
||||||
|
|
||||||
|
.fa-flip-horizontal {
|
||||||
|
-webkit-transform: scale(-1, 1);
|
||||||
|
transform: scale(-1, 1); }
|
||||||
|
|
||||||
|
.fa-flip-vertical {
|
||||||
|
-webkit-transform: scale(1, -1);
|
||||||
|
transform: scale(1, -1); }
|
||||||
|
|
||||||
|
.fa-flip-both,
|
||||||
|
.fa-flip-horizontal.fa-flip-vertical {
|
||||||
|
-webkit-transform: scale(-1, -1);
|
||||||
|
transform: scale(-1, -1); }
|
||||||
|
|
||||||
|
.fa-rotate-by {
|
||||||
|
-webkit-transform: rotate(var(--fa-rotate-angle, none));
|
||||||
|
transform: rotate(var(--fa-rotate-angle, none)); }
|
||||||
|
|
||||||
|
.fa-stack {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
height: 2em;
|
||||||
|
position: relative;
|
||||||
|
width: 2.5em; }
|
||||||
|
|
||||||
|
.fa-stack-1x,
|
||||||
|
.fa-stack-2x {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--fa-stack-z-index, auto); }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-stack-1x {
|
||||||
|
height: 1em;
|
||||||
|
width: 1.25em; }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-stack-2x {
|
||||||
|
height: 2em;
|
||||||
|
width: 2.5em; }
|
||||||
|
|
||||||
|
.fa-inverse {
|
||||||
|
color: var(--fa-inverse, #fff); }
|
||||||
|
|
||||||
|
.sr-only,
|
||||||
|
.fa-sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0; }
|
||||||
|
|
||||||
|
.sr-only-focusable:not(:focus),
|
||||||
|
.fa-sr-only-focusable:not(:focus) {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0; }
|
||||||
|
|
||||||
|
.svg-inline--fa .fa-primary {
|
||||||
|
fill: var(--fa-primary-color, currentColor);
|
||||||
|
opacity: var(--fa-primary-opacity, 1); }
|
||||||
|
|
||||||
|
.svg-inline--fa .fa-secondary {
|
||||||
|
fill: var(--fa-secondary-color, currentColor);
|
||||||
|
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-swap-opacity .fa-primary {
|
||||||
|
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||||
|
|
||||||
|
.svg-inline--fa.fa-swap-opacity .fa-secondary {
|
||||||
|
opacity: var(--fa-primary-opacity, 1); }
|
||||||
|
|
||||||
|
.svg-inline--fa mask .fa-primary,
|
||||||
|
.svg-inline--fa mask .fa-secondary {
|
||||||
|
fill: black; }
|
||||||
|
|
||||||
|
.fad.fa-inverse,
|
||||||
|
.fa-duotone.fa-inverse {
|
||||||
|
color: var(--fa-inverse, #fff); }
|
||||||
6
dashboard/assets/font-awesome/css/svg-with-js.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/svg-with-js.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
19
dashboard/assets/font-awesome/css/thin.css
vendored
Normal file
19
dashboard/assets/font-awesome/css/thin.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:root, :host {
|
||||||
|
--fa-style-family-classic: 'Font Awesome 6 Pro';
|
||||||
|
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Pro'; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-thin-100.woff2") format("woff2"), url("../webfonts/fa-thin-100.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
.fat,
|
||||||
|
.fa-thin {
|
||||||
|
font-weight: 100; }
|
||||||
6
dashboard/assets/font-awesome/css/thin.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/thin.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
:host,:root{--fa-style-family-classic:"Font Awesome 6 Pro";--fa-font-thin:normal 100 1em/1 "Font Awesome 6 Pro"}@font-face{font-family:"Font Awesome 6 Pro";font-style:normal;font-weight:100;font-display:block;src:url(../webfonts/fa-thin-100.woff2) format("woff2"),url(../webfonts/fa-thin-100.ttf) format("truetype")}.fa-thin,.fat{font-weight:100}
|
||||||
26
dashboard/assets/font-awesome/css/v4-font-face.css
vendored
Normal file
26
dashboard/assets/font-awesome/css/v4-font-face.css
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
|
||||||
|
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FontAwesome';
|
||||||
|
font-display: block;
|
||||||
|
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
|
||||||
|
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }
|
||||||
6
dashboard/assets/font-awesome/css/v4-font-face.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/v4-font-face.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
|
||||||
2194
dashboard/assets/font-awesome/css/v4-shims.css
vendored
Normal file
2194
dashboard/assets/font-awesome/css/v4-shims.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
dashboard/assets/font-awesome/css/v4-shims.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/v4-shims.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
34
dashboard/assets/font-awesome/css/v5-font-face.css
vendored
Normal file
34
dashboard/assets/font-awesome/css/v5-font-face.css
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Brands';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Pro';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Pro';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Pro';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url("../webfonts/fa-light-300.woff2") format("woff2"), url("../webfonts/fa-light-300.ttf") format("truetype"); }
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 5 Duotone';
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url("../webfonts/fa-duotone-900.woff2") format("woff2"), url("../webfonts/fa-duotone-900.ttf") format("truetype"); }
|
||||||
6
dashboard/assets/font-awesome/css/v5-font-face.min.css
vendored
Normal file
6
dashboard/assets/font-awesome/css/v5-font-face.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*!
|
||||||
|
* Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com
|
||||||
|
* License - https://fontawesome.com/license (Commercial License)
|
||||||
|
* Copyright 2023 Fonticons, Inc.
|
||||||
|
*/
|
||||||
|
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Pro";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Pro";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Pro";font-display:block;font-weight:300;src:url(../webfonts/fa-light-300.woff2) format("woff2"),url(../webfonts/fa-light-300.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Duotone";font-display:block;font-weight:900;src:url(../webfonts/fa-duotone-900.woff2) format("woff2"),url(../webfonts/fa-duotone-900.ttf) format("truetype")}
|
||||||
BIN
dashboard/assets/font-awesome/webfonts/fa-brands-400.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-brands-400.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-duotone-900.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-duotone-900.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-duotone-900.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-duotone-900.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-light-300.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-light-300.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-light-300.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-light-300.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-regular-400.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-regular-400.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-light-300.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-light-300.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-light-300.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-light-300.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-regular-400.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-regular-400.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-solid-900.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-solid-900.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-solid-900.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-solid-900.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-thin-100.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-thin-100.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-thin-100.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-sharp-thin-100.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-solid-900.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-solid-900.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-thin-100.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-thin-100.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-thin-100.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-thin-100.woff2
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-v4compatibility.ttf
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
dashboard/assets/font-awesome/webfonts/fa-v4compatibility.woff2
Normal file
BIN
dashboard/assets/font-awesome/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
85
dashboard/assets/scss/main.scss
Normal file
85
dashboard/assets/scss/main.scss
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;0,1000;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900;1,1000&display=swap');
|
||||||
|
@import url('https://fonts.cdnfonts.com/css/brockmann');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||||
|
|
||||||
|
@import '../font-awesome/css/all.css';
|
||||||
|
@import './utilities.scss';
|
||||||
|
|
||||||
|
@import url('https://fonts.cdnfonts.com/css/geometric-sans-serif-v1');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
|
// @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css");
|
||||||
|
|
||||||
|
.fas,
|
||||||
|
.far,
|
||||||
|
.fat {
|
||||||
|
font-family: "Font Awesome 6 Pro" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
font-family: "Font Awesome 6 Brands" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brockmann {
|
||||||
|
font-family: "Brockmann" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nunito {
|
||||||
|
font-family: "Nunito" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inter {
|
||||||
|
font-family: "Inter" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.geometric {
|
||||||
|
font-family: 'Geometric Sans Serif v1' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manrope {
|
||||||
|
font-family: 'Manrope' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lato {
|
||||||
|
font-family: 'Lato' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poppins {
|
||||||
|
font-family: 'Poppins' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poppins-childs {
|
||||||
|
font-family: 'Poppins' !important;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Poppins' !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: 0 0 18px #00000033;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100dvw;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Nunito';
|
||||||
|
}
|
||||||
15
dashboard/assets/scss/utilities.scss
Normal file
15
dashboard/assets/scss/utilities.scss
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.test0 {
|
||||||
|
@apply border-gray-400/20 border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test {
|
||||||
|
border: 1px solid yellow !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test2 {
|
||||||
|
border: 2px solid blue !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test3 {
|
||||||
|
border: 3px solid green !important;
|
||||||
|
}
|
||||||
110
dashboard/components/AdvancedLineChart.vue
Normal file
110
dashboard/components/AdvancedLineChart.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: any[],
|
||||||
|
labels: string[]
|
||||||
|
color: string,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
// borderDash: [5, 10]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 },
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 4,
|
||||||
|
boxPadding: 10,
|
||||||
|
caretPadding: 20,
|
||||||
|
yAlign: 'bottom',
|
||||||
|
xAlign: 'center',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line'>>({
|
||||||
|
labels: props.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: props.data,
|
||||||
|
backgroundColor: [props.color + '77'],
|
||||||
|
borderColor: props.color,
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 10,
|
||||||
|
hoverBackgroundColor: props.color,
|
||||||
|
hoverBorderColor: 'white',
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
const ctx = c.getContext("2d");
|
||||||
|
let gradient: any = `${props.color}22`;
|
||||||
|
if (ctx) {
|
||||||
|
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
gradient.addColorStop(0, `${props.color}99`);
|
||||||
|
gradient.addColorStop(0.35, `${props.color}66`);
|
||||||
|
gradient.addColorStop(1, `${props.color}22`);
|
||||||
|
} else {
|
||||||
|
console.warn('Cannot get context for gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
chartData.value.datasets[0].backgroundColor = [gradient];
|
||||||
|
|
||||||
|
watch(props, () => {
|
||||||
|
console.log('UPDATE')
|
||||||
|
chartData.value.labels = props.labels;
|
||||||
|
chartData.value.datasets[0].data = props.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</template>
|
||||||
116
dashboard/components/AdvancedStackedBarChart.vue
Normal file
116
dashboard/components/AdvancedStackedBarChart.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js';
|
||||||
|
import { useBarChart, BarChart } from 'vue-chart-3';
|
||||||
|
registerChartComponents();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
datasets: any[],
|
||||||
|
labels: string[],
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'bar'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
includeInvisible: true
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
ticks: { display: true },
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
drawBorder: false,
|
||||||
|
color: '#CCCCCC22',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
titleFont: { size: 16, weight: 'bold' },
|
||||||
|
bodyFont: { size: 14 },
|
||||||
|
padding: 10,
|
||||||
|
cornerRadius: 4,
|
||||||
|
boxPadding: 10,
|
||||||
|
caretPadding: 20,
|
||||||
|
yAlign: 'bottom',
|
||||||
|
xAlign: 'center',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'bar'>>({
|
||||||
|
labels: props.labels,
|
||||||
|
datasets: props.datasets.map(e => {
|
||||||
|
return {
|
||||||
|
data: e.data,
|
||||||
|
label: e.label || '?',
|
||||||
|
backgroundColor: [e.color],
|
||||||
|
borderWidth: 0,
|
||||||
|
borderRadius: 8
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { barChartProps, barChartRef } = useBarChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// const c = document.createElement('canvas');
|
||||||
|
// const ctx = c.getContext("2d");
|
||||||
|
// let gradient: any = `${props.color}22`;
|
||||||
|
// if (ctx) {
|
||||||
|
// gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||||
|
// gradient.addColorStop(0, `${props.color}99`);
|
||||||
|
// gradient.addColorStop(0.35, `${props.color}66`);
|
||||||
|
// gradient.addColorStop(1, `${props.color}22`);
|
||||||
|
// } else {
|
||||||
|
// console.warn('Cannot get context for gradient');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// chartData.value.datasets[0].backgroundColor = [gradient];
|
||||||
|
|
||||||
|
watch(props, () => {
|
||||||
|
console.log('UPDATE')
|
||||||
|
chartData.value.labels = props.labels;
|
||||||
|
chartData.value.datasets.length = 0;
|
||||||
|
chartData.value.datasets = props.datasets.map(e => {
|
||||||
|
return {
|
||||||
|
data: e.data,
|
||||||
|
label: e.label || '?',
|
||||||
|
backgroundColor: [e.color],
|
||||||
|
borderWidth: 0,
|
||||||
|
borderRadius: 8
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BarChart v-bind="barChartProps"> </BarChart>
|
||||||
|
</template>
|
||||||
14
dashboard/components/CButton.vue
Normal file
14
dashboard/components/CButton.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const props = defineProps<{ label: string, disabled?: boolean, loading?: boolean }>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="{ '!bg-[#354a87] !text-text/50 !cursor-not-allowed': (disabled || loading) }"
|
||||||
|
class="bg-accent text-text px-4 py-2 text-center cursor-pointer hover:bg-[#5075e2] hover:text-text-sub">
|
||||||
|
<span v-show="!loading">{{ label }}</span>
|
||||||
|
<i v-if="loading" class="fas fa-loader animate-[spin_2s_linear_infinite]"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
dashboard/components/CInput.vue
Normal file
31
dashboard/components/CInput.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: string, placeholder?: string, readonly?: boolean }>();
|
||||||
|
const emits = defineEmits(['update:modelValue', 'change']);
|
||||||
|
|
||||||
|
function updateText(e: any) {
|
||||||
|
emits('update:modelValue', e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitChange(e: any) {
|
||||||
|
emits('change', e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input @change="emitChange" :readonly="readonly" :value="modelValue" @input="updateText"
|
||||||
|
class="placeholder:text-text-sub/70 w-full read-only:bg-white/10 read-only:text-text-sub/60 placeholder:text-text-sub border-gray-400 bg-bg border-[1px] text-text rounded-md px-4 py-2"
|
||||||
|
:placeholder="placeholder" type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
dashboard/components/CVerticalNavigation.vue
Normal file
97
dashboard/components/CVerticalNavigation.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
export type Entry = {
|
||||||
|
label: string,
|
||||||
|
disabled?: boolean,
|
||||||
|
to?: string,
|
||||||
|
icon?: string,
|
||||||
|
action?: () => any,
|
||||||
|
adminOnly?: boolean,
|
||||||
|
external?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Section = {
|
||||||
|
title: string,
|
||||||
|
entries: Entry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sections: Section[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isOpen, open, close, toggle } = useMenu()
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const { isAdmin } = useUserRoles();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-0 md:w-[5rem] absolute top-0 md:relative h-full">
|
||||||
|
<div @mouseover="open()" @mouseleave="close()"
|
||||||
|
class="CVerticalNavigation absolute z-[80] bg-menu h-full overflow-hidden w-0 md:w-[5rem]"
|
||||||
|
:class="{ '!w-[18rem] shadow-[0_0_20px_#000000] rounded-r-2xl': isOpen }">
|
||||||
|
<div :class="{ 'w-[18rem]': isOpen }">
|
||||||
|
<div class="flex gap-4 items-center py-6 px-[.9rem] pb-8">
|
||||||
|
<div class="bg-accent h-[2.8rem] aspect-[1/1] flex items-center justify-center rounded-lg">
|
||||||
|
<img class="h-[2.4rem]" :src="'/logo.png'">
|
||||||
|
</div>
|
||||||
|
<div v-if="isOpen" class="font-bold text-[1.4rem] text-gray-300"> Litlyx </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-8" v-for="section of sections">
|
||||||
|
|
||||||
|
<div class="flex flex-col px-3 gap-2">
|
||||||
|
|
||||||
|
<template v-for="entry of section.entries">
|
||||||
|
|
||||||
|
<NuxtLink @click="entry.action?.()" :target="entry.external ? '_blank' : ''"
|
||||||
|
v-if="entry.to && (!entry.adminOnly || (isAdmin && !isAdminHidden))" tag="div"
|
||||||
|
:to="entry.to || '/'"
|
||||||
|
class="text-[#a3a9b6] flex w-full items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-[#363638] hover:text-[#ffffff]"
|
||||||
|
:class="{
|
||||||
|
'brightness-[.4] pointer-events-none': entry.disabled,
|
||||||
|
'bg-[#363638] shadow-[0px_0px_2px_#ffffff20_inset] border-[#ffffff20] border-[1px] !text-[#ffffff]': route.path == (entry.to || '#')
|
||||||
|
}">
|
||||||
|
<div class="flex items-center text-[1.4rem] w-[1.8rem] justify-center">
|
||||||
|
<i :class="entry.icon"></i>
|
||||||
|
</div>
|
||||||
|
<div v-if="isOpen" class="text-[.9rem] font-bold manrope"> {{ entry.label }} </div>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<div v-if="!entry.to" @click="entry.action?.()"
|
||||||
|
class="text-[#a3a9b6] flex w-full items-center gap-3 p-3 rounded-lg cursor-pointer hover:bg-[#363638] hover:text-[#ffffff]">
|
||||||
|
<div class="flex items-center text-[1.4rem] w-[1.8rem] justify-center">
|
||||||
|
<i :class="entry.icon"></i>
|
||||||
|
</div>
|
||||||
|
<div v-if="isOpen" class="text-[.9rem] font-bold manrope"> {{ entry.label }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.CVerticalNavigation {
|
||||||
|
transition: all .25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CVerticalNavigation * {
|
||||||
|
font-family: 'Inter';
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
dashboard/components/Card.vue
Normal file
9
dashboard/components/Card.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-card card-shadow rounded-xl">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
27
dashboard/components/CardTitled.vue
Normal file
27
dashboard/components/CardTitled.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const props = defineProps<{ title: string, sub?: string }>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="poppins font-semibold text-[1.1rem] md:text-[1.4rem] text-text">
|
||||||
|
{{ props.title }}
|
||||||
|
</div>
|
||||||
|
<div v-if="props.sub" class="poppins text-[.8rem] md:text-[1.1rem] text-text-sub">
|
||||||
|
{{ props.sub }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
557
dashboard/components/GlobeSvg.vue
Normal file
557
dashboard/components/GlobeSvg.vue
Normal file
File diff suppressed because one or more lines are too long
15
dashboard/components/MobileOnly.vue
Normal file
15
dashboard/components/MobileOnly.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="hidden lg:flex">
|
||||||
|
<slot name="desktop"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
34
dashboard/components/SelectButton.vue
Normal file
34
dashboard/components/SelectButton.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: { label: string }[],
|
||||||
|
currentIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(evt: 'changeIndex', newIndex: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex gap-2 border-[1px] border-gray-400 p-1 md:p-2 rounded-xl">
|
||||||
|
<div @click="$emit('changeIndex', index)" v-for="(opt, index) of options"
|
||||||
|
class="hover:bg-white/10 select-btn-animated cursor-pointer rounded-lg poppins font-semibold px-2 md:px-3 py-1 text-[.8rem] md:text-[1rem]"
|
||||||
|
:class="{ 'bg-accent hover:!bg-accent': currentIndex == index }">
|
||||||
|
{{ opt.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.select-btn-animated {
|
||||||
|
transition: all .4s linear;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
dashboard/components/TypeWriter.vue
Normal file
15
dashboard/components/TypeWriter.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const props = defineProps<{ text: string }>();
|
||||||
|
const currentText = ref<string>("");
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>{{ text }}</div>
|
||||||
|
</template>
|
||||||
156
dashboard/components/dashboard/BarsCard.vue
Normal file
156
dashboard/components/dashboard/BarsCard.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
export type IconProvider = (id: string) => ['img' | 'icon', string] | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: { _id: string, count: number }[],
|
||||||
|
iconProvider?: IconProvider,
|
||||||
|
elementTextTransformer?: (text: string) => string,
|
||||||
|
label: string,
|
||||||
|
subLabel: string,
|
||||||
|
desc: string,
|
||||||
|
loading?: boolean,
|
||||||
|
interactive?: boolean,
|
||||||
|
isDetailView?: boolean,
|
||||||
|
rawButton?: boolean,
|
||||||
|
hideShowMore?: boolean
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: 'dataReload'): void,
|
||||||
|
(e: 'showDetails', id: string): void,
|
||||||
|
(e: 'showRawData'): void,
|
||||||
|
(e: 'showGeneral'): void,
|
||||||
|
(e: 'showMore'): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const maxData = computed(() => {
|
||||||
|
const counts = props.data.map(e => e.count);
|
||||||
|
return Math.max(...counts);
|
||||||
|
});
|
||||||
|
|
||||||
|
function reloadData() {
|
||||||
|
emits('dataReload');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDetails(id: string) {
|
||||||
|
emits('showDetails', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
|
||||||
|
<div class="text-text flex flex-col items-start gap-4 w-full relative">
|
||||||
|
|
||||||
|
<div class="w-full p-4 flex flex-col bg-menu rounded-xl gap-8 card-shadow">
|
||||||
|
|
||||||
|
<div class="flex justify-between mb-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<div class="poppins font-semibold text-[1.4rem] text-text">
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i @click="reloadData()"
|
||||||
|
class="hover:rotate-[50deg] transition-all duration-100 fas fa-refresh text-[1.2rem] cursor-pointer"></i>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-[1rem] text-text-sub/90">
|
||||||
|
{{ desc }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="rawButton" class="hidden lg:flex">
|
||||||
|
<div @click="$emit('showRawData')"
|
||||||
|
class="cursor-pointer hover:bg-accent/60 flex items-center justify-center poppins bg-accent rounded-lg py-2 px-8">
|
||||||
|
Raw data
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between font-bold text-text-sub/80 text-[1.1rem] mb-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div v-if="isDetailView" class="flex items-center justify-center">
|
||||||
|
<i @click="$emit('showGeneral')"
|
||||||
|
class="fas fa-arrow-left text-[.9rem] hover:text-text cursor-pointer"></i>
|
||||||
|
</div>
|
||||||
|
<div> {{ subLabel }} </div>
|
||||||
|
</div>
|
||||||
|
<div> Count </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div v-if="props.data.length > 0" class="flex justify-between" v-for="element of props.data">
|
||||||
|
<div class="w-10/12 relative" @click="showDetails(element._id)"
|
||||||
|
:class="{ 'cursor-pointer line-active': interactive }">
|
||||||
|
<div class="absolute rounded-sm w-full h-full bg-[#92abcf38]"
|
||||||
|
:style="'width:' + 100 / maxData * element.count + '%;'"></div>
|
||||||
|
<div class="flex px-2 py-1 relative items-center gap-4">
|
||||||
|
<div v-if="iconProvider && iconProvider(element._id) != undefined"
|
||||||
|
class="flex items-center h-[1.3rem]">
|
||||||
|
<img v-if="iconProvider(element._id)?.[0] == 'img'" class="h-full"
|
||||||
|
:src="iconProvider(element._id)?.[1]">
|
||||||
|
<i v-else :class="iconProvider(element._id)?.[1]"></i>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-ellipsis line-clamp-1 ui-font z-[20] text-[.95rem] text-text/70">
|
||||||
|
{{ elementTextTransformer?.(element._id) || element._id }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-text font-semibold manrope"> {{ formatNumberK(element.count) }} </div>
|
||||||
|
</div>
|
||||||
|
<div v-if="props.data.length == 0"
|
||||||
|
class="flex justify-center text-text-sub font-bold text-[1.1rem]">
|
||||||
|
No visits yet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hideShowMore" class="flex justify-center mt-4 text-text-sub/90 ">
|
||||||
|
<div @click="$emit('showMore')"
|
||||||
|
class="poppins hover:bg-black cursor-pointer w-fit px-6 py-1 rounded-lg border-[1px] border-text-sub text-[.9rem]">
|
||||||
|
Show more
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading"
|
||||||
|
class="backdrop-blur-[1px] z-[20] left-0 top-0 w-full h-full flex items-center justify-center font-bold rockmann absolute">
|
||||||
|
<i
|
||||||
|
class="fas fa-spinner text-[2rem] text-accent animate-[spin_1s_linear_infinite] duration-500"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.line-active:hover {
|
||||||
|
.absolute {
|
||||||
|
@apply bg-accent/20
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-font {
|
||||||
|
font-feature-settings: normal;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
font-variation-settings: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
dashboard/components/dashboard/BrowsersBarCard.vue
Normal file
36
dashboard/components/dashboard/BrowsersBarCard.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { BrowsersAggregated } from '~/server/api/metrics/[project_id]/data/browsers';
|
||||||
|
|
||||||
|
const activeProject = await useActiveProject();
|
||||||
|
const { data: events, pending, refresh } = await useFetch<BrowsersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/browsers`, signHeaders());
|
||||||
|
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogBarData.value = [];
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
$fetch<any[]>(`/api/metrics/${activeProject.value?._id}/data/browsers`, signHeaders({
|
||||||
|
'x-query-limit': '200'
|
||||||
|
})).then(data => {
|
||||||
|
dialogBarData.value = data;
|
||||||
|
isDataLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard @showMore="showMore()" @dataReload="refresh" :data="events || []"
|
||||||
|
desc="The browsers most used to search your website." :dataIcons="false" :loading="pending"
|
||||||
|
label="Top Browsers" sub-label="Browsers"></DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
68
dashboard/components/dashboard/CountCard.vue
Normal file
68
dashboard/components/dashboard/CountCard.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
icon: string,
|
||||||
|
value: string,
|
||||||
|
text: string,
|
||||||
|
avg?: string,
|
||||||
|
trend?: number,
|
||||||
|
color: string,
|
||||||
|
data?: number[],
|
||||||
|
labels?: string[],
|
||||||
|
ready?: boolean
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<Card class="flex flex-col overflow-hidden relative max-h-[12rem] aspect-[2/1] w-full">
|
||||||
|
<div class="flex p-4 items-start">
|
||||||
|
<div class="flex items-center mt-2 mr-4">
|
||||||
|
<i :style="`color: ${props.color}`" :class="icon" class="text-[1.6rem] 2xl:text-[2rem]"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col grow">
|
||||||
|
<div class="flex items-end gap-2">
|
||||||
|
<div class="brockmann text-text-dirty text-[1.6rem] 2xl:text-[2rem]"> {{ value }} </div>
|
||||||
|
<div class="poppins text-text-sub text-[.7rem] 2xl:text-[.85rem] mb-2"> {{ avg }} </div>
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-text-sub text-[.9rem] 2xl:text-base"> {{ text }} </div>
|
||||||
|
</div>
|
||||||
|
<div v-if="trend" class="flex items-center gap-3 rounded-xl px-2 py-1"
|
||||||
|
:style="`background-color: ${props.color}33`">
|
||||||
|
<i :class="trend > 0 ? 'fa-arrow-trend-up' : 'fa-arrow-trend-down'" class="far text-[.9rem] 2xl:text-[1rem]"
|
||||||
|
:style="`color: ${props.color}`"></i>
|
||||||
|
<div :style="`color: ${props.color}`" class="font-semibold text-[.75rem] 2xl:text-[.875rem]">
|
||||||
|
{{ trend.toFixed(0) }} %
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 w-full h-[50%] flex items-end" v-if="(props.data?.length || 0) > 0">
|
||||||
|
<DashboardEmbedChartCard v-if="ready" :data="props.data || []" :labels="props.labels || []" :color="props.color">
|
||||||
|
</DashboardEmbedChartCard>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- <div class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full lg:w-[20rem] relative pb-2 lg:pb-4">
|
||||||
|
|
||||||
|
<div class="gap-4 flex flex-row items-center lg:items-start lg:gap-2 lg:flex-col">
|
||||||
|
<div class="w-[2.5rem] h-[2.5rem] lg:w-[3.5rem] lg:h-[3.5rem] flex items-center justify-center rounded-lg"
|
||||||
|
:style="`background: ${props.color}`">
|
||||||
|
<i :class="icon" class="text-[1rem] lg:text-[1.5rem]"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-[1rem] lg:text-[1.3rem] text-text-sub/90 poppins">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center lg:items-end">
|
||||||
|
<div class="brockmann text-text text-[2rem] lg:text-[2.8rem] grow">
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-text-sub/90 text-[.9rem] lg:text-[1rem]"> {{ sub }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</template>
|
||||||
40
dashboard/components/dashboard/CountCardOld.vue
Normal file
40
dashboard/components/dashboard/CountCardOld.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
icon: string,
|
||||||
|
title: string,
|
||||||
|
text: string,
|
||||||
|
sub: string,
|
||||||
|
color: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full lg:w-[20rem] relative pb-2 lg:pb-4">
|
||||||
|
|
||||||
|
<!-- <div class="absolute flex items-center justify-center right-4 top-4 cursor-pointer hover:text-blue-400">
|
||||||
|
<i class="fal fa-info-circle text-[.9rem] lg:text-[1.4rem]"></i>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="gap-4 flex flex-row items-center lg:items-start lg:gap-2 lg:flex-col">
|
||||||
|
<div class="w-[2.5rem] h-[2.5rem] lg:w-[3.5rem] lg:h-[3.5rem] flex items-center justify-center rounded-lg"
|
||||||
|
:style="`background: ${props.color}`">
|
||||||
|
<i :class="icon" class="text-[1rem] lg:text-[1.5rem]"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-[1rem] lg:text-[1.3rem] text-text-sub/90 poppins">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center lg:items-end">
|
||||||
|
<div class="brockmann text-text text-[2rem] lg:text-[2.8rem] grow">
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-text-sub/90 text-[.9rem] lg:text-[1rem]"> {{ sub }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
36
dashboard/components/dashboard/DevicesBarCard.vue
Normal file
36
dashboard/components/dashboard/DevicesBarCard.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { DevicesAggregated } from '~/server/api/metrics/[project_id]/data/devices';
|
||||||
|
|
||||||
|
const activeProject = await useActiveProject();
|
||||||
|
const { data: events, pending, refresh } = await useFetch<DevicesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/devices`, signHeaders());
|
||||||
|
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogBarData.value = [];
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
$fetch<any[]>(`/api/metrics/${activeProject.value?._id}/data/devices`, signHeaders({
|
||||||
|
'x-query-limit': '200'
|
||||||
|
})).then(data => {
|
||||||
|
dialogBarData.value = data;
|
||||||
|
isDataLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard @showMore="showMore()" @dataReload="refresh" :data="events || []" :dataIcons="false"
|
||||||
|
desc="The devices most used to access your website." :loading="pending" label="Top Devices"
|
||||||
|
sub-label="Devices"></DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
36
dashboard/components/dashboard/DialogBarCard.vue
Normal file
36
dashboard/components/dashboard/DialogBarCard.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const { dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: '_id', label: 'Value' },
|
||||||
|
{ key: 'count', label: 'Count' },
|
||||||
|
];
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="w-full h-full bg-bg rounded-xl p-8">
|
||||||
|
<div class="full h-full overflow-y-auto">
|
||||||
|
<UTable :columns="columns" :rows="dialogBarData" :loading="isDataLoading" v-if="dialogBarData">
|
||||||
|
<template #count-data="{ row }">
|
||||||
|
<div class="font-bold"> {{ formatNumberK(row.count) }} </div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #_id-data="{ row }">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div v-if="row.icon && row.icon[0] == 'img'" class="w-5 h-5">
|
||||||
|
<img class="w-full h-full" :src="row.icon[1]">
|
||||||
|
</div>
|
||||||
|
<i v-if="row.icon && row.icon[0] == 'icon'" :class="row.icon[1]"></i>
|
||||||
|
<div> {{ row._id }} </div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
67
dashboard/components/dashboard/EmbedChartCard.vue
Normal file
67
dashboard/components/dashboard/EmbedChartCard.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Chart, registerables, type ChartData, type ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
if (process.client) Chart.register(...registerables);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: any[],
|
||||||
|
labels: string[]
|
||||||
|
color: string,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
aspectRatio: 0,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: false },
|
||||||
|
grid: { display: false }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: false },
|
||||||
|
grid: { display: false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
title: { display: false },
|
||||||
|
tooltip: { enabled: false },
|
||||||
|
subtitle: { display: false },
|
||||||
|
decimation: { enabled: false },
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
top: 6,
|
||||||
|
bottom: -8,
|
||||||
|
left: -8,
|
||||||
|
right: -8
|
||||||
|
},
|
||||||
|
autoPadding: false
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'line'>>({
|
||||||
|
labels: props.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: props.data,
|
||||||
|
backgroundColor: [props.color + '77'],
|
||||||
|
borderColor: props.color,
|
||||||
|
borderWidth: 4,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.45,
|
||||||
|
pointRadius: 0
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
// onMounted(() => { })
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LineChart class="max-h-full max-w-full w-full h-full" ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||||
|
</template>
|
||||||
41
dashboard/components/dashboard/EventsBarCard.vue
Normal file
41
dashboard/components/dashboard/EventsBarCard.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { CustomEventsAggregated } from '~/server/api/metrics/[project_id]/visits/events';
|
||||||
|
|
||||||
|
const activeProject = await useActiveProject();
|
||||||
|
const { data: events, pending, refresh } = await useFetch<CustomEventsAggregated[]>(`/api/metrics/${activeProject.value?._id}/visits/events`, signHeaders());
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function goToView() {
|
||||||
|
router.push('/dashboard/events');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogBarData.value = [];
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
$fetch<any[]>(`/api/metrics/${activeProject.value?._id}/visits/events`, signHeaders({
|
||||||
|
'x-query-limit': '200'
|
||||||
|
})).then(data => {
|
||||||
|
dialogBarData.value = data;
|
||||||
|
isDataLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard @showMore="showMore()" @showRawData="goToView()" desc="Most frequent user events triggered in this project" @dataReload="refresh" :data="events || []" :loading="pending" label="Top Events"
|
||||||
|
sub-label="Events" :rawButton="!isLiveDemo()"></DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
91
dashboard/components/dashboard/EventsChart.vue
Normal file
91
dashboard/components/dashboard/EventsChart.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { Chart, registerables, type ChartData, type ChartOptions } from 'chart.js';
|
||||||
|
import { DoughnutChart, useDoughnutChart } from 'vue-chart-3';
|
||||||
|
import type { EventsPie } from '~/server/api/metrics/[project_id]/events_pie';
|
||||||
|
|
||||||
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
|
if (process.client) Chart.register(...registerables);
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'doughnut'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: false },
|
||||||
|
grid: { display: false, drawBorder: false },
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: false },
|
||||||
|
grid: { display: false, drawBorder: false },
|
||||||
|
},
|
||||||
|
// r: {
|
||||||
|
// ticks: { display: false },
|
||||||
|
// grid: {
|
||||||
|
// display: true,
|
||||||
|
// drawBorder: false,
|
||||||
|
// color: '#CCCCCC22',
|
||||||
|
// borderDash: [20, 8]
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
align: 'center',
|
||||||
|
labels: {
|
||||||
|
color: 'white',
|
||||||
|
font: {
|
||||||
|
family: 'Poppins',
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = ref<ChartData<'doughnut'>>({
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
rotation: 1,
|
||||||
|
data: [],
|
||||||
|
backgroundColor: ['#6bbbe3','#5655d0', '#a6d5cb', '#fae0b9'],
|
||||||
|
borderColor: ['#1d1d1f'],
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
const activeProject = useActiveProject()
|
||||||
|
|
||||||
|
const eventsData = await $fetch<EventsPie[]>(`/api/metrics/${activeProject.value?._id}/visits/events`, signHeaders());
|
||||||
|
chartData.value.labels = eventsData.map(e => {
|
||||||
|
return `${e._id}`;
|
||||||
|
});
|
||||||
|
chartData.value.datasets[0].data = eventsData.map(e => e.count);
|
||||||
|
doughnutChartRef.value?.update();
|
||||||
|
|
||||||
|
if (window.innerWidth < 800) {
|
||||||
|
if (chartOptions.value?.plugins?.legend?.display) {
|
||||||
|
chartOptions.value.plugins.legend.display = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DoughnutChart v-bind="doughnutChartProps"> </DoughnutChart>
|
||||||
|
</template>
|
||||||
73
dashboard/components/dashboard/EventsPie.vue
Normal file
73
dashboard/components/dashboard/EventsPie.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<!-- <script lang="ts" setup>
|
||||||
|
|
||||||
|
import { Chart, registerables, type ChartData, type ChartOptions } from 'chart.js';
|
||||||
|
import { PieChart, usePieChart } from 'vue-chart-3';
|
||||||
|
import type { EventsPie } from '~/server/api/metrics/[project_id]/events_pie';
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
Chart.register(...registerables);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = await useCurrentProject();
|
||||||
|
|
||||||
|
const { data: eventsPieData } = await useFetch<EventsPie[]>(`/api/metrics/${project.value?._id}/events_pie`, signHeaders());
|
||||||
|
|
||||||
|
const eventsTimelineOptions = ref<ChartOptions<'pie'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Events',
|
||||||
|
color: '#EEECF6',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsTimelineData = computed<ChartData<'pie'>>(() => ({
|
||||||
|
labels: (eventsPieData.value || []).map((e: EventsPie) => {
|
||||||
|
return e._id;
|
||||||
|
}),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: (eventsPieData.value || []).map((e: EventsPie) => {
|
||||||
|
return e.count;
|
||||||
|
}),
|
||||||
|
backgroundColor: [
|
||||||
|
"#295270",
|
||||||
|
"#304F71",
|
||||||
|
"#374C72",
|
||||||
|
"#3E4A73",
|
||||||
|
"#444773",
|
||||||
|
"#4B4474",
|
||||||
|
"#524175",
|
||||||
|
],
|
||||||
|
borderColor: '#222222'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const { pieChartProps } = usePieChart({ chartData: eventsTimelineData, options: eventsTimelineOptions });
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="graph">
|
||||||
|
<PieChart v-bind="pieChartProps">
|
||||||
|
</PieChart>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template> -->
|
||||||
35
dashboard/components/dashboard/GeolocationBarCard.vue
Normal file
35
dashboard/components/dashboard/GeolocationBarCard.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { CountriesAggregated } from '~/server/api/metrics/[project_id]/data/countries';
|
||||||
|
|
||||||
|
const activeProject = await useActiveProject();
|
||||||
|
const { data: countries, pending, refresh } = await useFetch<CountriesAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/countries`, signHeaders());
|
||||||
|
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogBarData.value = [];
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
$fetch<any[]>(`/api/metrics/${activeProject.value?._id}/data/countries`, signHeaders({
|
||||||
|
'x-query-limit': '200'
|
||||||
|
})).then(data => {
|
||||||
|
dialogBarData.value = data;
|
||||||
|
isDataLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard @showMore="showMore()" @dataReload="refresh" :data="countries || []" :dataIcons="false" :loading="pending"
|
||||||
|
label="Top Countries" sub-label="Countries" desc=" Lists the countries where users access your website."></DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
58
dashboard/components/dashboard/MiniChart.vue
Normal file
58
dashboard/components/dashboard/MiniChart.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { Chart, registerables, type ChartData, type ChartOptions } from 'chart.js';
|
||||||
|
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||||
|
|
||||||
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
|
if (process.client) Chart.register(...registerables);
|
||||||
|
|
||||||
|
type Props = { xs?: any[], ys?: any[], color: string, border: string }
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const chartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { display: false },
|
||||||
|
grid: { display: false, drawBorder: false },
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: false },
|
||||||
|
grid: { display: false, drawBorder: false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
title: { display: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = computed<ChartData<'line'>>(() => ({
|
||||||
|
labels: (props.xs || []),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: (props.ys || []),
|
||||||
|
backgroundColor: [props.color],
|
||||||
|
borderColor: props.border,
|
||||||
|
borderWidth: 3,
|
||||||
|
fill: true,
|
||||||
|
pointRadius: 0,
|
||||||
|
lineTension: 0.3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const { lineChartProps } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LineChart v-bind="lineChartProps"> </LineChart>
|
||||||
|
</template>
|
||||||
36
dashboard/components/dashboard/OssBarCard.vue
Normal file
36
dashboard/components/dashboard/OssBarCard.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { OssAggregated } from '~/server/api/metrics/[project_id]/data/oss';
|
||||||
|
|
||||||
|
const activeProject = await useActiveProject();
|
||||||
|
const { data: events, pending, refresh } = await useFetch<OssAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/oss`, signHeaders());
|
||||||
|
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogBarData.value = [];
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
$fetch<any[]>(`/api/metrics/${activeProject.value?._id}/data/oss`, signHeaders({
|
||||||
|
'x-query-limit': '200'
|
||||||
|
})).then(data => {
|
||||||
|
dialogBarData.value = data;
|
||||||
|
isDataLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard @showMore="showMore()" @dataReload="refresh" :data="events || []"
|
||||||
|
desc="The operating systems most commonly used by your website's visitors." :dataIcons="false"
|
||||||
|
:loading="pending" label="Top OS" sub-label="OSs"></DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
44
dashboard/components/dashboard/ProjectSelectionCard.vue
Normal file
44
dashboard/components/dashboard/ProjectSelectionCard.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
type Prop = {
|
||||||
|
title: string,
|
||||||
|
subtitle: string,
|
||||||
|
chip?: string,
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Prop>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col rounded-xl overflow-hidden hover:shadow-[0_0_50px_#2969f1] hover:outline hover:outline-[2px] hover:outline-accent cursor-pointer">
|
||||||
|
<div class="h-[14rem] aspect-[9/7] bg-[#2f2a64] flex relative">
|
||||||
|
<img class="object-cover" :src="'/report/card_image.png'">
|
||||||
|
|
||||||
|
<div v-if="chip"
|
||||||
|
class="absolute px-4 py-1 rounded-lg poppins left-2 flex gap-2 bottom-2 bg-orange-500/80 items-center">
|
||||||
|
<div class="flex items-center"> <i class="far fa-fire text-[1.1rem]"></i></div>
|
||||||
|
<div class="poppins text-[1rem] font-semibold"> {{ chip }} </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="bg-[#444444cc] p-4 h-[7rem] relative">
|
||||||
|
<div class="poppins text-[1.2rem] font-bold text-text">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div class="poppins text-[1rem] text-text-sub/90">
|
||||||
|
{{ subtitle }}
|
||||||
|
</div>
|
||||||
|
<div class="absolute right-4 bottom-3">
|
||||||
|
<i class="fas fa-arrow-right text-[1.2rem]"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
51
dashboard/components/dashboard/ReferrersBarCard.vue
Normal file
51
dashboard/components/dashboard/ReferrersBarCard.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { ReferrersAggregated } from '~/server/api/metrics/[project_id]/data/referrers';
|
||||||
|
import type { IconProvider } from './BarsCard.vue';
|
||||||
|
|
||||||
|
const activeProject = await useActiveProject();
|
||||||
|
const { data: events, pending, refresh } = await useFetch<ReferrersAggregated[]>(`/api/metrics/${activeProject.value?._id}/data/referrers`, signHeaders());
|
||||||
|
|
||||||
|
|
||||||
|
function iconProvider(id: string): ReturnType<IconProvider> {
|
||||||
|
if (id === 'self') return ['icon', 'fas fa-link'];
|
||||||
|
return ['img', `https://s2.googleusercontent.com/s2/favicons?domain=${id}&sz=64`]
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementTextTransformer(element: string) {
|
||||||
|
if (element === 'self') return 'Direct Link';
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const { showDialog, dialogBarData, isDataLoading } = useBarCardDialog();
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
|
||||||
|
|
||||||
|
showDialog.value = true;
|
||||||
|
dialogBarData.value = [];
|
||||||
|
isDataLoading.value = true;
|
||||||
|
|
||||||
|
$fetch<any[]>(`/api/metrics/${activeProject.value?._id}/data/referrers`, signHeaders({
|
||||||
|
'x-query-limit': '200'
|
||||||
|
})).then(data => {
|
||||||
|
dialogBarData.value = data.map(e => {
|
||||||
|
return { ...e, icon: iconProvider(e._id) }
|
||||||
|
});
|
||||||
|
isDataLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard @showMore="showMore()" :elementTextTransformer="elementTextTransformer"
|
||||||
|
:iconProvider="iconProvider" @dataReload="refresh" :data="events || []"
|
||||||
|
desc="Where users find your website." :dataIcons="true" :loading="pending" label="Top Referrers"
|
||||||
|
sub-label="Referrers"></DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
dashboard/components/dashboard/SessionsLineChart.vue
Normal file
31
dashboard/components/dashboard/SessionsLineChart.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
const data = ref<number[]>([]);
|
||||||
|
const labels = ref<string[]>([]);
|
||||||
|
const ready = ref<boolean>(false);
|
||||||
|
const key = ref<string>('0');
|
||||||
|
|
||||||
|
const props = defineProps<{ slice: SliceName }>();
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const response = await useTimelineData('sessions', props.slice);
|
||||||
|
if (!response) return;
|
||||||
|
data.value = response.data;
|
||||||
|
labels.value = response.labels;
|
||||||
|
ready.value = true;
|
||||||
|
key.value = Date.now().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData();
|
||||||
|
watch(props, async () => { await loadData(); });
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AdvancedLineChart v-if="ready" :data="data" :labels="labels" color="#f56523"></AdvancedLineChart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
113
dashboard/components/dashboard/TopCards.vue
Normal file
113
dashboard/components/dashboard/TopCards.vue
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MetricsTimeline } from '~/server/api/metrics/[project_id]/timeline/generic';
|
||||||
|
|
||||||
|
|
||||||
|
const { data: metricsInfo } = useMetricsData();
|
||||||
|
|
||||||
|
const avgVisitDay = computed(() => {
|
||||||
|
if (!metricsInfo.value) return '0.00';
|
||||||
|
const days = (Date.now() - (metricsInfo.value?.firstViewDate || 0)) / 1000 / 60 / 60 / 24;
|
||||||
|
const avg = metricsInfo.value.visitsCount / Math.max(days, 1);
|
||||||
|
return avg.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgEventsDay = computed(() => {
|
||||||
|
if (!metricsInfo.value) return '0.00';
|
||||||
|
const days = (Date.now() - (metricsInfo.value?.firstEventDate || 0)) / 1000 / 60 / 60 / 24;
|
||||||
|
const avg = metricsInfo.value.eventsCount / Math.max(days, 1);
|
||||||
|
return avg.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgSessionsDay = computed(() => {
|
||||||
|
if (!metricsInfo.value) return '0.00';
|
||||||
|
const days = (Date.now() - (metricsInfo.value?.firstViewDate || 0)) / 1000 / 60 / 60 / 24;
|
||||||
|
const avg = metricsInfo.value.sessionsVisitsCount / Math.max(days, 1);
|
||||||
|
return avg.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const avgSessionDuration = computed(() => {
|
||||||
|
if (!metricsInfo.value) return '0.00';
|
||||||
|
const avg = metricsInfo.value.avgSessionDuration;
|
||||||
|
|
||||||
|
let hours = 0;
|
||||||
|
let minutes = 0;
|
||||||
|
let seconds = 0;
|
||||||
|
seconds += avg * 60;
|
||||||
|
|
||||||
|
while (seconds > 60) {
|
||||||
|
seconds -= 60;
|
||||||
|
minutes += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (minutes > 60) {
|
||||||
|
minutes -= 60;
|
||||||
|
hours += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
|
||||||
|
});
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
data: number[],
|
||||||
|
labels: string[],
|
||||||
|
trend: number,
|
||||||
|
ready: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const visitsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
|
||||||
|
const eventsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
|
||||||
|
const sessionsData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
|
||||||
|
const sessionsDurationData = reactive<Data>({ data: [], labels: [], trend: 0, ready: false });
|
||||||
|
|
||||||
|
async function loadData(timelineEndpointName: string, target: Data) {
|
||||||
|
const response = await useTimelineData(timelineEndpointName, 'day');
|
||||||
|
if (!response) return;
|
||||||
|
target.data = response.data;
|
||||||
|
target.labels = response.labels;
|
||||||
|
target.trend = response.trend;
|
||||||
|
target.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
await loadData('visits', visitsData);
|
||||||
|
await loadData('events', eventsData);
|
||||||
|
await loadData('sessions', sessionsData);
|
||||||
|
await loadData('sessions_duration', sessionsDurationData);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="gap-6 px-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-4" v-if="metricsInfo">
|
||||||
|
|
||||||
|
<DashboardCountCard :ready="visitsData.ready" icon="far fa-earth" text="Total page visits"
|
||||||
|
:value="formatNumberK(metricsInfo.visitsCount)" :avg="formatNumberK(avgVisitDay) + '/day'"
|
||||||
|
:trend="visitsData.trend" :data="visitsData.data" :labels="visitsData.labels" color="#5655d7">
|
||||||
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
<DashboardCountCard :ready="eventsData.ready" icon="far fa-flag" text="Total custom events"
|
||||||
|
:value="formatNumberK(metricsInfo.eventsCount)" :avg="formatNumberK(avgEventsDay) + '/day'"
|
||||||
|
:trend="eventsData.trend" :data="eventsData.data" :labels="eventsData.labels" color="#1e9b86">
|
||||||
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
<DashboardCountCard :ready="sessionsData.ready" icon="far fa-user" text="Unique visits sessions"
|
||||||
|
:value="formatNumberK(metricsInfo.sessionsVisitsCount)" :avg="formatNumberK(avgSessionsDay) + '/day'"
|
||||||
|
:trend="sessionsData.trend" :data="sessionsData.data" :labels="sessionsData.labels" color="#4abde8">
|
||||||
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
<DashboardCountCard :ready="sessionsDurationData.ready" icon="far fa-timer" text="Avg session time"
|
||||||
|
:value="avgSessionDuration" :trend="sessionsDurationData.trend" :data="sessionsDurationData.data"
|
||||||
|
:labels="sessionsDurationData.labels" color="#f56523">
|
||||||
|
</DashboardCountCard>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
42
dashboard/components/dashboard/TopSection.vue
Normal file
42
dashboard/components/dashboard/TopSection.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const activeProject = useActiveProject();
|
||||||
|
const { onlineUsers, stopWatching, startWatching } = useOnlineUsers();
|
||||||
|
onMounted(()=> startWatching());
|
||||||
|
onUnmounted(() => stopWatching());
|
||||||
|
|
||||||
|
|
||||||
|
function copyProjectId() {
|
||||||
|
if (!navigator.clipboard) alert('NON PUOI COPIARE IN HTTP');
|
||||||
|
navigator.clipboard.writeText((activeProject.value?._id || 0).toString());
|
||||||
|
alert('Copiato !');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="w-full px-6 py-2 lg:py-6 font-bold text-text-sub/40 flex flex-col xl:flex-row text-lg lg:text-2xl gap-2 xl:gap-12">
|
||||||
|
|
||||||
|
<div class="flex gap-2 items-center text-text/90">
|
||||||
|
<div class="animate-pulse w-[1rem] h-[1rem] bg-green-400 rounded-full"> </div>
|
||||||
|
<div> {{ onlineUsers }} Online users</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grow"></div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div>Project:</div>
|
||||||
|
<div class="text-text/90"> {{ activeProject?.name || 'Loading...' }} </div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div>Project id:</div>
|
||||||
|
<div class="text-text/90 text-[.9rem] lg:text-2xl">
|
||||||
|
{{ activeProject?._id || 'Loading...' }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center ml-3">
|
||||||
|
<i @click="copyProjectId()" class="far fa-copy hover:text-text cursor-pointer text-[1.2rem]"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
dashboard/components/dashboard/VisitsLineChart.vue
Normal file
31
dashboard/components/dashboard/VisitsLineChart.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
const data = ref<number[]>([]);
|
||||||
|
const labels = ref<string[]>([]);
|
||||||
|
const ready = ref<boolean>(false);
|
||||||
|
|
||||||
|
const props = defineProps<{ slice: SliceName }>();
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const response = await useTimelineDataRaw('visits', props.slice);
|
||||||
|
if (!response) return;
|
||||||
|
const fixed = fixMetrics(response, props.slice);
|
||||||
|
console.log(fixed);
|
||||||
|
data.value = fixed.data;
|
||||||
|
labels.value = fixed.labels;
|
||||||
|
ready.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData();
|
||||||
|
watch(props, async () => { await loadData(); });
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AdvancedLineChart v-if="ready" :data="data" :labels="labels" color="#5655d7"></AdvancedLineChart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
61
dashboard/components/dashboard/WebsitesBarCard.vue
Normal file
61
dashboard/components/dashboard/WebsitesBarCard.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import type { VisitsWebsiteAggregated } from '~/server/api/metrics/[project_id]/data/websites';
|
||||||
|
|
||||||
|
const { data: websites, pending, refresh } = useWebsitesData();
|
||||||
|
|
||||||
|
const currentViewData = ref<(VisitsWebsiteAggregated[] | null)>(websites.value);
|
||||||
|
|
||||||
|
watch(pending, () => {
|
||||||
|
currentViewData.value = websites.value;
|
||||||
|
})
|
||||||
|
|
||||||
|
const isPagesView = ref<boolean>(false);
|
||||||
|
const isLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
async function showDetails(website: string) {
|
||||||
|
if (isPagesView.value == true) return;
|
||||||
|
isLoading.value = true;
|
||||||
|
isPagesView.value = true;
|
||||||
|
|
||||||
|
const { data: pagesData, pending } = usePagesData(website, 10);
|
||||||
|
|
||||||
|
watch(pending, () => {
|
||||||
|
currentViewData.value = pagesData.value;
|
||||||
|
isLoading.value = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function goToView() {
|
||||||
|
router.push('/dashboard/visits');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultData() {
|
||||||
|
currentViewData.value = websites.value;
|
||||||
|
isPagesView.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dataReload() {
|
||||||
|
await refresh();
|
||||||
|
setDefaultData();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<DashboardBarsCard :hideShowMore="true" @showGeneral="setDefaultData()" @showRawData="goToView()"
|
||||||
|
@dataReload="dataReload()" @showDetails="showDetails" :data="currentViewData || []"
|
||||||
|
:loading="pending || isLoading" :label="isPagesView ? 'Top pages' : 'Top Websites'"
|
||||||
|
:sub-label="isPagesView ? 'Page' : 'Website'"
|
||||||
|
:desc="isPagesView ? 'Most visited pages' : 'Most visited website in this project'"
|
||||||
|
:interactive="!isPagesView" :rawButton="!isLiveDemo()" :isDetailView="isPagesView">
|
||||||
|
</DashboardBarsCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
23
dashboard/components/dashboard/events/EventsColorManager.vue
Normal file
23
dashboard/components/dashboard/events/EventsColorManager.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
const builtinEvents = [
|
||||||
|
{ name: 'INFO', color: '#33a0e8' },
|
||||||
|
{ name: 'WARNING', color: '#ef8d44' },
|
||||||
|
{ name: 'ERROR', color: '#e47069' },
|
||||||
|
]
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="bg-[#2a2e34] outline outline-[1px] outline-[#bdbdbd70] rounded-lg p-2 px-4 flex gap-2 items-center w-full" v-for="e of builtinEvents">
|
||||||
|
<div :style="`background-color: ${e.color}`" class="w-[1rem] h-[1rem] rounded-full"> </div>
|
||||||
|
<div class="poppins"> {{ e.name }} </div>
|
||||||
|
<div class="grow"></div>
|
||||||
|
<!-- <div> <i class="far fa-trash"></i> </div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
52
dashboard/components/events/EventsStackedBarChart.vue
Normal file
52
dashboard/components/events/EventsStackedBarChart.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
const datasets = ref<any[]>([]);
|
||||||
|
const labels = ref<string[]>([]);
|
||||||
|
const ready = ref<boolean>(false);
|
||||||
|
|
||||||
|
const props = defineProps<{ slice: SliceName }>();
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const response = await useTimelineDataRaw('events_stacked', props.slice);
|
||||||
|
if (!response) return;
|
||||||
|
|
||||||
|
const fixed = fixMetrics(response, props.slice, { advanced: true, advancedGroupKey: 'name' });
|
||||||
|
|
||||||
|
const parsedDatasets: any[] = [];
|
||||||
|
const colors = ['#5655d0', '#6bbbe3', '#a6d5cb', '#fae0b9'];
|
||||||
|
|
||||||
|
for (let i = 0; i < fixed.allKeys.length; i++) {
|
||||||
|
const line: any = {
|
||||||
|
data: [],
|
||||||
|
color: colors[i] || '#FF0000',
|
||||||
|
label: fixed.allKeys[i]
|
||||||
|
};
|
||||||
|
parsedDatasets.push(line)
|
||||||
|
fixed.data.forEach((e: { key: string, value: number }[]) => {
|
||||||
|
const target = e.find(e => e.key == fixed.allKeys[i]);
|
||||||
|
if (!target) return;
|
||||||
|
line.data.push(target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
datasets.value = parsedDatasets;
|
||||||
|
labels.value = fixed.labels;
|
||||||
|
ready.value = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData();
|
||||||
|
watch(props, async () => { await loadData(); });
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AdvancedStackedBarChart v-if="ready" :datasets="datasets" :labels="labels">
|
||||||
|
</AdvancedStackedBarChart>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
108
dashboard/components/home/BgGrid.vue
Normal file
108
dashboard/components/home/BgGrid.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
size: number,
|
||||||
|
spacing: number,
|
||||||
|
opacity: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const sizeArr = new Array(props.size).fill('a');
|
||||||
|
|
||||||
|
function calculateOpacity(x: number, y: number) {
|
||||||
|
const distanceFromCenter = Math.sqrt(Math.pow(x - props.size / 2, 2) + Math.pow(y - props.size / 2, 2));
|
||||||
|
const normalizedDistance = distanceFromCenter / (props.size / 2);
|
||||||
|
return (1 - normalizedDistance).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widthHeight = computed(() => {
|
||||||
|
return 9 + props.size * props.spacing;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-fit h-fit">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" :width="widthHeight" :height="widthHeight" :style="`opacity: ${props.opacity};`"
|
||||||
|
fill="none">
|
||||||
|
|
||||||
|
<template v-for="(p, x) of sizeArr">
|
||||||
|
<template v-for="(p, y) of sizeArr">
|
||||||
|
<circle :cx="9 + (spacing * x)" :cy="9 + (spacing * y)" r="1" fill="#fff"
|
||||||
|
:fill-opacity="calculateOpacity(x, y)" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <circle cx="27" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="9" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="27" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="45" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="63" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="81" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="99" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="117" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="9" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="27" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="45" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="63" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="81" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="99" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="117" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
<circle cx="135" cy="135" r="1" fill="#fff" fill-opacity=".9" />
|
||||||
|
-->
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
23
dashboard/components/home/HomeCard.vue
Normal file
23
dashboard/components/home/HomeCard.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps<{ title: string, text: string, icon: string }>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-menu flex flex-col justify-center items-center px-8 py-10 w-[20rem] gap-4 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<div class="bg-[#36363f] p-6 flex items-center justify-center aspect-[1/1] rounded-2xl">
|
||||||
|
<i :class="props.icon" class="text-text text-[1.6rem]"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-text text-[1.3rem] poppins font-semibold text-center mt-6">
|
||||||
|
{{ props.title }}
|
||||||
|
</div>
|
||||||
|
<div class="text-text-sub/80 text-[1rem] poppins text-center">
|
||||||
|
{{ props.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
58
dashboard/components/pricing/PricingCard.vue
Normal file
58
dashboard/components/pricing/PricingCard.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
|
||||||
|
type Prop = {
|
||||||
|
title: string,
|
||||||
|
icon: string,
|
||||||
|
list: { text: string, icon: string }[],
|
||||||
|
price: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Prop>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-menu py-6 rounded-lg w-full h-full flex flex-col items-center justify-normal px-6 relative">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute rounded-full top-[-2.1rem] bg-accent w-[4.2rem] h-[4.2rem] flex items-center justify-center">
|
||||||
|
<i :class="icon" class="text-[2.5rem]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poppins mt-6 font-semibold text-[1.4rem]">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-400/50 h-[1px] w-full mt-6 mb-10"></div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex gap-3 items-center" v-for="element of list">
|
||||||
|
|
||||||
|
<div class="shrink-0 flex items-center bg-accent w-[2rem] h-[2rem] justify-center rounded-full">
|
||||||
|
<i :class="element.icon" class="text-[.9rem]"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poppins">
|
||||||
|
{{ element.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-400/50 h-[1px] w-full mt-10 mb-6"></div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 justify-between w-full">
|
||||||
|
<div class="flex gap-2 items-end">
|
||||||
|
<div class="manrope text-[2.5rem] font-bold text-text"> {{ price }} </div>
|
||||||
|
<div class="poppins text-text-sub/90 mb-1">/month</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Tasto bello
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
11
dashboard/composables/registerChartComponents.ts
Normal file
11
dashboard/composables/registerChartComponents.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
|
||||||
|
let registered = false;
|
||||||
|
export async function registerChartComponents() {
|
||||||
|
if (registered) return;
|
||||||
|
if (process.client) {
|
||||||
|
Chart.register(...registerables);
|
||||||
|
registered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
dashboard/composables/useAccessToken.ts
Normal file
34
dashboard/composables/useAccessToken.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const ACCESS_TOKEN_STATE_KEY = 'access_token';
|
||||||
|
const ACCESS_TOKEN_COOKIE_KEY = 'access_token';
|
||||||
|
|
||||||
|
export function signHeaders(headers?: Record<string, string>) {
|
||||||
|
const { token } = useAccessToken()
|
||||||
|
return { headers: { ...(headers || {}), 'Authorization': 'Bearer ' + token.value } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAccessToken() {
|
||||||
|
|
||||||
|
const tokenCookie = useCookie(ACCESS_TOKEN_COOKIE_KEY, { expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30) })
|
||||||
|
|
||||||
|
const token = useState<string | undefined | null>(ACCESS_TOKEN_STATE_KEY);
|
||||||
|
const needLoad = useState<boolean>('needAccessTokenLoad', () => true);
|
||||||
|
|
||||||
|
|
||||||
|
const readToken = () => {
|
||||||
|
token.value = tokenCookie.value;
|
||||||
|
needLoad.value = false;
|
||||||
|
}
|
||||||
|
const setToken = (newToken: string) => {
|
||||||
|
tokenCookie.value = newToken;
|
||||||
|
token.value = tokenCookie.value;
|
||||||
|
needLoad.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needLoad.value == true) readToken();
|
||||||
|
|
||||||
|
|
||||||
|
return { token, readToken, setToken, needLoad }
|
||||||
|
}
|
||||||
12
dashboard/composables/useBarCardDialog.ts
Normal file
12
dashboard/composables/useBarCardDialog.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function useBarCardDialog() {
|
||||||
|
const showDialog = useState('show-bar-card-dialog', () => false);
|
||||||
|
const dialogBarData = useState<any[]>('bar-card-dialog-data', () => []);
|
||||||
|
const isDataLoading = useState('bar-card-dialog-data-loading', () => false);
|
||||||
|
function closeDialog() {
|
||||||
|
showDialog.value = false;
|
||||||
|
}
|
||||||
|
return { showDialog, dialogBarData, closeDialog, isDataLoading };
|
||||||
|
}
|
||||||
28
dashboard/composables/useCurrentProject.ts
Normal file
28
dashboard/composables/useCurrentProject.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import type { TProject } from "@schema/ProjectSchema";
|
||||||
|
|
||||||
|
const projects = useFetch<TProject[]>('/api/project/list', { key: 'projectslist', ...signHeaders() });
|
||||||
|
export function useProjectsList() { return projects; }
|
||||||
|
|
||||||
|
const activeProjectId = useFetch<string>(`/api/user/active_project`, { key: 'activeProjectId', ...signHeaders() });
|
||||||
|
export function useActiveProjectId() { return activeProjectId; }
|
||||||
|
|
||||||
|
export function useActiveProject() {
|
||||||
|
if (isLiveDemo()) {
|
||||||
|
const { data: liveDemoProject } = useLiveDemo();
|
||||||
|
return liveDemoProject;
|
||||||
|
}
|
||||||
|
const { data: projects } = useProjectsList();
|
||||||
|
const { data: activeProjectId } = useActiveProjectId();
|
||||||
|
return computed(() => {
|
||||||
|
if (!projects.value) return;
|
||||||
|
if (!activeProjectId.value) return;
|
||||||
|
return projects.value.find(e => e._id.toString() == activeProjectId.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function setActiveProject(project_id: string) {
|
||||||
|
await $fetch<string>(`/api/user/set_active_project?project_id=${project_id}`, signHeaders());
|
||||||
|
activeProjectId.refresh();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user