This commit is contained in:
artem 2024-04-14 21:08:44 +03:00
parent 23f15dac6b
commit c7b49df155
167 changed files with 6566 additions and 3391 deletions

View File

@ -0,0 +1,32 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions
on: [push]
jobs:
build_and_push:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v2 # Required to mount the Github Workspace to a volume
- name: Login to Docker Hub
uses: https://github.com/docker/login-action@v3
with:
registry: gitea.webart-tech.ru
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: https://github.com/docker/build-push-action@v5
with:
context: .
push: true
tags: gitea.webart-tech.ru/webart/strava-frontend/platformer:${{ gitea.sha }}
# - name: Deploy
# run: |
# git clone https://gitea.webart-tech.ru/webart/kuber-deploy.git deploy
# cd ./deploy
# sed -i -E 's/platformer:.+/platformer:${{ gitea.sha }}/g' strava-frontend.yml
# git config --global user.email "deploy@deploy.deploy"
# git commit -am 'auto-deploy'
# git push https://${{ secrets.DOCKER_USERNAME }}:${{ secrets.DOCKER_PASSWORD }}@gitea.webart-tech.ru/webart/kuber-deploy

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
### STAGE 1: Build ----------------------------------------------------------------------------------------------------
### STAGE 1: Build ----------------------------------------------------------------------------------------------------
FROM node:18
ARG VERSION
ARG BUILD
ARG TARGETENV
ARG CURLOPT_SSL_VERIFYPEER=FALSE
LABEL version=$VERSION build=$BUILD mode=$TARGETENV
EXPOSE 80
RUN apt-get update && apt-get install -y curl ca-certificates
# RUN curl -fsSLk https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install -y nginx
RUN rm -rf /usr/share/nginx/html/
RUN mkdir -p /usr/share/nginx/html
# RUN npm install forever -g
RUN mkdir /data
WORKDIR /data
COPY package.json package-lock.json ./
ARG VERSION
ARG BUILD
ARG TARGETENV
ARG CURLOPT_SSL_VERIFYPEER=FALSE
COPY . .
RUN npm run build
# служебные штуки
RUN cp -R /data/dist/* /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN mkdir /usr/share/nginx/html/.well-known
RUN chmod +x /data/run.sh
CMD ["/data/run.sh"]

9
eslint.config.mjs Normal file
View File

@ -0,0 +1,9 @@
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
export default [
{ languageOptions: { globals: globals.browser } },
...tseslint.configs.recommended,
...pluginVue.configs["flat/essential"],
];

View File

@ -3,8 +3,14 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet" /> <link
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" /> href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,300..600,0,0" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,300..600,0,0"

1509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"prepare": "husky install", "prepare": "husky install",
"dev": "vite", "dev": "vite",
"build": "npm run lint && vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"build:ci": "vite build", "build:ci": "vite build",
"start:ci": "serve -s ./dist", "start:ci": "serve -s ./dist",
"prelint": "npm run format", "prelint": "npm run format",
@ -42,6 +42,7 @@
"vuestic-ui": "^1.9.0" "vuestic-ui": "^1.9.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.0.0",
"@intlify/unplugin-vue-i18n": "^1.5.0", "@intlify/unplugin-vue-i18n": "^1.5.0",
"@storybook/addon-essentials": "^7.4.6", "@storybook/addon-essentials": "^7.4.6",
"@storybook/addon-interactions": "^7.4.6", "@storybook/addon-interactions": "^7.4.6",
@ -58,10 +59,12 @@
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"eslint": "^8.13.0", "eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-vue": "^9.18.1", "eslint-plugin-vue": "^9.25.0",
"globals": "^15.0.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"lint-staged": "^15.1.0", "lint-staged": "^15.1.0",
"postcss": "^8.4.21", "postcss": "^8.4.21",
@ -69,6 +72,7 @@
"storybook": "^7.4.6", "storybook": "^7.4.6",
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-eslint": "^7.6.0",
"vite": "^4.4.6", "vite": "^4.4.6",
"vue-eslint-parser": "^9.3.2", "vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.22" "vue-tsc": "^1.8.22"

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

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

4
run.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
nginx
#node /usr/share/nginx/html/server/main.js

View File

@ -3,10 +3,10 @@
</template> </template>
<style lang="scss"> <style lang="scss">
@import 'scss/main.scss'; @import "scss/main.scss";
#app { #app {
font-family: 'Inter', Avenir, Helvetica, Arial, sans-serif; font-family: "Inter", Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

View File

@ -1,5 +1,11 @@
<template> <template>
<svg fill="none" height="200" viewBox="0 0 200 200" width="200" xmlns="http://www.w3.org/2000/svg"> <svg
fill="none"
height="200"
viewBox="0 0 200 200"
width="200"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
clip-rule="evenodd" clip-rule="evenodd"
d="M141.03 63.87c2.58-3.14 5.5-6.4 7.4-8.46l-1.4-1.3a215.73 215.73 0 0 0-7.47 8.55 90.25 90.25 0 0 0-3.51 4.5 17.33 17.33 0 0 0-2.08 3.46l1.79.67c.23-.63.87-1.68 1.83-3.02a87.85 87.85 0 0 1 3.44-4.4ZM94.82 51.4c2-2.4 4.27-4.9 5.74-6.46l-1.39-1.3a166.85 166.85 0 0 0-5.81 6.53 69.06 69.06 0 0 0-2.74 3.46 13.4 13.4 0 0 0-1.65 2.7l1.78.68c.18-.45.65-1.23 1.4-2.26.73-1 1.66-2.16 2.67-3.35Z" d="M141.03 63.87c2.58-3.14 5.5-6.4 7.4-8.46l-1.4-1.3a215.73 215.73 0 0 0-7.47 8.55 90.25 90.25 0 0 0-3.51 4.5 17.33 17.33 0 0 0-2.08 3.46l1.79.67c.23-.63.87-1.68 1.83-3.02a87.85 87.85 0 0 1 3.44-4.4ZM94.82 51.4c2-2.4 4.27-4.9 5.74-6.46l-1.39-1.3a166.85 166.85 0 0 0-5.81 6.53 69.06 69.06 0 0 0-2.74 3.46 13.4 13.4 0 0 0-1.65 2.7l1.78.68c.18-.45.65-1.23 1.4-2.26.73-1 1.66-2.16 2.67-3.35Z"

View File

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

View File

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

View File

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

View File

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

View File

@ -21,12 +21,12 @@
<script lang="ts" setup> <script lang="ts" setup>
withDefaults( withDefaults(
defineProps<{ defineProps<{
color?: string color?: string;
}>(), }>(),
{ {
color: 'inherit', color: "inherit",
}, },
) );
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,9 +1,20 @@
<template> <template>
<svg class="va-icon-faster" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-faster"
version="1.1"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Generator: sketchtool 48.2 (47327) - http://www.bohemiancoding.com/sketch --> <!-- Generator: sketchtool 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>62EBC3B8-A55C-4B01-95A2-52FB8EDD4150</title> <title>62EBC3B8-A55C-4B01-95A2-52FB8EDD4150</title>
<defs /> <defs />
<g id="symbols" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"> <g
id="symbols"
fill="none"
fill-rule="evenodd"
stroke="none"
stroke-width="1"
>
<g id="icon-faster" fill="#34495E"> <g id="icon-faster" fill="#34495E">
<g> <g>
<path <path

View File

@ -1,5 +1,9 @@
<template> <template>
<svg class="va-icon-free" viewBox="0 0 44.99 51.04" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-free"
viewBox="0 0 44.99 51.04"
xmlns="http://www.w3.org/2000/svg"
>
<defs /> <defs />
<title>overview_icon_2</title> <title>overview_icon_2</title>
<g id="Layer_2" data-name="Layer 2"> <g id="Layer_2" data-name="Layer 2">

View File

@ -1,5 +1,9 @@
<template> <template>
<svg class="va-icon-fresh" viewBox="0 0 50.98 47.66" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-fresh"
viewBox="0 0 50.98 47.66"
xmlns="http://www.w3.org/2000/svg"
>
<defs /> <defs />
<title>overview_icon_5</title> <title>overview_icon_5</title>
<g id="Layer_2" data-name="Layer 2"> <g id="Layer_2" data-name="Layer 2">

View File

@ -1,5 +1,11 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"> <svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path <path
d="M18.3516 2.8125H3.97663C3.53557 2.8125 3.17802 3.17005 3.17802 3.61111C3.17802 4.05217 3.53557 4.40972 3.97663 4.40972H18.3516C18.7927 4.40972 19.1502 4.05217 19.1502 3.61111C19.1502 3.17005 18.7927 2.8125 18.3516 2.8125Z" d="M18.3516 2.8125H3.97663C3.53557 2.8125 3.17802 3.17005 3.17802 3.61111C3.17802 4.05217 3.53557 4.40972 3.97663 4.40972H18.3516C18.7927 4.40972 19.1502 4.05217 19.1502 3.61111C19.1502 3.17005 18.7927 2.8125 18.3516 2.8125Z"
fill="#767C88" fill="#767C88"

View File

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

View File

@ -1,5 +1,11 @@
<template> <template>
<svg class="va-icon-menu-collapsed" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-menu-collapsed"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="nonzero"> <g fill="none" fill-rule="nonzero">
<path d="M0 0h24v24H0z" /> <path d="M0 0h24v24H0z" />
<rect :fill="color" height="2" rx="1" width="20" x="2" y="3" /> <rect :fill="color" height="2" rx="1" width="20" x="2" y="3" />
@ -15,12 +21,12 @@
<script lang="ts" setup> <script lang="ts" setup>
withDefaults( withDefaults(
defineProps<{ defineProps<{
color?: string color?: string;
}>(), }>(),
{ {
color: 'inherit', color: "inherit",
}, },
) );
</script> </script>
<style lang="scss"> <style lang="scss">

View File

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

View File

@ -10,10 +10,10 @@
<script lang="ts" setup> <script lang="ts" setup>
withDefaults( withDefaults(
defineProps<{ defineProps<{
color?: string color?: string;
}>(), }>(),
{ {
color: 'inherit', color: "inherit",
}, },
) );
</script> </script>

View File

@ -1,11 +1,21 @@
<template> <template>
<svg class="va-icon-responsive" viewBox="0 0 47.5 49" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-responsive"
viewBox="0 0 47.5 49"
xmlns="http://www.w3.org/2000/svg"
>
<defs /> <defs />
<title>overview_icon_3</title> <title>overview_icon_3</title>
<g id="Layer_2" data-name="Layer 2"> <g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1"> <g id="Layer_1-2" data-name="Layer 1">
<polygon class="cls-1" points="37 26 37 7 11 7 11 18 3 18 3 46 11 46 15 46 30 46 37 46 45 46 45 26 37 26" /> <polygon
<path class="cls-2" d="M40,19V0H8V11H0V49H47.5V19ZM3,46V14H8V46Zm34,0H11V3H37Zm7.5,0H40V22h4.5Z" /> class="cls-1"
points="37 26 37 7 11 7 11 18 3 18 3 46 11 46 15 46 30 46 37 46 45 46 45 26 37 26"
/>
<path
class="cls-2"
d="M40,19V0H8V11H0V49H47.5V19ZM3,46V14H8V46Zm34,0H11V3H37Zm7.5,0H40V22h4.5Z"
/>
<circle class="cls-2" cx="24" cy="41" r="2.67" /> <circle class="cls-2" cx="24" cy="41" r="2.67" />
</g> </g>
</g> </g>

View File

@ -1,5 +1,9 @@
<template> <template>
<svg class="va-icon-rich" viewBox="0 0 56.99 55" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-rich"
viewBox="0 0 56.99 55"
xmlns="http://www.w3.org/2000/svg"
>
<defs /> <defs />
<title>overview_icon_6</title> <title>overview_icon_6</title>
<g id="Layer_2" data-name="Layer 2"> <g id="Layer_2" data-name="Layer 2">
@ -9,7 +13,10 @@
class="cls-2" class="cls-2"
d="M57,41.18l-7.85-16V24H8.81v1.11L0,41.11l2.63,1.45L8.81,31.33V55H49.15V32L54.3,42.5ZM46.15,52H11.81V27H46.15Z" d="M57,41.18l-7.85-16V24H8.81v1.11L0,41.11l2.63,1.45L8.81,31.33V55H49.15V32L54.3,42.5ZM46.15,52H11.81V27H46.15Z"
/> />
<polygon class="cls-2" points="35.3 1.8 32.9 0 28.12 6.39 26.16 4.63 24.16 6.87 28.56 10.8 35.3 1.8" /> <polygon
class="cls-2"
points="35.3 1.8 32.9 0 28.12 6.39 26.16 4.63 24.16 6.87 28.56 10.8 35.3 1.8"
/>
<polygon <polygon
class="cls-2" class="cls-2"
points="22.3 12.46 19.9 10.67 15.12 17.05 13.16 15.3 11.16 17.54 15.56 21.47 22.3 12.46" points="22.3 12.46 19.9 10.67 15.12 17.05 13.16 15.3 11.16 17.54 15.56 21.47 22.3 12.46"

View File

@ -1,9 +1,20 @@
<template> <template>
<svg class="va-icon-slower" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-slower"
version="1.1"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Generator: sketchtool 48.2 (47327) - http://www.bohemiancoding.com/sketch --> <!-- Generator: sketchtool 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>67046716-A590-445C-AC65-1EEF69089C00</title> <title>67046716-A590-445C-AC65-1EEF69089C00</title>
<defs /> <defs />
<g id="symbols" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"> <g
id="symbols"
fill="none"
fill-rule="evenodd"
stroke="none"
stroke-width="1"
>
<g id="icon-slower" fill="#34495E"> <g id="icon-slower" fill="#34495E">
<g> <g>
<path <path

View File

@ -1,5 +1,9 @@
<template> <template>
<svg class="va-icon-vue" viewBox="0 0 55.05 47.8" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-vue"
viewBox="0 0 55.05 47.8"
xmlns="http://www.w3.org/2000/svg"
>
<defs /> <defs />
<title>overview_icon_1</title> <title>overview_icon_1</title>
<g id="Layer_2" data-name="Layer 2"> <g id="Layer_2" data-name="Layer 2">

View File

@ -1,5 +1,11 @@
<template> <template>
<svg class="va-icon-vuestic" height="31" viewBox="0 0 304 31" width="304" xmlns="http://www.w3.org/2000/svg"> <svg
class="va-icon-vuestic"
height="31"
viewBox="0 0 304 31"
width="304"
xmlns="http://www.w3.org/2000/svg"
>
<defs> <defs>
<linearGradient :id="'ORIGINAL'" x1="0%" y1="50%" y2="50%"> <linearGradient :id="'ORIGINAL'" x1="0%" y1="50%" y2="50%">
<stop offset="0%" stop-color="#4AE387" /> <stop offset="0%" stop-color="#4AE387" />
@ -25,17 +31,17 @@
</template> </template>
<script> <script>
export default { export default {
name: 'VaIconVuestic', name: "VaIconVuestic",
inject: ['contextConfig'], inject: ["contextConfig"],
computed: { computed: {
themeGradientId() { themeGradientId() {
return this.contextConfig.invertedColor ? 'CORPORATE' : 'ORIGINAL' return this.contextConfig.invertedColor ? "CORPORATE" : "ORIGINAL";
}, },
textColor() { textColor() {
return this.contextConfig.invertedColor ? '#6E85E8' : '#E4FF32' return this.contextConfig.invertedColor ? "#6E85E8" : "#E4FF32";
}, },
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -23,18 +23,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' import { storeToRefs } from "pinia";
import { useGlobalStore } from '../../stores/global-store' import { useGlobalStore } from "../../stores/global-store";
import AppNavbarActions from './components/AppNavbarActions.vue' import AppNavbarActions from "./components/AppNavbarActions.vue";
import VuesticLogo from '../VuesticLogo.vue' import VuesticLogo from "../VuesticLogo.vue";
defineProps({ defineProps({
isMobile: { type: Boolean, default: false }, isMobile: { type: Boolean, default: false },
}) });
const GlobalStore = useGlobalStore() const GlobalStore = useGlobalStore();
const { isSidebarMinimized } = storeToRefs(GlobalStore) const { isSidebarMinimized } = storeToRefs(GlobalStore);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -22,21 +22,23 @@
{{ t('helpAndSupport') }} {{ t('helpAndSupport') }}
</VaButton> --> </VaButton> -->
<!-- <NotificationDropdown class="app-navbar-actions__item" /> --> <!-- <NotificationDropdown class="app-navbar-actions__item" /> -->
<ProfileDropdown class="app-navbar-actions__item app-navbar-actions__item--profile mr-1" /> <ProfileDropdown
class="app-navbar-actions__item app-navbar-actions__item--profile mr-1"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import ProfileDropdown from './dropdowns/ProfileDropdown.vue' import ProfileDropdown from "./dropdowns/ProfileDropdown.vue";
import NotificationDropdown from './dropdowns/NotificationDropdown.vue' import NotificationDropdown from "./dropdowns/NotificationDropdown.vue";
import GithubButton from './GitHubButton.vue' import GithubButton from "./GitHubButton.vue";
defineProps({ defineProps({
isMobile: { type: Boolean, default: false }, isMobile: { type: Boolean, default: false },
}) });
import { useI18n } from 'vue-i18n' import { useI18n } from "vue-i18n";
const { t } = useI18n() const { t } = useI18n();
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -11,5 +11,5 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import VaIconGitHub from '../../icons/VaIconGitHub.vue' import VaIconGitHub from "../../icons/VaIconGitHub.vue";
</script> </script>

View File

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

View File

@ -1,6 +1,11 @@
<template> <template>
<div class="profile-dropdown-wrapper"> <div class="profile-dropdown-wrapper">
<VaDropdown v-model="isShown" :offset="[9, 0]" class="profile-dropdown" stick-to-edges> <VaDropdown
v-model="isShown"
:offset="[9, 0]"
class="profile-dropdown"
stick-to-edges
>
<template #anchor> <template #anchor>
<VaButton preset="secondary" color="textPrimary"> <VaButton preset="secondary" color="textPrimary">
<span class="profile-dropdown__anchor min-w-max"> <span class="profile-dropdown__anchor min-w-max">
@ -14,7 +19,10 @@
:style="{ '--hover-color': hoverColor }" :style="{ '--hover-color': hoverColor }"
> >
<VaList v-for="group in options" :key="group.name"> <VaList v-for="group in options" :key="group.name">
<header v-if="group.name" class="uppercase text-[var(--va-secondary)] opacity-80 font-bold text-xs px-4"> <header
v-if="group.name"
class="uppercase text-[var(--va-secondary)] opacity-80 font-bold text-xs px-4"
>
{{ t(`user.${group.name}`) }} {{ t(`user.${group.name}`) }}
</header> </header>
<VaListItem <VaListItem
@ -34,86 +42,90 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue' import { ref, computed } from "vue";
import { useI18n } from 'vue-i18n' import { useI18n } from "vue-i18n";
import { useColors } from 'vuestic-ui' import { useColors } from "vuestic-ui";
const { colors, setHSLAColor } = useColors() const { colors, setHSLAColor } = useColors();
const hoverColor = computed(() => setHSLAColor(colors.focus, { a: 0.1 })) const hoverColor = computed(() => setHSLAColor(colors.focus, { a: 0.1 }));
const { t } = useI18n() const { t } = useI18n();
type ProfileListItem = { type ProfileListItem = {
name: string name: string;
to?: string to?: string;
href?: string href?: string;
icon: string icon: string;
} };
type ProfileOptions = { type ProfileOptions = {
name: string name: string;
separator: boolean separator: boolean;
list: ProfileListItem[] list: ProfileListItem[];
} };
withDefaults( withDefaults(
defineProps<{ defineProps<{
options?: ProfileOptions[] options?: ProfileOptions[];
}>(), }>(),
{ {
options: () => [ options: () => [
{ {
name: 'account', name: "account",
separator: true, separator: true,
list: [ list: [
{ {
name: 'profile', name: "profile",
to: 'preferences', to: "preferences",
icon: 'mso-account_circle', icon: "mso-account_circle",
}, },
{ {
name: 'settings', name: "settings",
to: 'settings', to: "settings",
icon: 'mso-settings', icon: "mso-settings",
}, },
], ],
}, },
{ {
name: 'explore', name: "explore",
separator: true, separator: true,
list: [ list: [
{ {
name: 'faq', name: "faq",
to: 'faq', to: "faq",
icon: 'mso-quiz', icon: "mso-quiz",
}, },
{ {
name: 'helpAndSupport', name: "helpAndSupport",
href: 'https://discord.gg/u7fQdqQt8c', href: "https://discord.gg/u7fQdqQt8c",
icon: 'mso-error', icon: "mso-error",
}, },
], ],
}, },
{ {
name: '', name: "",
separator: false, separator: false,
list: [ list: [
{ {
name: 'logout', name: "logout",
to: 'login', to: "login",
icon: 'mso-logout', icon: "mso-logout",
}, },
], ],
}, },
], ],
}, },
) );
const isShown = ref(false) const isShown = ref(false);
const resolveLinkAttribute = (item: ProfileListItem) => { const resolveLinkAttribute = (item: ProfileListItem) => {
return item.to ? { to: { name: item.to } } : item.href ? { href: item.href, target: '_blank' } : {} return item.to
} ? { to: { name: item.to } }
: item.href
? { href: item.href, target: "_blank" }
: {};
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

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

View File

@ -1,21 +1,21 @@
export interface INavigationRoute { export interface INavigationRoute {
name: string name: string;
displayName: string displayName: string;
meta: { icon: string } meta: { icon: string };
children?: INavigationRoute[] children?: INavigationRoute[];
} }
export default { export default {
root: { root: {
name: '/', name: "/",
displayName: 'navigationRoutes.home', displayName: "navigationRoutes.home",
}, },
routes: [ routes: [
{ {
name: 'dashboard', name: "dashboard",
displayName: 'menu.dashboard', displayName: "menu.dashboard",
meta: { meta: {
icon: 'vuestic-iconset-dashboard', icon: "vuestic-iconset-dashboard",
}, },
}, },
// { // {
@ -104,4 +104,4 @@ export default {
// }, // },
// }, // },
] as INavigationRoute[], ] as INavigationRoute[],
} };

View File

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

View File

@ -7,48 +7,57 @@
<div class="mb-8"> <div class="mb-8">
<h1>Display 1 Heading</h1> <h1>Display 1 Heading</h1>
<p> <p>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a Of all of the celestial bodies that capture our attention and
greater influence on life on planet Earth than its own satellite, the moon. When you think about it. fascination as astronomers, none has a greater influence on life
on planet Earth than its own satellite, the moon. When you think
about it.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<h2>Display 2 Heading</h2> <h2>Display 2 Heading</h2>
<p> <p>
None has a greater influence on life on planet Earth than its own satellite, the moon. When you think None has a greater influence on life on planet Earth than its own
about it. satellite, the moon. When you think about it.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<h3>Display 3 Heading</h3> <h3>Display 3 Heading</h3>
<p> <p>
Lets talk about meat fondue recipes and what you need to know first. Meat fondue also known as oil fondue Lets talk about meat fondue recipes and what you need to know
is a method of cooking all kinds of meats, poultry, and seafood in a pot of heated oil. first. Meat fondue also known as oil fondue is a method of cooking
all kinds of meats, poultry, and seafood in a pot of heated oil.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<h4>Display 4 Heading</h4> <h4>Display 4 Heading</h4>
<p> <p>
There is something about parenthood that gives us a sense of history and a deeply rooted desire to send on There is something about parenthood that gives us a sense of
into the next generation the great things we have discovered about life. history and a deeply rooted desire to send on into the next
generation the great things we have discovered about life.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<h5>Display 5 Heading</h5> <h5>Display 5 Heading</h5>
<p> <p>
There is a moment in the life of any aspiring astronomer that it is time to buy that first telescope. Its There is a moment in the life of any aspiring astronomer that it
exciting to think about setting up your own viewing station. is time to buy that first telescope. Its exciting to think about
setting up your own viewing station.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<p> <p>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a Of all of the celestial bodies that capture our attention and
greater influence on life on planet Earth than its own satellite, the moon. When you think about it. fascination as astronomers, none has a greater influence on life
on planet Earth than its own satellite, the moon. When you think
about it.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<div class="text--secondary"> <div class="text--secondary">
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a Of all of the celestial bodies that capture our attention and
greater influence on life on planet Earth than its own satellite, the moon. When you think about it. fascination as astronomers, none has a greater influence on life
on planet Earth than its own satellite, the moon. When you think
about it.
</div> </div>
</div> </div>
<div class="mb-8"> <div class="mb-8">
@ -59,9 +68,11 @@
&lt;/p></pre &lt;/p></pre
> >
<p> <p>
Of all of the celestial bodies that capture our attention and fascination as astronomers, Of all of the celestial bodies that capture our attention and
<span class="text--code">currentColor</span> none has a greater influence on life on planet Earth than fascination as astronomers,
its own satellite, the moon. <span class="text--code">currentColor</span> none has a greater
influence on life on planet Earth than its own satellite, the
moon.
</p> </p>
</div> </div>
</VaCardContent> </VaCardContent>
@ -73,10 +84,12 @@
<p class="va-h3">Lists</p> <p class="va-h3">Lists</p>
<ol class="va-ordered"> <ol class="va-ordered">
<li> <li>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a Of all of the celestial bodies that capture our attention and
greater influence. fascination as astronomers, none has a greater influence.
</li>
<li>
Earth than its own satellite, the moon. When you think about it.
</li> </li>
<li>Earth than its own satellite, the moon. When you think about it.</li>
<li>Attention and fascination as.</li> <li>Attention and fascination as.</li>
</ol> </ol>
<ol class="va-ordered"> <ol class="va-ordered">
@ -104,10 +117,12 @@
</ol> </ol>
<ul class="va-unordered"> <ul class="va-unordered">
<li> <li>
Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a Of all of the celestial bodies that capture our attention and
greater influence. fascination as astronomers, none has a greater influence.
</li>
<li>
Earth than its own satellite, the moon. When you think about it.
</li> </li>
<li>Earth than its own satellite, the moon. When you think about it.</li>
<li>Attention and fascination as .</li> <li>Attention and fascination as .</li>
</ul> </ul>
<ul class="va-unordered"> <ul class="va-unordered">
@ -135,22 +150,28 @@
</ul> </ul>
<p class="va-h3">Links</p> <p class="va-h3">Links</p>
<div class="mb-8"> <div class="mb-8">
<a class="link mr-8" href="/default" @click.prevent> Default Link </a> <a class="link mr-8" href="/default" @click.prevent>
<a class="link-secondary" href="/secondary" @click.prevent> Secondary Link </a> Default Link
</a>
<a class="link-secondary" href="/secondary" @click.prevent>
Secondary Link
</a>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<p class="va-h3">Other Elements</p> <p class="va-h3">Other Elements</p>
<p> <p>
None has a greater influence on None has a greater influence on
<span class="text--highlighted">highlighted text</span> <span class="text--highlighted">highlighted text</span>
life on planet Earth than its own satellite, the selected chunk of text. When you think about it. life on planet Earth than its own satellite, the selected chunk
of text. When you think about it.
</p> </p>
</div> </div>
<div class="mb-8"> <div class="mb-8">
<blockquote class="va-blockquote border-primary"> <blockquote class="va-blockquote border-primary">
<p> <p>
BQ: Lets talk about meat fondue recipes and what you need to know first. Meat fondue also known as oil BQ: Lets talk about meat fondue recipes and what you need to
fondue is a method of cooking all kinds. know first. Meat fondue also known as oil fondue is a method of
cooking all kinds.
</p> </p>
<p> <p>
<i> Mister Lebowski</i> <i> Mister Lebowski</i>
@ -161,9 +182,10 @@
<div class="text-block"> <div class="text-block">
<p class="va-h3">va-h3 Heading</p> <p class="va-h3">va-h3 Heading</p>
<span <span
>Of all of the celestial bodies that capture our attention and fascination as astronomers, none has a >Of all of the celestial bodies that capture our attention and
greater influence on life on planet Earth than its own satellite, the moon. When you think about fascination as astronomers, none has a greater influence on life
it.</span on planet Earth than its own satellite, the moon. When you
think about it.</span
> >
</div> </div>
</div> </div>
@ -171,11 +193,16 @@
<table class="va-table"> <table class="va-table">
<thead> <thead>
<tr> <tr>
<th v-for="(data, index) in tableData[0]" :key="index">{{ data }}</th> <th v-for="(data, index) in tableData[0]" :key="index">
{{ data }}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(rowData, rowIndex) in tableData.slice(1)" :key="rowIndex"> <tr
v-for="(rowData, rowIndex) in tableData.slice(1)"
:key="rowIndex"
>
<td v-for="(itemData, colIndex) in rowData" :key="colIndex"> <td v-for="(itemData, colIndex) in rowData" :key="colIndex">
{{ itemData }} {{ itemData }}
</td> </td>
@ -190,17 +217,17 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from "vue";
// import { useI18n } from 'vue-i18n' // import { useI18n } from 'vue-i18n'
// //
// const { t } = useI18n() // const { t } = useI18n()
const tableData = computed(() => [ const tableData = computed(() => [
['Id', 'FooBar type', 'Actions'], ["Id", "FooBar type", "Actions"],
['1', 'Zebra', 'Delete'], ["1", "Zebra", "Delete"],
['2', 'Not Zebra', 'Remove'], ["2", "Not Zebra", "Remove"],
['3', 'Very Zebra', 'Eradicate'], ["3", "Very Zebra", "Eradicate"],
]) ]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,28 +1,38 @@
<template> <template>
<component :is="chartComponent" :chart-data="data" :data="data" :options="chartOptions" class="va-chart" /> <component
:is="chartComponent"
:chart-data="data"
:data="data"
:options="chartOptions"
class="va-chart"
/>
</template> </template>
<script lang="ts" setup generic="T extends 'line' | 'bar' | 'bubble' | 'doughnut' | 'pie'"> <script
import { computed } from 'vue' lang="ts"
import type { ChartOptions, ChartData } from 'chart.js' setup
import { defaultConfig, chartTypesMap } from './vaChartConfigs' generic="T extends 'line' | 'bar' | 'bubble' | 'doughnut' | 'pie'"
>
import { computed } from "vue";
import type { ChartOptions, ChartData } from "chart.js";
import { defaultConfig, chartTypesMap } from "./vaChartConfigs";
defineOptions({ defineOptions({
name: 'VaChart', name: "VaChart",
}) });
const props = defineProps<{ const props = defineProps<{
data: ChartData<T> data: ChartData<T>;
options?: ChartOptions<T> options?: ChartOptions<T>;
type: T type: T;
}>() }>();
const chartComponent = chartTypesMap[props.type] const chartComponent = chartTypesMap[props.type];
const chartOptions = computed<ChartOptions<T>>(() => ({ const chartOptions = computed<ChartOptions<T>>(() => ({
...(defaultConfig as any), ...(defaultConfig as any),
...props.options, ...props.options,
})) }));
</script> </script>
<style lang="scss"> <style lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { defineAsyncComponent, markRaw } from 'vue' import { defineAsyncComponent, markRaw } from "vue";
const DEFAULT_FONT_FAMILY = "'Inter', sans-serif" const DEFAULT_FONT_FAMILY = "'Inter', sans-serif";
export const defaultConfig = { export const defaultConfig = {
scales: { scales: {
@ -21,10 +21,10 @@ export const defaultConfig = {
}, },
plugins: { plugins: {
legend: { legend: {
position: 'bottom', position: "bottom",
labels: { labels: {
font: { font: {
color: '#34495e', color: "#34495e",
family: DEFAULT_FONT_FAMILY, family: DEFAULT_FONT_FAMILY,
size: 14, size: 14,
}, },
@ -41,23 +41,23 @@ export const defaultConfig = {
}, },
datasets: { datasets: {
line: { line: {
fill: 'origin', fill: "origin",
tension: 0.3, tension: 0.3,
borderColor: 'transparent', borderColor: "transparent",
}, },
bubble: { bubble: {
borderColor: 'transparent', borderColor: "transparent",
}, },
bar: { bar: {
borderColor: 'transparent', borderColor: "transparent",
}, },
}, },
maintainAspectRatio: false, maintainAspectRatio: false,
animation: true, animation: true,
} };
export const doughnutConfig = { export const doughnutConfig = {
cutout: '80%', cutout: "80%",
scales: { scales: {
x: { x: {
display: false, display: false,
@ -82,26 +82,38 @@ export const doughnutConfig = {
}, },
datasets: { datasets: {
line: { line: {
fill: 'origin', fill: "origin",
tension: 0.3, tension: 0.3,
borderColor: 'transparent', borderColor: "transparent",
}, },
bubble: { bubble: {
borderColor: 'transparent', borderColor: "transparent",
}, },
bar: { bar: {
borderColor: 'transparent', borderColor: "transparent",
}, },
}, },
maintainAspectRatio: false, maintainAspectRatio: false,
animation: true, animation: true,
} };
export const chartTypesMap = { export const chartTypesMap = {
pie: markRaw(defineAsyncComponent(() => import('./chart-types/PieChart.vue'))), pie: markRaw(
doughnut: markRaw(defineAsyncComponent(() => import('./chart-types/DoughnutChart.vue'))), defineAsyncComponent(() => import("./chart-types/PieChart.vue")),
bubble: markRaw(defineAsyncComponent(() => import('./chart-types/BubbleChart.vue'))), ),
line: markRaw(defineAsyncComponent(() => import('./chart-types/LineChart.vue'))), doughnut: markRaw(
bar: markRaw(defineAsyncComponent(() => import('./chart-types/BarChart.vue'))), defineAsyncComponent(() => import("./chart-types/DoughnutChart.vue")),
'horizontal-bar': markRaw(defineAsyncComponent(() => import('./chart-types/HorizontalBarChart.vue'))), ),
} bubble: markRaw(
defineAsyncComponent(() => import("./chart-types/BubbleChart.vue")),
),
line: markRaw(
defineAsyncComponent(() => import("./chart-types/LineChart.vue")),
),
bar: markRaw(
defineAsyncComponent(() => import("./chart-types/BarChart.vue")),
),
"horizontal-bar": markRaw(
defineAsyncComponent(() => import("./chart-types/HorizontalBarChart.vue")),
),
};

View File

@ -5,56 +5,56 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, Ref, onMounted, onBeforeUnmount } from 'vue' import { ref, Ref, onMounted, onBeforeUnmount } from "vue";
import MediumEditor from 'medium-editor' import MediumEditor from "medium-editor";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
editorOptions?: { editorOptions?: {
buttonLabels: string buttonLabels: string;
autoLink: boolean autoLink: boolean;
toolbar: { toolbar: {
buttons: string[] buttons: string[];
} };
} };
}>(), }>(),
{ {
editorOptions: () => ({ editorOptions: () => ({
buttonLabels: 'fontawesome', buttonLabels: "fontawesome",
autoLink: true, autoLink: true,
toolbar: { toolbar: {
buttons: ['bold', 'italic', 'underline', 'anchor', 'h1', 'h2', 'h3'], buttons: ["bold", "italic", "underline", "anchor", "h1", "h2", "h3"],
}, },
}), }),
}, },
) );
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'initialized', editor: typeof MediumEditor): void (e: "initialized", editor: typeof MediumEditor): void;
}>() }>();
const editorElement: Ref<null | HTMLElement> = ref(null) const editorElement: Ref<null | HTMLElement> = ref(null);
let editor: typeof MediumEditor | null = null let editor: typeof MediumEditor | null = null;
onMounted(() => { onMounted(() => {
if (!editorElement.value) { if (!editorElement.value) {
return return;
} }
editor = new MediumEditor(editorElement.value, props.editorOptions) editor = new MediumEditor(editorElement.value, props.editorOptions);
emit('initialized', editor) emit("initialized", editor);
}) });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (editor) { if (editor) {
editor.destroy() editor.destroy();
} }
}) });
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'medium-editor/src/sass/medium-editor'; @import "medium-editor/src/sass/medium-editor";
@import 'variables'; @import "variables";
$medium-editor-shadow: var(--va-box-shadow); $medium-editor-shadow: var(--va-box-shadow);
$medium-editor-background-color: var(--va-divider); $medium-editor-background-color: var(--va-divider);
@ -163,7 +163,8 @@ $medium-editor-active-text-color: var(--va-white);
} }
.medium-toolbar-arrow-under::after { .medium-toolbar-arrow-under::after {
border-color: $medium-editor-background-color transparent transparent transparent; border-color: $medium-editor-background-color transparent transparent
transparent;
top: 100%; top: 100%;
} }

View File

@ -22,9 +22,9 @@
defineProps({ defineProps({
date: { date: {
type: String, type: String,
default: '', default: "",
}, },
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -47,7 +47,7 @@ defineProps({
height: 100%; height: 100%;
&::after { &::after {
content: ''; content: "";
width: 2px; width: 2px;
height: 100%; height: 100%;
background: var(--va-background-border); background: var(--va-background-border);

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { TBubbleChartData } from '../types' import { TBubbleChartData } from "../types";
export const bubbleChartData: TBubbleChartData = { export const bubbleChartData: TBubbleChartData = {
datasets: [ datasets: [
{ {
label: 'USA', label: "USA",
backgroundColor: 'danger', backgroundColor: "danger",
data: [ data: [
{ {
x: 23, x: 23,
@ -49,8 +49,8 @@ export const bubbleChartData: TBubbleChartData = {
], ],
}, },
{ {
label: 'Russia', label: "Russia",
backgroundColor: 'primary', backgroundColor: "primary",
data: [ data: [
{ {
x: 0, x: 0,
@ -95,8 +95,8 @@ export const bubbleChartData: TBubbleChartData = {
], ],
}, },
{ {
label: 'Canada', label: "Canada",
backgroundColor: 'warning', backgroundColor: "warning",
data: [ data: [
{ {
x: 10, x: 10,
@ -136,8 +136,8 @@ export const bubbleChartData: TBubbleChartData = {
], ],
}, },
{ {
label: 'Belarus', label: "Belarus",
backgroundColor: 'info', backgroundColor: "info",
data: [ data: [
{ {
x: 35, x: 35,
@ -182,8 +182,8 @@ export const bubbleChartData: TBubbleChartData = {
], ],
}, },
{ {
label: 'Ukraine', label: "Ukraine",
backgroundColor: 'success', backgroundColor: "success",
data: [ data: [
{ {
x: 25, x: 25,
@ -228,4 +228,4 @@ export const bubbleChartData: TBubbleChartData = {
], ],
}, },
], ],
} };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,63 +18,63 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from "vue";
import { useColors } from 'vuestic-ui' import { useColors } from "vuestic-ui";
import DataSectionItem from './DataSectionItem.vue' import DataSectionItem from "./DataSectionItem.vue";
interface DashboardMetric { interface DashboardMetric {
id: string id: string;
title: string title: string;
value: string value: string;
icon: string icon: string;
changeText: string changeText: string;
changeDirection: 'up' | 'down' changeDirection: "up" | "down";
iconBackground: string iconBackground: string;
iconColor: string iconColor: string;
} }
const { getColor } = useColors() const { getColor } = useColors();
const dashboardMetrics = computed<DashboardMetric[]>(() => [ const dashboardMetrics = computed<DashboardMetric[]>(() => [
{ {
id: 'openInvoices', id: "openInvoices",
title: 'Open invoices', title: "Open invoices",
value: '$35,548', value: "$35,548",
icon: 'mso-attach_money', icon: "mso-attach_money",
changeText: '$1, 450', changeText: "$1, 450",
changeDirection: 'down', changeDirection: "down",
iconBackground: getColor('success'), iconBackground: getColor("success"),
iconColor: getColor('on-success'), iconColor: getColor("on-success"),
}, },
{ {
id: 'ongoingProjects', id: "ongoingProjects",
title: 'Ongoing project', title: "Ongoing project",
value: '15', value: "15",
icon: 'mso-folder_open', icon: "mso-folder_open",
changeText: '25.36%', changeText: "25.36%",
changeDirection: 'up', changeDirection: "up",
iconBackground: getColor('info'), iconBackground: getColor("info"),
iconColor: getColor('on-info'), iconColor: getColor("on-info"),
}, },
{ {
id: 'employees', id: "employees",
title: 'Employees', title: "Employees",
value: '25', value: "25",
icon: 'mso-account_circle', icon: "mso-account_circle",
changeText: '2.5%', changeText: "2.5%",
changeDirection: 'up', changeDirection: "up",
iconBackground: getColor('danger'), iconBackground: getColor("danger"),
iconColor: getColor('on-danger'), iconColor: getColor("on-danger"),
}, },
{ {
id: 'newProfit', id: "newProfit",
title: 'New profit', title: "New profit",
value: '27%', value: "27%",
icon: 'mso-grade', icon: "mso-grade",
changeText: '4%', changeText: "4%",
changeDirection: 'up', changeDirection: "up",
iconBackground: getColor('warning'), iconBackground: getColor("warning"),
iconColor: getColor('on-warning'), iconColor: getColor("on-warning"),
}, },
]) ]);
</script> </script>

View File

@ -31,20 +31,20 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from "vue";
import { VaCard } from 'vuestic-ui' import { VaCard } from "vuestic-ui";
const props = defineProps<{ const props = defineProps<{
title: string title: string;
value: string | number value: string | number;
changeText: string changeText: string;
up: boolean up: boolean;
iconBackground: string iconBackground: string;
iconColor: string iconColor: string;
}>() }>();
const changeClass = computed(() => ({ const changeClass = computed(() => ({
'text-success': props.up, "text-success": props.up,
'text-red-600': !props.up, "text-red-600": !props.up,
})) }));
</script> </script>

View File

@ -1,7 +1,9 @@
<template> <template>
<VaCard> <VaCard>
<VaCardTitle> <VaCardTitle>
<h1 class="card-title text-tag text-secondary font-bold uppercase">Monthly Earnings</h1> <h1 class="card-title text-tag text-secondary font-bold uppercase">
Monthly Earnings
</h1>
</VaCardTitle> </VaCardTitle>
<VaCardContent> <VaCardContent>
<div class="p-1 bg-black rounded absolute right-4 top-4"> <div class="p-1 bg-black rounded absolute right-4 top-4">
@ -16,22 +18,27 @@
</p> </p>
</section> </section>
<div class="w-full flex items-center"> <div class="w-full flex items-center">
<VaChart :data="chartData" class="h-24" type="line" :options="options" /> <VaChart
:data="chartData"
class="h-24"
type="line"
:options="options"
/>
</div> </div>
</VaCardContent> </VaCardContent>
</VaCard> </VaCard>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { VaCard } from 'vuestic-ui' import { VaCard } from "vuestic-ui";
import VaChart from '../../../../components/va-charts/VaChart.vue' import VaChart from "../../../../components/va-charts/VaChart.vue";
import { useChartData } from '../../../../data/charts/composables/useChartData' import { useChartData } from "../../../../data/charts/composables/useChartData";
import { lineChartData } from '../../../../data/charts/lineChartData' import { lineChartData } from "../../../../data/charts/lineChartData";
import { ChartOptions } from 'chart.js' import { ChartOptions } from "chart.js";
const chartData = useChartData(lineChartData) const chartData = useChartData(lineChartData);
const options: ChartOptions<'line'> = { const options: ChartOptions<"line"> = {
scales: { scales: {
x: { x: {
display: false, display: false,
@ -51,7 +58,7 @@ const options: ChartOptions<'line'> = {
}, },
interaction: { interaction: {
intersect: false, intersect: false,
mode: 'index', mode: "index",
}, },
plugins: { plugins: {
legend: { legend: {
@ -61,5 +68,5 @@ const options: ChartOptions<'line'> = {
enabled: true, enabled: true,
}, },
}, },
} };
</script> </script>

View File

@ -1,35 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineVaDataTableColumns } from 'vuestic-ui' import { defineVaDataTableColumns } from "vuestic-ui";
import { Project } from '../../../projects/types' import { Project } from "../../../projects/types";
import UserAvatar from '../../../users/widgets/UserAvatar.vue' import UserAvatar from "../../../users/widgets/UserAvatar.vue";
import ProjectStatusBadge from '../../../projects/components/ProjectStatusBadge.vue' import ProjectStatusBadge from "../../../projects/components/ProjectStatusBadge.vue";
import { useProjects } from '../../../projects/composables/useProjects' import { useProjects } from "../../../projects/composables/useProjects";
import { Pagination } from '../../../../data/pages/projects' import { Pagination } from "../../../../data/pages/projects";
import { ref } from 'vue' import { ref } from "vue";
const columns = defineVaDataTableColumns([ const columns = defineVaDataTableColumns([
{ label: 'Name', key: 'project_name', sortable: true }, { label: "Name", key: "project_name", sortable: true },
{ label: 'Status', key: 'status', sortable: true }, { label: "Status", key: "status", sortable: true },
{ label: 'Team', key: 'team', sortable: true }, { label: "Team", key: "team", sortable: true },
]) ]);
const pagination = ref<Pagination>({ page: 1, perPage: 5, total: 0 }) const pagination = ref<Pagination>({ page: 1, perPage: 5, total: 0 });
const { projects, isLoading, sorting } = useProjects({ const { projects, isLoading, sorting } = useProjects({
pagination, pagination,
}) });
const avatarColor = (userName: string) => { const avatarColor = (userName: string) => {
const colors = ['primary', '#FFD43A', '#ADFF00', '#262824', 'danger'] const colors = ["primary", "#FFD43A", "#ADFF00", "#262824", "danger"];
const index = userName.charCodeAt(0) % colors.length const index = userName.charCodeAt(0) % colors.length;
return colors[index] return colors[index];
} };
</script> </script>
<template> <template>
<VaCard> <VaCard>
<VaCardTitle class="flex items-start justify-between"> <VaCardTitle class="flex items-start justify-between">
<h1 class="card-title text-secondary font-bold uppercase">Projects</h1> <h1 class="card-title text-secondary font-bold uppercase">Projects</h1>
<VaButton preset="primary" size="small" to="/projects">View all projects</VaButton> <VaButton preset="primary" size="small" to="/projects"
>View all projects</VaButton
>
</VaCardTitle> </VaCardTitle>
<VaCardContent> <VaCardContent>
<div v-if="projects.length > 0"> <div v-if="projects.length > 0">
@ -70,7 +72,12 @@ const avatarColor = (userName: string) => {
</template> </template>
</VaDataTable> </VaDataTable>
</div> </div>
<div v-else class="p-4 flex justify-center items-center text-[var(--va-secondary)]">No projects</div> <div
v-else
class="p-4 flex justify-center items-center text-[var(--va-secondary)]"
>
No projects
</div>
</VaCardContent> </VaCardContent>
</VaCard> </VaCard>
</template> </template>

View File

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

View File

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

View File

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

View File

@ -5,60 +5,71 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from "vue";
import { Chart, registerables } from 'chart.js' import { Chart, registerables } from "chart.js";
import type { Revenues } from '../../../../data/charts/revenueChartData' import type { Revenues } from "../../../../data/charts/revenueChartData";
import { earningsColor, expensesColor, formatMoney } from '../../../../data/charts/revenueChartData' import {
earningsColor,
expensesColor,
formatMoney,
} from "../../../../data/charts/revenueChartData";
const { revenues, months } = defineProps<{ const { revenues, months } = defineProps<{
months: string[] months: string[];
revenues: Revenues[] revenues: Revenues[];
}>() }>();
Chart.register(...registerables) Chart.register(...registerables);
const BR_THICKNESS = 4 const BR_THICKNESS = 4;
Chart.register([ Chart.register([
{ {
id: 'background-color', id: "background-color",
beforeDatasetDraw: function (chart) { beforeDatasetDraw: function (chart) {
const ctx = chart.ctx const ctx = chart.ctx;
const config = chart.config const config = chart.config;
config.data.datasets.forEach(function (dataset, datasetIndex) { config.data.datasets.forEach(function (dataset, datasetIndex) {
const meta = chart.getDatasetMeta(datasetIndex) const meta = chart.getDatasetMeta(datasetIndex);
if (meta.type === 'bar') { if (meta.type === "bar") {
const bgColor = earningsColor const bgColor = earningsColor;
// Loop through each bar in the dataset // Loop through each bar in the dataset
meta.data.forEach(function (bar) { meta.data.forEach(function (bar) {
ctx.fillStyle = bgColor ctx.fillStyle = bgColor;
ctx.fillRect(bar.x - BR_THICKNESS / 2, 0, BR_THICKNESS, chart.chartArea.bottom) ctx.fillRect(
}) bar.x - BR_THICKNESS / 2,
0,
BR_THICKNESS,
chart.chartArea.bottom,
);
});
} }
}) });
}, },
}, },
]) ]);
const canvas = ref<HTMLCanvasElement | null>(null) const canvas = ref<HTMLCanvasElement | null>(null);
const doShowChart = ref(false) const doShowChart = ref(false);
onMounted(() => { onMounted(() => {
if (canvas.value) { if (canvas.value) {
const ctx = canvas.value.getContext('2d') const ctx = canvas.value.getContext("2d");
if (ctx) { if (ctx) {
new Chart(ctx, { new Chart(ctx, {
type: 'bar', type: "bar",
data: { data: {
labels: months, labels: months,
datasets: [ datasets: [
{ {
// Show relative expenses ratio // Show relative expenses ratio
data: revenues.map(({ earning, expenses }) => (expenses / earning) * 100), data: revenues.map(
({ earning, expenses }) => (expenses / earning) * 100,
),
backgroundColor: expensesColor, backgroundColor: expensesColor,
barThickness: BR_THICKNESS, barThickness: BR_THICKNESS,
}, },
@ -86,20 +97,20 @@ onMounted(() => {
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
callback: function (value) { callback: function (value) {
return formatMoney(Number(value)) return formatMoney(Number(value));
}, },
}, },
}, },
}, },
}, },
}) });
} }
} }
nextTick(() => { nextTick(() => {
doShowChart.value = true doShowChart.value = true;
}) });
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import VaTimelineItem from '../../../../components/va-timeline-item.vue' import VaTimelineItem from "../../../../components/va-timeline-item.vue";
</script> </script>
<template> <template>
@ -11,30 +11,55 @@ import VaTimelineItem from '../../../../components/va-timeline-item.vue'
<table class="mt-4"> <table class="mt-4">
<tbody> <tbody>
<VaTimelineItem date="25m ago"> <VaTimelineItem date="25m ago">
<RouterLink class="va-link font-semibold" to="/users">Donald</RouterLink> updated the status of <RouterLink class="va-link font-semibold" to="/users"
<RouterLink class="va-link font-semibold" to="/users">Refund #1234</RouterLink> to awaiting customer >Donald</RouterLink
response >
updated the status of
<RouterLink class="va-link font-semibold" to="/users"
>Refund #1234</RouterLink
>
to awaiting customer response
</VaTimelineItem> </VaTimelineItem>
<VaTimelineItem date="1h ago"> <VaTimelineItem date="1h ago">
<RouterLink class="va-link font-semibold" to="/users">Lycy Peterson</RouterLink> was added to the group, <RouterLink class="va-link font-semibold" to="/users"
group name is Overtake >Lycy Peterson</RouterLink
>
was added to the group, group name is Overtake
</VaTimelineItem> </VaTimelineItem>
<VaTimelineItem date="2h ago"> <VaTimelineItem date="2h ago">
<RouterLink class="va-link font-semibold" to="/users">Joseph Rust</RouterLink> opened new showcase <RouterLink class="va-link font-semibold" to="/users"
<RouterLink class="va-link font-semibold" to="/users">Mannat #112233</RouterLink> with theme market >Joseph Rust</RouterLink
>
opened new showcase
<RouterLink class="va-link font-semibold" to="/users"
>Mannat #112233</RouterLink
>
with theme market
</VaTimelineItem> </VaTimelineItem>
<VaTimelineItem date="3d ago"> <VaTimelineItem date="3d ago">
<RouterLink class="va-link font-semibold" to="/users">Donald</RouterLink> updated the status to awaiting <RouterLink class="va-link font-semibold" to="/users"
customer response >Donald</RouterLink
>
updated the status to awaiting customer response
</VaTimelineItem> </VaTimelineItem>
<VaTimelineItem date="Nov 14, 2023"> <VaTimelineItem date="Nov 14, 2023">
<RouterLink class="va-link font-semibold" to="/users">Lycy Peterson</RouterLink> was added to the group <RouterLink class="va-link font-semibold" to="/users"
>Lycy Peterson</RouterLink
>
was added to the group
</VaTimelineItem> </VaTimelineItem>
<VaTimelineItem date="Nov 14, 2023"> <VaTimelineItem date="Nov 14, 2023">
<RouterLink class="va-link font-semibold" to="/users">Dan Rya</RouterLink> was added to the group <RouterLink class="va-link font-semibold" to="/users"
>Dan Rya</RouterLink
>
was added to the group
</VaTimelineItem> </VaTimelineItem>
<VaTimelineItem date="Nov 15, 2023"> <VaTimelineItem date="Nov 15, 2023">
Project <RouterLink class="va-link font-semibold" to="/projects">Vuestic 2023</RouterLink> was created Project
<RouterLink class="va-link font-semibold" to="/projects"
>Vuestic 2023</RouterLink
>
was created
</VaTimelineItem> </VaTimelineItem>
</tbody> </tbody>
</table> </table>

View File

@ -1,7 +1,9 @@
<template> <template>
<VaCard> <VaCard>
<VaCardTitle class="pb-0!"> <VaCardTitle class="pb-0!">
<h1 class="card-title text-secondary font-bold uppercase">Yearly Breakup</h1> <h1 class="card-title text-secondary font-bold uppercase">
Yearly Breakup
</h1>
</VaCardTitle> </VaCardTitle>
<VaCardContent class="flex flex-row gap-1"> <VaCardContent class="flex flex-row gap-1">
<section class="w-1/2"> <section class="w-1/2">
@ -13,11 +15,17 @@
</p> </p>
<div class="my-4 gap-2 flex flex-col"> <div class="my-4 gap-2 flex flex-col">
<div class="flex items-center"> <div class="flex items-center">
<span class="inline-block w-2 h-2 mr-2" :style="{ backgroundColor: earningsBackground }"></span> <span
class="inline-block w-2 h-2 mr-2"
:style="{ backgroundColor: earningsBackground }"
></span>
<span class="text-secondary">Earnings</span> <span class="text-secondary">Earnings</span>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<span class="inline-block w-2 h-2 mr-2" :style="{ backgroundColor: profitBackground }"></span> <span
class="inline-block w-2 h-2 mr-2"
:style="{ backgroundColor: profitBackground }"
></span>
<span class="text-secondary">Profit</span> <span class="text-secondary">Profit</span>
</div> </div>
</div> </div>
@ -36,27 +44,37 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { VaCard } from 'vuestic-ui' import { VaCard } from "vuestic-ui";
import VaChart from '../../../../components/va-charts/VaChart.vue' import VaChart from "../../../../components/va-charts/VaChart.vue";
import { useChartData } from '../../../../data/charts/composables/useChartData' import { useChartData } from "../../../../data/charts/composables/useChartData";
import { doughnutChartData, profitBackground, earningsBackground } from '../../../../data/charts/doughnutChartData' import {
import { doughnutConfig } from '../../../../components/va-charts/vaChartConfigs' doughnutChartData,
import { ChartOptions } from 'chart.js' profitBackground,
import { externalTooltipHandler } from '../../../../components/va-charts/external-tooltip' earningsBackground,
} from "../../../../data/charts/doughnutChartData";
import { doughnutConfig } from "../../../../components/va-charts/vaChartConfigs";
import { ChartOptions } from "chart.js";
import { externalTooltipHandler } from "../../../../components/va-charts/external-tooltip";
const chartData = useChartData(doughnutChartData) const chartData = useChartData(doughnutChartData);
const options: ChartOptions<'doughnut'> = { const options: ChartOptions<"doughnut"> = {
...doughnutConfig, ...doughnutConfig,
plugins: { plugins: {
...doughnutConfig.plugins, ...doughnutConfig.plugins,
tooltip: { tooltip: {
// Chart to small to show tooltips // Chart to small to show tooltips
enabled: false, enabled: false,
position: 'nearest', position: "nearest",
external: externalTooltipHandler, external: externalTooltipHandler,
}, },
}, },
circumference: 360 * (chartData.value.datasets[0].data.reduce((acc: number, d: number) => acc + d, 0) / 800), circumference:
} 360 *
(chartData.value.datasets[0].data.reduce(
(acc: number, d: number) => acc + d,
0,
) /
800),
};
</script> </script>

View File

@ -10,7 +10,7 @@
{{ item.label }} {{ item.label }}
<div class="not-found-pages__button-container pt-4 mb-0"> <div class="not-found-pages__button-container pt-4 mb-0">
<VaButton :to="{ name: item.buttonTo }"> <VaButton :to="{ name: item.buttonTo }">
{{ 'View Example' }} {{ "View Example" }}
</VaButton> </VaButton>
</div> </div>
</VaCardContent> </VaCardContent>
@ -19,28 +19,28 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from "vue";
const items = ref([ const items = ref([
{ {
imageUrl: 'https://i.imgur.com/GzUR0Wz.png', imageUrl: "https://i.imgur.com/GzUR0Wz.png",
label: 'Advanced layouts', label: "Advanced layouts",
buttonTo: 'not-found-advanced', buttonTo: "not-found-advanced",
}, },
{ {
imageUrl: 'https://i.imgur.com/HttcXPi.png', imageUrl: "https://i.imgur.com/HttcXPi.png",
label: 'Simple', label: "Simple",
buttonTo: 'not-found-simple', buttonTo: "not-found-simple",
}, },
{ {
imageUrl: 'https://i.imgur.com/dlcZMiG.png', imageUrl: "https://i.imgur.com/dlcZMiG.png",
label: 'Custom image', label: "Custom image",
buttonTo: 'not-found-custom', buttonTo: "not-found-custom",
}, },
{ {
imageUrl: 'https://i.imgur.com/qcOlDz7.png', imageUrl: "https://i.imgur.com/qcOlDz7.png",
label: 'Large text heading', label: "Large text heading",
buttonTo: 'not-found-large-text', buttonTo: "not-found-large-text",
}, },
]) ]);
</script> </script>

View File

@ -2,8 +2,9 @@
<div> <div>
<h1 class="font-semibold text-4xl mb-4">Check the email</h1> <h1 class="font-semibold text-4xl mb-4">Check the email</h1>
<p class="text-base mb-4 leading-5"> <p class="text-base mb-4 leading-5">
Password reset instructions have been sent to your email. Check your inbox, including the spam folder if needed. Password reset instructions have been sent to your email. Check your
For assistance, <span class="va-link">contact support</span>. inbox, including the spam folder if needed. For assistance,
<span class="va-link">contact support</span>.
</p> </p>
<div class="flex justify-center mt-4"> <div class="flex justify-center mt-4">

View File

@ -3,24 +3,52 @@
<h1 class="font-semibold text-4xl mb-4">Войти</h1> <h1 class="font-semibold text-4xl mb-4">Войти</h1>
<p class="text-base mb-4 leading-5"> <p class="text-base mb-4 leading-5">
Впервые здесь? Впервые здесь?
<RouterLink :to="{ name: 'signup' }" class="font-semibold text-primary">Регистрация</RouterLink> <RouterLink :to="{ name: 'signup' }" class="font-semibold text-primary"
>Регистрация</RouterLink
>
</p> </p>
<VaInput v-model="formData.email" :rules="[validators.required, validators.email]" class="mb-4" label="Email" <VaInput
type="email" /> v-model="formData.email"
:rules="[validators.required, validators.email]"
class="mb-4"
label="Email"
type="email"
/>
<VaValue v-slot="isPasswordVisible" :default-value="false"> <VaValue v-slot="isPasswordVisible" :default-value="false">
<VaInput v-model="formData.password" :rules="[validators.required]" <VaInput
:type="isPasswordVisible.value ? 'text' : 'password'" class="mb-4" label="Пароль" v-model="formData.password"
@clickAppendInner.stop="isPasswordVisible.value = !isPasswordVisible.value"> :rules="[validators.required]"
:type="isPasswordVisible.value ? 'text' : 'password'"
class="mb-4"
label="Пароль"
@clickAppendInner.stop="
isPasswordVisible.value = !isPasswordVisible.value
"
>
<template #appendInner> <template #appendInner>
<VaIcon :name="isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'" class="cursor-pointer" <VaIcon
color="secondary" /> :name="
isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'
"
class="cursor-pointer"
color="secondary"
/>
</template> </template>
</VaInput> </VaInput>
</VaValue> </VaValue>
<div class="auth-layout__options flex flex-col sm:flex-row items-start sm:items-center justify-between"> <div
<VaCheckbox v-model="formData.keepLoggedIn" class="mb-2 sm:mb-0" label="Запомнить меня" /> class="auth-layout__options flex flex-col sm:flex-row items-start sm:items-center justify-between"
<RouterLink :to="{ name: 'recover-password' }" class="mt-2 sm:mt-0 sm:ml-1 font-semibold text-primary"> >
<VaCheckbox
v-model="formData.keepLoggedIn"
class="mb-2 sm:mb-0"
label="Запомнить меня"
/>
<RouterLink
:to="{ name: 'recover-password' }"
class="mt-2 sm:mt-0 sm:ml-1 font-semibold text-primary"
>
Забыли пароль? Забыли пароль?
</RouterLink> </RouterLink>
</div> </div>
@ -32,31 +60,35 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import axios from 'axios'; import axios from "axios";
import { reactive } from 'vue' import { reactive } from "vue";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
import { useForm, useToast } from 'vuestic-ui' import { useForm, useToast } from "vuestic-ui";
import { validators } from '../../services/utils' import { validators } from "../../services/utils";
const { validate } = useForm('form') const { validate } = useForm("form");
const { push } = useRouter() const { push } = useRouter();
const { init } = useToast() const { init } = useToast();
const formData = reactive({ const formData = reactive({
email: '', email: "",
password: '', password: "",
keepLoggedIn: false, keepLoggedIn: false,
}) });
const submit = () => { const submit = () => {
if (validate()) { if (validate()) {
axios.post('http://localhost:3000/api/v0/signin', {"login": formData.email, "password":formData.password }) axios
.then(response => { .post("http://localhost:3000/api/v0/signin", {
// TODO: save token login: formData.email,
init({ message: "Вы успешно вошли!", color: 'success' }) password: formData.password,
push({ name: 'dashboard' })
}) })
.catch(error => { }); .then((response) => {
// TODO: save token
init({ message: "Вы успешно вошли!", color: "success" });
push({ name: "dashboard" });
})
.catch((error) => {});
} }
} };
</script> </script>

View File

@ -2,8 +2,9 @@
<VaForm ref="passwordForm" @submit.prevent="submit"> <VaForm ref="passwordForm" @submit.prevent="submit">
<h1 class="font-semibold text-4xl mb-4">Забыли свой пароль?</h1> <h1 class="font-semibold text-4xl mb-4">Забыли свой пароль?</h1>
<p class="text-base mb-4 leading-5"> <p class="text-base mb-4 leading-5">
Если вы забыли свой пароль, не волнуйтесь. Просто введите свой адрес электронной почты ниже, и мы отправим вам электронное письмо Если вы забыли свой пароль, не волнуйтесь. Просто введите свой адрес
с временным паролем. электронной почты ниже, и мы отправим вам электронное письмо с временным
паролем.
</p> </p>
<VaInput <VaInput
v-model="email" v-model="email"
@ -13,22 +14,28 @@
type="email" type="email"
/> />
<VaButton class="w-full mb-2" @click="submit">Отправить пароль</VaButton> <VaButton class="w-full mb-2" @click="submit">Отправить пароль</VaButton>
<VaButton :to="{ name: 'login' }" class="w-full" preset="secondary" @click="submit">Вернуться</VaButton> <VaButton
:to="{ name: 'login' }"
class="w-full"
preset="secondary"
@click="submit"
>Вернуться</VaButton
>
</VaForm> </VaForm>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from "vue";
import { useForm } from 'vuestic-ui' import { useForm } from "vuestic-ui";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
const email = ref('') const email = ref("");
const form = useForm('passwordForm') const form = useForm("passwordForm");
const router = useRouter() const router = useRouter();
const submit = () => { const submit = () => {
if (form.validate()) { if (form.validate()) {
router.push({ name: 'recover-password-email' }) router.push({ name: "recover-password-email" });
} }
} };
</script> </script>

View File

@ -3,29 +3,65 @@
<h1 class="font-semibold text-4xl mb-4">Регистрация</h1> <h1 class="font-semibold text-4xl mb-4">Регистрация</h1>
<p class="text-base mb-4 leading-5"> <p class="text-base mb-4 leading-5">
Уже есть аккаунт? Уже есть аккаунт?
<RouterLink :to="{ name: 'login' }" class="font-semibold text-primary">Логин</RouterLink> <RouterLink :to="{ name: 'login' }" class="font-semibold text-primary"
>Логин</RouterLink
>
</p> </p>
<VaInput v-model="formData.email" <VaInput
:rules="[(v) => !!v || 'Email обязателен', (v) => /.+@.+\..+/.test(v) || 'Email должен быть валидным']" v-model="formData.email"
class="mb-4" label="Email" type="email" /> :rules="[
(v) => !!v || 'Email обязателен',
(v) => /.+@.+\..+/.test(v) || 'Email должен быть валидным',
]"
class="mb-4"
label="Email"
type="email"
/>
<VaValue v-slot="isPasswordVisible" :default-value="false"> <VaValue v-slot="isPasswordVisible" :default-value="false">
<VaInput ref="password1" v-model="formData.password" :rules="passwordRules" <VaInput
:type="isPasswordVisible.value ? 'text' : 'password'" class="mb-4" label="Пароль" ref="password1"
v-model="formData.password"
:rules="passwordRules"
:type="isPasswordVisible.value ? 'text' : 'password'"
class="mb-4"
label="Пароль"
messages="Пароль должен быть длинее 4-х символов." messages="Пароль должен быть длинее 4-х символов."
@clickAppendInner.stop="isPasswordVisible.value = !isPasswordVisible.value"> @clickAppendInner.stop="
isPasswordVisible.value = !isPasswordVisible.value
"
>
<template #appendInner> <template #appendInner>
<VaIcon :name="isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'" class="cursor-pointer" <VaIcon
color="secondary" /> :name="
isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'
"
class="cursor-pointer"
color="secondary"
/>
</template> </template>
</VaInput> </VaInput>
<VaInput ref="password2" v-model="formData.repeatPassword" :rules="[ <VaInput
(v) => !!v || 'Поле повторите пароль обязательное', ref="password2"
(v) => v === formData.password || 'Пароли не совпадают', v-model="formData.repeatPassword"
]" :type="isPasswordVisible.value ? 'text' : 'password'" class="mb-4" label="Повторите пароль" :rules="[
@clickAppendInner.stop="isPasswordVisible.value = !isPasswordVisible.value"> (v) => !!v || 'Поле повторите пароль обязательное',
(v) => v === formData.password || 'Пароли не совпадают',
]"
:type="isPasswordVisible.value ? 'text' : 'password'"
class="mb-4"
label="Повторите пароль"
@clickAppendInner.stop="
isPasswordVisible.value = !isPasswordVisible.value
"
>
<template #appendInner> <template #appendInner>
<VaIcon :name="isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'" class="cursor-pointer" <VaIcon
color="secondary" /> :name="
isPasswordVisible.value ? 'mso-visibility_off' : 'mso-visibility'
"
class="cursor-pointer"
color="secondary"
/>
</template> </template>
</VaInput> </VaInput>
</VaValue> </VaValue>
@ -37,38 +73,41 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import axios from 'axios'; import axios from "axios";
import { reactive } from 'vue' import { reactive } from "vue";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
import { useForm, useToast } from 'vuestic-ui' import { useForm, useToast } from "vuestic-ui";
const { validate } = useForm('form') const { validate } = useForm("form");
const { push } = useRouter() const { push } = useRouter();
const { init } = useToast() const { init } = useToast();
const formData = reactive({ const formData = reactive({
email: '', email: "",
password: '', password: "",
repeatPassword: '', repeatPassword: "",
}) });
const submit = () => { const submit = () => {
if (validate()) { if (validate()) {
axios.post('http://localhost:3000/api/v0/signin', { "email": formData.email, "password": formData.password }) axios
.then(response => { .post("http://localhost:3000/api/v0/signin", {
email: formData.email,
password: formData.password,
})
.then((response) => {
// TODO: save token // TODO: save token
init({ init({
message: "Вы успешно вошли", message: "Вы успешно вошли",
color: 'success', color: "success",
}) });
push({ name: 'dashboard' }) push({ name: "dashboard" }).catch((error) => {});
.catch(error => { });
}); });
} }
} };
const passwordRules: ((v: string) => boolean | string)[] = [ const passwordRules: ((v: string) => boolean | string)[] = [
(v) => !!v || 'Поле пароль обязательное', (v) => !!v || "Поле пароль обязательное",
(v) => (v && v.length >= 5) || 'Пароль должен быть длинее 5-ти символов', (v) => (v && v.length >= 5) || "Пароль должен быть длинее 5-ти символов",
] ];
</script> </script>

View File

@ -15,11 +15,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MembeshipTier from './MembeshipTier.vue' import MembeshipTier from "./MembeshipTier.vue";
import PaymentInfo from './PaymentInfo.vue' import PaymentInfo from "./PaymentInfo.vue";
import { usePaymentCardsStore } from '../../stores/payment-cards' import { usePaymentCardsStore } from "../../stores/payment-cards";
import Invoices from './Invoices.vue' import Invoices from "./Invoices.vue";
const cardStore = usePaymentCardsStore() const cardStore = usePaymentCardsStore();
cardStore.load() cardStore.load();
</script> </script>

View File

@ -19,76 +19,93 @@
</template> </template>
</VaCardContent> </VaCardContent>
<VaCardActions vertical class="flex flex-wrap content-center mt-4"> <VaCardActions vertical class="flex flex-wrap content-center mt-4">
<VaButton v-if="numberOfInvoicesInVIew < maxNumberOfInvoices" preset="primary" @click="increaseNumberOfInvoices()" <VaButton
v-if="numberOfInvoicesInVIew < maxNumberOfInvoices"
preset="primary"
@click="increaseNumberOfInvoices()"
>Show more >Show more
</VaButton> </VaButton>
<VaButton v-else preset="primary" @click="numberOfInvoicesInVIew = minNumberOfInvoices">Show less </VaButton> <VaButton
v-else
preset="primary"
@click="numberOfInvoicesInVIew = minNumberOfInvoices"
>Show less
</VaButton>
</VaCardActions> </VaCardActions>
</VaCard> </VaCard>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from "vue";
import { useToast } from 'vuestic-ui' import { useToast } from "vuestic-ui";
import { useI18n } from 'vue-i18n' import { useI18n } from "vue-i18n";
const { init } = useToast() const { init } = useToast();
const { locale } = useI18n() const { locale } = useI18n();
const minNumberOfInvoices = 7 const minNumberOfInvoices = 7;
const maxNumberOfInvoices = 20 const maxNumberOfInvoices = 20;
const numberOfInvoicesInVIew = ref(minNumberOfInvoices) const numberOfInvoicesInVIew = ref(minNumberOfInvoices);
const increaseNumberOfInvoices = (step = 10) => { const increaseNumberOfInvoices = (step = 10) => {
numberOfInvoicesInVIew.value = Math.min(numberOfInvoicesInVIew.value + step, maxNumberOfInvoices) numberOfInvoicesInVIew.value = Math.min(
} numberOfInvoicesInVIew.value + step,
maxNumberOfInvoices,
);
};
function getRandomDateInBetween(start: string, end: string): Date { function getRandomDateInBetween(start: string, end: string): Date {
const startDate = Date.parse(start) const startDate = Date.parse(start);
const endDate = Date.parse(end) const endDate = Date.parse(end);
return new Date(Math.floor(Math.random() * (endDate - startDate + 1) + startDate)) return new Date(
Math.floor(Math.random() * (endDate - startDate + 1) + startDate),
);
} }
function getLanguageCode(): string { function getLanguageCode(): string {
const countryCodeToLanguageCodeMapping: Record<any, string> = { const countryCodeToLanguageCodeMapping: Record<any, string> = {
br: 'pt', br: "pt",
cn: 'zh-CN', cn: "zh-CN",
gb: 'en-GB', gb: "en-GB",
ir: 'fa', ir: "fa",
} };
return countryCodeToLanguageCodeMapping[locale.value] || 'en-GB' return countryCodeToLanguageCodeMapping[locale.value] || "en-GB";
} }
function getRandomDateString(): string { function getRandomDateString(): string {
const startDate = '2020-01-01' const startDate = "2020-01-01";
const endDate = '2023-12-01' const endDate = "2023-12-01";
const dateFormatOptions: Intl.DateTimeFormatOptions = { const dateFormatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric', day: "numeric",
} };
return getRandomDateInBetween(startDate, endDate).toLocaleDateString(getLanguageCode(), dateFormatOptions) return getRandomDateInBetween(startDate, endDate).toLocaleDateString(
getLanguageCode(),
dateFormatOptions,
);
} }
const allItems = Array.from({ length: maxNumberOfInvoices }, (_, i) => ({ const allItems = Array.from({ length: maxNumberOfInvoices }, (_, i) => ({
id: i, id: i,
date: getRandomDateString(), date: getRandomDateString(),
amount: `$${(Math.random() * 100).toFixed(2)}`, amount: `$${(Math.random() * 100).toFixed(2)}`,
})) }));
const itemsInView = computed(() => { const itemsInView = computed(() => {
return allItems.slice(0, numberOfInvoicesInVIew.value) return allItems.slice(0, numberOfInvoicesInVIew.value);
}) });
const download = () => { const download = () => {
init({ init({
message: "Request received. We'll email your invoice once we've completed data collection.", message:
color: 'success', "Request received. We'll email your invoice once we've completed data collection.",
}) color: "success",
} });
};
</script> </script>

View File

@ -9,7 +9,12 @@
> >
<div class="flex items-center md:w-48"> <div class="flex items-center md:w-48">
<div class="font-bold">{{ plan.name }}</div> <div class="font-bold">{{ plan.name }}</div>
<VaBadge v-if="plan.type === 'current'" class="ml-2" color="success" text="Selected" /> <VaBadge
v-if="plan.type === 'current'"
class="ml-2"
color="success"
text="Selected"
/>
</div> </div>
<div class="md:w-48"> <div class="md:w-48">
<p class="mb-1">{{ plan.padletsTotal }} padlets</p> <p class="mb-1">{{ plan.padletsTotal }} padlets</p>
@ -24,9 +29,17 @@
</div> </div>
</div> </div>
<div class="md:w-48 flex justify-end"> <div class="md:w-48 flex justify-end">
<div v-if="plan.type === 'current'" class="font-bold">{{ plan.padletsUsed }} padlets used</div> <div v-if="plan.type === 'current'" class="font-bold">
<VaButton v-else-if="plan.type === 'upgrade'" @click="switchPlan(plan.id)">Upgrade</VaButton> {{ plan.padletsUsed }} padlets used
<VaButton v-else preset="primary" @click="switchPlan(plan.id)">Downgrade</VaButton> </div>
<VaButton
v-else-if="plan.type === 'upgrade'"
@click="switchPlan(plan.id)"
>Upgrade</VaButton
>
<VaButton v-else preset="primary" @click="switchPlan(plan.id)"
>Downgrade</VaButton
>
</div> </div>
</div> </div>
@ -37,69 +50,69 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vuestic-ui' import { useToast } from "vuestic-ui";
import { reactive } from 'vue' import { reactive } from "vue";
const { init } = useToast() const { init } = useToast();
type MembershipTier = { type MembershipTier = {
id: string id: string;
name: string name: string;
type: 'upgrade' | 'downgrade' | 'current' type: "upgrade" | "downgrade" | "current";
padletsUsed: number padletsUsed: number;
padletsTotal: string padletsTotal: string;
priceMonth?: string priceMonth?: string;
priceYear?: string priceYear?: string;
uploadLimit: string uploadLimit: string;
} };
const plans = reactive<MembershipTier[]>([ const plans = reactive<MembershipTier[]>([
{ {
id: '1', id: "1",
name: 'Platinum', name: "Platinum",
type: 'upgrade', type: "upgrade",
padletsUsed: 0, padletsUsed: 0,
padletsTotal: 'Unlimited', padletsTotal: "Unlimited",
priceMonth: '$9.99', priceMonth: "$9.99",
priceYear: '$99.99', priceYear: "$99.99",
uploadLimit: '500MB', uploadLimit: "500MB",
}, },
{ {
id: '2', id: "2",
name: 'Gold', name: "Gold",
type: 'current', type: "current",
padletsUsed: 19, padletsUsed: 19,
padletsTotal: '20', padletsTotal: "20",
priceMonth: '$6.99', priceMonth: "$6.99",
priceYear: '$69.99', priceYear: "$69.99",
uploadLimit: '100MB', uploadLimit: "100MB",
}, },
{ {
id: '3', id: "3",
name: 'Neon', name: "Neon",
type: 'downgrade', type: "downgrade",
padletsUsed: 0, padletsUsed: 0,
padletsTotal: '3', padletsTotal: "3",
priceMonth: undefined, priceMonth: undefined,
priceYear: undefined, priceYear: undefined,
uploadLimit: '20MB', uploadLimit: "20MB",
}, },
]) ]);
const switchPlan = (planId: string) => { const switchPlan = (planId: string) => {
plans.forEach((item, index) => { plans.forEach((item, index) => {
if (item.id === planId) { if (item.id === planId) {
// Set the selected plan to 'current' // Set the selected plan to 'current'
item.type = 'current' item.type = "current";
} else { } else {
// Determine if other plans are an 'upgrade' or 'downgrade' // Determine if other plans are an 'upgrade' or 'downgrade'
const selectedIndex = plans.findIndex((plan) => plan.id === planId) const selectedIndex = plans.findIndex((plan) => plan.id === planId);
item.type = index < selectedIndex ? 'upgrade' : 'downgrade' item.type = index < selectedIndex ? "upgrade" : "downgrade";
} }
}) });
init({ init({
message: "You've successfully changed the membership tier", message: "You've successfully changed the membership tier",
color: 'success', color: "success",
}) });
} };
</script> </script>

View File

@ -10,16 +10,18 @@
<div class="md:w-48"> <div class="md:w-48">
<p class="mb-1">Payment plan</p> <p class="mb-1">Payment plan</p>
<p class="font-bold"> <p class="font-bold">
{{ paymentPlan.isYearly ? paymentPlan.priceYear : paymentPlan.priceMonth }}&nbsp;/{{ {{
paymentPlan.isYearly ? 'yearly' : 'monthly' paymentPlan.isYearly
}} ? paymentPlan.priceYear
: paymentPlan.priceMonth
}}&nbsp;/{{ paymentPlan.isYearly ? "yearly" : "monthly" }}
</p> </p>
</div> </div>
</div> </div>
<div class="md:w-48 flex flex-col justify-end items-end"> <div class="md:w-48 flex flex-col justify-end items-end">
<VaButton preset="primary" @click="togglePaymentPlanModal"> <VaButton preset="primary" @click="togglePaymentPlanModal">
Switch to {{ paymentPlan.isYearly ? 'monthly' : 'annual' }} Switch to {{ paymentPlan.isYearly ? "monthly" : "annual" }}
</VaButton> </VaButton>
<div v-if="!paymentPlan.isYearly" class="mt-2 text-regularSmall"> <div v-if="!paymentPlan.isYearly" class="mt-2 text-regularSmall">
@ -38,11 +40,16 @@
> >
<div class="md:w-48"> <div class="md:w-48">
<p class="mb-1">Payment method</p> <p class="mb-1">Payment method</p>
<p class="font-bold capitalize">{{ paymentCard.paymentSystem }} {{ paymentCard.cardNumberMasked }}</p> <p class="font-bold capitalize">
{{ paymentCard.paymentSystem }}
{{ paymentCard.cardNumberMasked }}
</p>
</div> </div>
</div> </div>
<div class="md:w-48 flex justify-end"> <div class="md:w-48 flex justify-end">
<VaButton :to="{ name: 'payment-methods' }" preset="primary">Payment preferences</VaButton> <VaButton :to="{ name: 'payment-methods' }" preset="primary"
>Payment preferences</VaButton
>
</div> </div>
</div> </div>
</template> </template>
@ -58,35 +65,36 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from "vue";
import { usePaymentCardsStore } from '../../stores/payment-cards' import { usePaymentCardsStore } from "../../stores/payment-cards";
import ChangeYourPaymentPlan from './modals/ChangeYourPaymentPlan.vue' import ChangeYourPaymentPlan from "./modals/ChangeYourPaymentPlan.vue";
const paymentPlan = ref({ const paymentPlan = ref({
id: '1', id: "1",
name: 'Gold', name: "Gold",
isYearly: false, isYearly: false,
type: 'current', type: "current",
padletsUsed: 19, padletsUsed: 19,
padletsTotal: '20', padletsTotal: "20",
priceMonth: '$6.99', priceMonth: "$6.99",
priceYear: '$69.99', priceYear: "$69.99",
switchToYearlySave: '16%', switchToYearlySave: "16%",
uploadLimit: '100MB', uploadLimit: "100MB",
}) });
const cardStore = usePaymentCardsStore() const cardStore = usePaymentCardsStore();
const isChangeYourPaymentPlanModalOpen = ref(false) const isChangeYourPaymentPlanModalOpen = ref(false);
const paymentCard = computed(() => cardStore.currentPaymentCard) const paymentCard = computed(() => cardStore.currentPaymentCard);
const togglePaymentPlanModal = () => { const togglePaymentPlanModal = () => {
isChangeYourPaymentPlanModalOpen.value = !isChangeYourPaymentPlanModalOpen.value isChangeYourPaymentPlanModalOpen.value =
} !isChangeYourPaymentPlanModalOpen.value;
};
const updatePaymentPlan = () => { const updatePaymentPlan = () => {
paymentPlan.value.isYearly = !paymentPlan.value.isYearly paymentPlan.value.isYearly = !paymentPlan.value.isYearly;
isChangeYourPaymentPlanModalOpen.value = false isChangeYourPaymentPlanModalOpen.value = false;
} };
</script> </script>

View File

@ -11,34 +11,45 @@
<div class="space-y-6"> <div class="space-y-6">
<h3> <h3>
Are you sure you want to switch to the Are you sure you want to switch to the
<span class="font-bold text-primary">{{ yearlyPlan ? 'monthly' : 'annual' }}</span> <span class="font-bold text-primary">{{
yearlyPlan ? "monthly" : "annual"
}}</span>
plan? plan?
</h3> </h3>
<div class="flex flex-col-reverse md:justify-end md:flex-row md:space-x-4"> <div
<VaButton preset="secondary" color="secondary" @click="emits('cancel')"> Cancel</VaButton> class="flex flex-col-reverse md:justify-end md:flex-row md:space-x-4"
<VaButton class="mb-4 md:mb-0" @click="confirm()"> Update Plan</VaButton> >
<VaButton preset="secondary" color="secondary" @click="emits('cancel')">
Cancel</VaButton
>
<VaButton class="mb-4 md:mb-0" @click="confirm()">
Update Plan</VaButton
>
</div> </div>
</div> </div>
</VaModal> </VaModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useToast } from 'vuestic-ui' import { useToast } from "vuestic-ui";
const { init } = useToast() const { init } = useToast();
defineProps({ defineProps({
yearlyPlan: { yearlyPlan: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}) });
const emits = defineEmits(['cancel', 'confirm']) const emits = defineEmits(["cancel", "confirm"]);
const confirm = () => { const confirm = () => {
init({ message: "You've successfully changed your payment plan", color: 'success' }) init({
emits('confirm') message: "You've successfully changed your payment plan",
} color: "success",
});
emits("confirm");
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -7,8 +7,8 @@
</template> </template>
<script setup> <script setup>
import Categories from './widgets/Categories.vue' import Categories from "./widgets/Categories.vue";
import Questions from './widgets/Questions.vue' import Questions from "./widgets/Questions.vue";
import RequestDemo from './widgets/RequestDemo.vue' import RequestDemo from "./widgets/RequestDemo.vue";
import Navigation from './widgets/Navigation.vue' import Navigation from "./widgets/Navigation.vue";
</script> </script>

View File

@ -4,35 +4,51 @@
<VaIcon color="secondary" name="mso-search" /> <VaIcon color="secondary" name="mso-search" />
</template> </template>
</VaInput> </VaInput>
<section v-if="filteredCategories.length" class="grid grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-5"> <section
v-if="filteredCategories.length"
class="grid grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-5"
>
<template v-for="category in filteredCategories" :key="category.id"> <template v-for="category in filteredCategories" :key="category.id">
<VaCard class="col-span-3 md:col-span-1 min-h-[146px]" href="#"> <VaCard class="col-span-3 md:col-span-1 min-h-[146px]" href="#">
<VaCardContent class="leading-5 text-sm"> <VaCardContent class="leading-5 text-sm">
<VaIcon :name="`mso-${category.icon}`" class="font-light mb-2" color="primary" size="2rem" /> <VaIcon
<h2 class="text-primary mb-2 text-primary text-lg leading-7 font-bold">{{ category.name }}</h2> :name="`mso-${category.icon}`"
class="font-light mb-2"
color="primary"
size="2rem"
/>
<h2
class="text-primary mb-2 text-primary text-lg leading-7 font-bold"
>
{{ category.name }}
</h2>
<p>{{ category.intro }}</p> <p>{{ category.intro }}</p>
</VaCardContent> </VaCardContent>
</VaCard> </VaCard>
</template> </template>
</section> </section>
<VaAlert v-else class="mb-4 leading-5" color="info" outline> <VaAlert v-else class="mb-4 leading-5" color="info" outline>
No matches found. Try refining your search or browse through the categories to find the help you need. No matches found. Try refining your search or browse through the categories
to find the help you need.
</VaAlert> </VaAlert>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import categories from '../data/popularCategories.json' import categories from "../data/popularCategories.json";
import { ref, computed } from 'vue' import { ref, computed } from "vue";
const searchValue = ref('') const searchValue = ref("");
const filteredCategories = computed(() => { const filteredCategories = computed(() => {
const value = searchValue.value.trim().toLowerCase() const value = searchValue.value.trim().toLowerCase();
if (value.length === 0) { if (value.length === 0) {
return categories return categories;
} }
return categories.filter((category) => { return categories.filter((category) => {
return category.intro.toLowerCase().includes(value) || category.name.toLowerCase().includes(value) return (
}) category.intro.toLowerCase().includes(value) ||
}) category.name.toLowerCase().includes(value)
);
});
});
</script> </script>

View File

@ -4,7 +4,11 @@
<div v-for="section in navSections" :key="section" class="mb-6 md:mb-0"> <div v-for="section in navSections" :key="section" class="mb-6 md:mb-0">
<h3 class="h5 mb-4">{{ section }}</h3> <h3 class="h5 mb-4">{{ section }}</h3>
<ul class="leading-5"> <ul class="leading-5">
<li v-for="item in navigation[section]" :key="`${section}-${item.name}`" class="mb-4"> <li
v-for="item in navigation[section]"
:key="`${section}-${item.name}`"
class="mb-4"
>
<a class="va-link" href="#">{{ item.name }}</a> <a class="va-link" href="#">{{ item.name }}</a>
</li> </li>
</ul> </ul>
@ -14,10 +18,10 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { computed } from "vue";
import navigation from '../data/navigationLinks.json' import navigation from "../data/navigationLinks.json";
const navSections = computed(() => { const navSections = computed(() => {
return Object.keys(navigation) return Object.keys(navigation);
}) });
</script> </script>

View File

@ -2,22 +2,31 @@
<VaCard class="mb-4"> <VaCard class="mb-4">
<VaCardContent> <VaCardContent>
<h2 class="va-h5">Popular questions</h2> <h2 class="va-h5">Popular questions</h2>
<VaAccordion v-model="accordionState" :style="{ '--va-collapse-padding': '1rem 0' }" class="mb-1"> <VaAccordion
v-model="accordionState"
:style="{ '--va-collapse-padding': '1rem 0' }"
class="mb-1"
>
<VaCollapse header="How do I reload a page?"> <VaCollapse header="How do I reload a page?">
<article class="max-w-3xl leading-5"> <article class="max-w-3xl leading-5">
<p class="mb-2"> <p class="mb-2">
Get ready for some page-refreshing wisdom! We're about to dive into the magical world of reloading web Get ready for some page-refreshing wisdom! We're about to dive
pages. Here are some techniques: into the magical world of reloading web pages. Here are some
techniques:
</p> </p>
<ul class="list-disc list-inside leading-5 ml-4"> <ul class="list-disc list-inside leading-5 ml-4">
<li> <li>
Press the F5 key or Ctrl + R (Windows/Linux) or Command + R (Mac) to manually reload the current page. Press the F5 key or Ctrl + R (Windows/Linux) or Command + R
(Mac) to manually reload the current page.
</li> </li>
<li> <li>
Click the reload button in your browser. It usually looks like a circular arrow and is typically located Click the reload button in your browser. It usually looks like a
in the address bar. circular arrow and is typically located in the address bar.
</li>
<li>
Use JavaScript to reload the page programmatically:
location.reload();
</li> </li>
<li>Use JavaScript to reload the page programmatically: location.reload();</li>
</ul> </ul>
</article> </article>
</VaCollapse> </VaCollapse>
@ -25,16 +34,19 @@
<VaCollapse header="What is a Secure Key?"> <VaCollapse header="What is a Secure Key?">
<article class="max-w-3xl text-sm"> <article class="max-w-3xl text-sm">
<p class="mb-4"> <p class="mb-4">
A Secure Key is an extra layer of security that helps look after you and your money by generating a A Secure Key is an extra layer of security that helps look after
unique, single use security code every time you log on or authorise a payment. There are two types of you and your money by generating a unique, single use security
Secure Key - a Digital version that works as part of the Mobile Banking App - perfect if you have a code every time you log on or authorise a payment. There are two
smartphone or tablet. Or, you can use a Physical version that is a little key ring that looks like a mini types of Secure Key - a Digital version that works as part of the
calculator. Mobile Banking App - perfect if you have a smartphone or tablet.
Or, you can use a Physical version that is a little key ring that
looks like a mini calculator.
</p> </p>
<p class="mb-4"> <p class="mb-4">
We recommend setting up a Digital Secure Key. Its built into the first direct Banking App, and if you We recommend setting up a Digital Secure Key. Its built into the
have the right type of smartphone you can use your fingerprint or face recognition to seamlessly log on or first direct Banking App, and if you have the right type of
authorise payments. smartphone you can use your fingerprint or face recognition to
seamlessly log on or authorise payments.
</p> </p>
<a class="va-link font-semibold" href="#" <a class="va-link font-semibold" href="#"
>Find out more about Secure Keys >Find out more about Secure Keys
@ -46,68 +58,87 @@
<VaCollapse header="How do I report fraud?"> <VaCollapse header="How do I report fraud?">
<article class="max-w-3xl text-sm"> <article class="max-w-3xl text-sm">
<p class="mb-3"> <p class="mb-3">
Reporting fraud is a serious matter, and it's important to take appropriate steps to address and prevent Reporting fraud is a serious matter, and it's important to take
fraudulent activities. The specific process for reporting fraud may vary based on your location and the appropriate steps to address and prevent fraudulent activities.
nature of the fraud, but here's a general guide: The specific process for reporting fraud may vary based on your
location and the nature of the fraud, but here's a general guide:
</p> </p>
<ul class="list-disc list-inside leading-6 ml-4 mb-4"> <ul class="list-disc list-inside leading-6 ml-4 mb-4">
<li> <li>
<span class="font-semibold">Report to Relevant Authorities:</span> Depending on the nature of the fraud, <span class="font-semibold"
you may need to report to other relevant authorities, such as the Securities and Exchange Commission >Report to Relevant Authorities:</span
(SEC) for investment-related fraud or the Better Business Bureau (BBB) for scams and deceptive >
practices. Depending on the nature of the fraud, you may need to report to
other relevant authorities, such as the Securities and Exchange
Commission (SEC) for investment-related fraud or the Better
Business Bureau (BBB) for scams and deceptive practices.
</li> </li>
<li> <li>
<span class="font-semibold">Document All Details:</span> Make sure to document all the relevant details <span class="font-semibold">Document All Details:</span> Make
about the fraud, including dates, times, names of individuals involved (if known), financial sure to document all the relevant details about the fraud,
transactions, and any communication related to the fraud. including dates, times, names of individuals involved (if
known), financial transactions, and any communication related to
the fraud.
</li> </li>
<li> <li>
<span class="font-semibold">Report to Internet Crime Organizations:</span> If the fraud involves online <span class="font-semibold"
activities, consider reporting to organizations that deal with internet crimes, such as the Internet >Report to Internet Crime Organizations:</span
Crime Complaint Center (IC3). IC3 is a partnership between the FBI and the National White Collar Crime >
Center. If the fraud involves online activities, consider reporting to
organizations that deal with internet crimes, such as the
Internet Crime Complaint Center (IC3). IC3 is a partnership
between the FBI and the National White Collar Crime Center.
</li> </li>
</ul> </ul>
<p> <p>
Remember to act promptly and follow the specific reporting procedures relevant to your location and the Remember to act promptly and follow the specific reporting
type of fraud you've encountered. Taking swift action can help mitigate potential damage and prevent procedures relevant to your location and the type of fraud you've
further fraudulent activities. encountered. Taking swift action can help mitigate potential
damage and prevent further fraudulent activities.
</p> </p>
</article> </article>
</VaCollapse> </VaCollapse>
<VaCollapse :style="{ '--va-background-border': 'transparent' }" header="How to download statements?"> <VaCollapse
:style="{ '--va-background-border': 'transparent' }"
header="How to download statements?"
>
<article class="max-w-3xl text-sm"> <article class="max-w-3xl text-sm">
<p class="mb-3"> <p class="mb-3">
Downloading statements can vary depending on the type of statements you're referring to (e.g., bank Downloading statements can vary depending on the type of
statements, credit card statements, utility statements). Below are general steps to download statements statements you're referring to (e.g., bank statements, credit card
from various platforms: statements, utility statements). Below are general steps to
download statements from various platforms:
</p> </p>
<ul class="list-disc list-inside leading-6 ml-4 mb-4"> <ul class="list-disc list-inside leading-6 ml-4 mb-4">
<li> <li>
<span class="font-semibold">Ensure Secure Connection:</span> Always access and download statements from <span class="font-semibold">Ensure Secure Connection:</span>
a secure and trusted network to protect your sensitive financial information. Always access and download statements from a secure and trusted
network to protect your sensitive financial information.
</li> </li>
<li> <li>
<span class="font-semibold">Regularly Check Statements:</span> Review your statements regularly to <span class="font-semibold">Regularly Check Statements:</span>
verify transactions, detect errors, or identify any suspicious activity promptly. Review your statements regularly to verify transactions, detect
errors, or identify any suspicious activity promptly.
</li> </li>
<li> <li>
<span class="font-semibold">Verify Account Details:</span> Ensure that the statements you download <span class="font-semibold">Verify Account Details:</span>
correspond to the correct account, including account numbers and account names. Ensure that the statements you download correspond to the
correct account, including account numbers and account names.
</li> </li>
<li> <li>
<span class="font-semibold">Use Official Channels:</span> Download statements only from official and <span class="font-semibold">Use Official Channels:</span>
authorized platforms, such as your bank's official website or the secure online portals of service Download statements only from official and authorized platforms,
providers. such as your bank's official website or the secure online
portals of service providers.
</li> </li>
</ul> </ul>
<p> <p>
Always ensure that you follow the specific steps and instructions provided by the respective service Always ensure that you follow the specific steps and instructions
provider to download statements securely and accurately. If you encounter any difficulties or have provided by the respective service provider to download statements
specific questions, consider reaching out to the customer support of the respective institution or securely and accurately. If you encounter any difficulties or have
service. specific questions, consider reaching out to the customer support
of the respective institution or service.
</p> </p>
</article> </article>
</VaCollapse> </VaCollapse>
@ -117,7 +148,7 @@
</template> </template>
<script setup> <script setup>
import { reactive } from 'vue' import { reactive } from "vue";
const accordionState = reactive([false, true, false, false]) const accordionState = reactive([false, true, false, false]);
</script> </script>

View File

@ -3,7 +3,9 @@
<div> <div>
<VaCardContent> <VaCardContent>
<h2 class="va-h5">Got questions?</h2> <h2 class="va-h5">Got questions?</h2>
<p class="text-base leading-5">Request a free demo to have all your questions answered by an expert.</p> <p class="text-base leading-5">
Request a free demo to have all your questions answered by an expert.
</p>
</VaCardContent> </VaCardContent>
<VaCardActions align="left"> <VaCardActions align="left">
<VaButton @click="showModal = !showModal">Request a demo</VaButton> <VaButton @click="showModal = !showModal">Request a demo</VaButton>
@ -11,15 +13,25 @@
</div> </div>
<img alt="Send a message" src="../request-demo.svg" /> <img alt="Send a message" src="../request-demo.svg" />
</VaCard> </VaCard>
<VaModal v-model="showModal" :before-ok="submit" close-button ok-text="Request demo" size="small"> <VaModal
v-model="showModal"
:before-ok="submit"
close-button
ok-text="Request demo"
size="small"
>
<VaForm ref="form" @submit.prevent="submit"> <VaForm ref="form" @submit.prevent="submit">
<h3 class="va-h3">Request free demo</h3> <h3 class="va-h3">Request free demo</h3>
<p class="text-base mb-4 leading-5"> <p class="text-base mb-4 leading-5">
Claim your spot now and ignite innovation with our exceptional software solution! 🔥 Claim your spot now and ignite innovation with our exceptional software
solution! 🔥
</p> </p>
<VaInput <VaInput
v-model="email" v-model="email"
:rules="[(v) => !!v || 'Email field is required', (v) => /.+@.+\..+/.test(v) || 'Email should be valid']" :rules="[
(v) => !!v || 'Email field is required',
(v) => /.+@.+\..+/.test(v) || 'Email should be valid',
]"
class="mb-4" class="mb-4"
label="Email" label="Email"
type="email" type="email"
@ -29,24 +41,24 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from "vue";
import { useForm, useToast } from 'vuestic-ui' import { useForm, useToast } from "vuestic-ui";
const showModal = ref(false) const showModal = ref(false);
const email = ref('') const email = ref("");
const { validate } = useForm('form') const { validate } = useForm("form");
const { init } = useToast() const { init } = useToast();
const submit = async () => { const submit = async () => {
if (!validate()) { if (!validate()) {
return return;
} }
init({ init({
title: 'Demo Request Submitted!', title: "Demo Request Submitted!",
message: 'An expert will get in touch soon', message: "An expert will get in touch soon",
color: 'success', color: "success",
}) });
showModal.value = false showModal.value = false;
} };
</script> </script>

View File

@ -27,6 +27,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import PaymentCardList from './widgets/my-cards/PaymentCardList.vue' import PaymentCardList from "./widgets/my-cards/PaymentCardList.vue";
import BillingAddressList from './widgets/billing-address/BillingAddressList.vue' import BillingAddressList from "./widgets/billing-address/BillingAddressList.vue";
</script> </script>

View File

@ -1,10 +1,10 @@
import PaymentSystem from './PaymentSystem.vue' import PaymentSystem from "./PaymentSystem.vue";
export default { export default {
title: 'PaymentSystem', title: "PaymentSystem",
component: PaymentSystem, component: PaymentSystem,
tags: ['autodocs'], tags: ["autodocs"],
} };
export const Default = () => ({ export const Default = () => ({
components: { PaymentSystem }, components: { PaymentSystem },
@ -13,4 +13,4 @@ export const Default = () => ({
<br> <br>
<PaymentSystem type="mastercard"/> <PaymentSystem type="mastercard"/>
`, `,
}) });

View File

@ -1,14 +1,20 @@
<template> <template>
<div class="w-20 h-12 bg-backgroundElement rounded flex justify-center items-center align-bottom"> <div
<img v-if="props.type === 'mastercard'" alt="Mastercard Logo" src="./mastercard.png" /> class="w-20 h-12 bg-backgroundElement rounded flex justify-center items-center align-bottom"
>
<img
v-if="props.type === 'mastercard'"
alt="Mastercard Logo"
src="./mastercard.png"
/>
<img v-if="props.type === 'visa'" alt="Visa Logo" src="./visa.png" /> <img v-if="props.type === 'visa'" alt="Visa Logo" src="./visa.png" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PaymentSystemType } from '../types' import { PaymentSystemType } from "../types";
const props = defineProps<{ const props = defineProps<{
type: PaymentSystemType type: PaymentSystemType;
}>() }>();
</script> </script>

View File

@ -1,26 +1,26 @@
export enum PaymentSystemType { export enum PaymentSystemType {
Visa = 'visa', Visa = "visa",
MasterCard = 'mastercard', MasterCard = "mastercard",
} }
export const paymentSystemTypeOptions = Object.values(PaymentSystemType) export const paymentSystemTypeOptions = Object.values(PaymentSystemType);
export interface PaymentCard { export interface PaymentCard {
id: string id: string;
name: string name: string;
isPrimary: boolean // show Primary badge isPrimary: boolean; // show Primary badge
paymentSystem: PaymentSystemType // Enum or union type for various payment systems paymentSystem: PaymentSystemType; // Enum or union type for various payment systems
cardNumberMasked: string // ****1679 cardNumberMasked: string; // ****1679
expirationDate: string // 09/24 expirationDate: string; // 09/24
} }
export interface BillingAddress { export interface BillingAddress {
id: string id: string;
name: string name: string;
isPrimary: boolean // show Primary badge isPrimary: boolean; // show Primary badge
street: string street: string;
city: string city: string;
state: string state: string;
postalCode: string postalCode: string;
country: string country: string;
} }

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