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

1509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
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>
<style lang="scss">
@import 'scss/main.scss';
@import "scss/main.scss";
#app {
font-family: 'Inter', Avenir, Helvetica, Arial, sans-serif;
font-family: "Inter", Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -1,5 +1,11 @@
<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
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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,20 @@
<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 -->
<title>62EBC3B8-A55C-4B01-95A2-52FB8EDD4150</title>
<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>
<path

View File

@ -1,5 +1,9 @@
<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 />
<title>overview_icon_2</title>
<g id="Layer_2" data-name="Layer 2">

View File

@ -1,5 +1,9 @@
<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 />
<title>overview_icon_5</title>
<g id="Layer_2" data-name="Layer 2">

View File

@ -1,5 +1,11 @@
<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
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"

View File

@ -1,9 +1,18 @@
<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)">
<path d="M0 0h24v24H0z" />
<rect :fill="color" height="2" rx="1" width="20" x="2" y="3" />
<path :fill="color" d="M11 11h10a1 1 0 0 1 0 2H11a1 1 0 0 1 0-2zM1 11h5a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2z" />
<path
:fill="color"
d="M11 11h10a1 1 0 0 1 0 2H11a1 1 0 0 1 0-2zM1 11h5a1 1 0 0 1 0 2H1a1 1 0 0 1 0-2z"
/>
<rect :fill="color" height="2" rx="1" width="20" x="2" y="19" />
<path :stroke="color" d="M4 9l-3 3 3 3" stroke-width="2" />
</g>
@ -13,12 +22,12 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
color?: string;
}>(),
{
color: 'inherit',
color: "inherit",
},
)
);
</script>
<style lang="scss">

View File

@ -1,5 +1,11 @@
<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">
<path d="M0 0h24v24H0z" />
<rect :fill="color" height="2" rx="1" width="20" x="2" y="3" />
@ -15,12 +21,12 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
color?: string;
}>(),
{
color: 'inherit',
color: "inherit",
},
)
);
</script>
<style lang="scss">

View File

@ -1,5 +1,11 @@
<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
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"
@ -10,12 +16,12 @@
<script lang="ts" setup>
withDefaults(
defineProps<{
color?: string
color?: string;
}>(),
{
color: 'inherit',
color: "inherit",
},
)
);
</script>
<style lang="scss">

View File

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

View File

@ -1,11 +1,21 @@
<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 />
<title>overview_icon_3</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<polygon class="cls-1" points="37 26 37 7 11 7 11 18 3 18 3 46 11 46 15 46 30 46 37 46 45 46 45 26 37 26" />
<path class="cls-2" d="M40,19V0H8V11H0V49H47.5V19ZM3,46V14H8V46Zm34,0H11V3H37Zm7.5,0H40V22h4.5Z" />
<polygon
class="cls-1"
points="37 26 37 7 11 7 11 18 3 18 3 46 11 46 15 46 30 46 37 46 45 46 45 26 37 26"
/>
<path
class="cls-2"
d="M40,19V0H8V11H0V49H47.5V19ZM3,46V14H8V46Zm34,0H11V3H37Zm7.5,0H40V22h4.5Z"
/>
<circle class="cls-2" cx="24" cy="41" r="2.67" />
</g>
</g>

View File

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

View File

@ -1,9 +1,20 @@
<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 -->
<title>67046716-A590-445C-AC65-1EEF69089C00</title>
<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>
<path

View File

@ -1,5 +1,9 @@
<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 />
<title>overview_icon_1</title>
<g id="Layer_2" data-name="Layer 2">

View File

@ -1,5 +1,11 @@
<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>
<linearGradient :id="'ORIGINAL'" x1="0%" y1="50%" y2="50%">
<stop offset="0%" stop-color="#4AE387" />
@ -25,17 +31,17 @@
</template>
<script>
export default {
name: 'VaIconVuestic',
inject: ['contextConfig'],
name: "VaIconVuestic",
inject: ["contextConfig"],
computed: {
themeGradientId() {
return this.contextConfig.invertedColor ? 'CORPORATE' : 'ORIGINAL'
return this.contextConfig.invertedColor ? "CORPORATE" : "ORIGINAL";
},
textColor() {
return this.contextConfig.invertedColor ? '#6E85E8' : '#E4FF32'
return this.contextConfig.invertedColor ? "#6E85E8" : "#E4FF32";
},
},
}
};
</script>
<style lang="scss">

View File

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

View File

@ -22,21 +22,23 @@
{{ t('helpAndSupport') }}
</VaButton> -->
<!-- <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>
</template>
<script lang="ts" setup>
import ProfileDropdown from './dropdowns/ProfileDropdown.vue'
import NotificationDropdown from './dropdowns/NotificationDropdown.vue'
import GithubButton from './GitHubButton.vue'
import ProfileDropdown from "./dropdowns/ProfileDropdown.vue";
import NotificationDropdown from "./dropdowns/NotificationDropdown.vue";
import GithubButton from "./GitHubButton.vue";
defineProps({
isMobile: { type: Boolean, default: false },
})
});
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
import { useI18n } from "vue-i18n";
const { t } = useI18n();
</script>
<style lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,117 +1,121 @@
import { Chart, TooltipModel } from 'chart.js'
import { computePosition, flip, shift } from '@floating-ui/dom'
import { Chart, TooltipModel } from "chart.js";
import { computePosition, flip, shift } from "@floating-ui/dom";
const getOrCreateTooltip = (chart: Chart) => {
let tooltipEl = chart.canvas.parentNode?.querySelector('div')
let tooltipEl = chart.canvas.parentNode?.querySelector("div");
if (!tooltipEl) {
tooltipEl = document.createElement('div')
tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)'
tooltipEl.style.borderRadius = '3px'
tooltipEl.style.color = 'white'
tooltipEl.style.opacity = '1'
tooltipEl.style.pointerEvents = 'none'
tooltipEl.style.position = 'absolute'
tooltipEl = document.createElement("div");
tooltipEl.style.background = "rgba(0, 0, 0, 0.7)";
tooltipEl.style.borderRadius = "3px";
tooltipEl.style.color = "white";
tooltipEl.style.opacity = "1";
tooltipEl.style.pointerEvents = "none";
tooltipEl.style.position = "absolute";
// tooltipEl.style.transform = 'translate(-50%, 0)'
tooltipEl.style.left = '0'
tooltipEl.style.top = '0'
tooltipEl.style.transition = 'all .1s ease'
tooltipEl.style.height = 'min-content'
tooltipEl.style.maxWidth = '200px'
tooltipEl.style.zIndex = '9999'
tooltipEl.style.left = "0";
tooltipEl.style.top = "0";
tooltipEl.style.transition = "all .1s ease";
tooltipEl.style.height = "min-content";
tooltipEl.style.maxWidth = "200px";
tooltipEl.style.zIndex = "9999";
const table = document.createElement('table')
table.style.margin = '0px'
const table = document.createElement("table");
table.style.margin = "0px";
tooltipEl.appendChild(table)
chart.canvas.parentNode?.appendChild(tooltipEl)
tooltipEl.appendChild(table);
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
const { chart, tooltip } = context
const tooltipEl = getOrCreateTooltip(chart)
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = '0'
return
tooltipEl.style.opacity = "0";
return;
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || []
const bodyLines = tooltip.body.map((b) => b.lines)
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map((b) => b.lines);
const tableHead = document.createElement('thead')
const tableHead = document.createElement("thead");
titleLines.forEach((title) => {
const tr = document.createElement('tr')
tr.style.borderWidth = '0'
const tr = document.createElement("tr");
tr.style.borderWidth = "0";
const th = document.createElement('th')
th.style.borderWidth = '0'
const text = document.createTextNode(title)
const th = document.createElement("th");
th.style.borderWidth = "0";
const text = document.createTextNode(title);
th.appendChild(text)
tr.appendChild(th)
tableHead.appendChild(tr)
})
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement('tbody')
const tableBody = document.createElement("tbody");
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i]
const colors = tooltip.labelColors[i];
const span = document.createElement('span')
span.style.background = String(colors.backgroundColor)
span.style.borderColor = String(colors.borderColor)
span.style.borderWidth = '2px'
span.style.marginRight = '10px'
span.style.height = '10px'
span.style.width = '10px'
span.style.display = 'inline-block'
const span = document.createElement("span");
span.style.background = String(colors.backgroundColor);
span.style.borderColor = String(colors.borderColor);
span.style.borderWidth = "2px";
span.style.marginRight = "10px";
span.style.height = "10px";
span.style.width = "10px";
span.style.display = "inline-block";
const tr = document.createElement('tr')
tr.style.backgroundColor = 'inherit'
tr.style.borderWidth = '0'
const tr = document.createElement("tr");
tr.style.backgroundColor = "inherit";
tr.style.borderWidth = "0";
const td = document.createElement('td')
td.style.borderWidth = '0'
const td = document.createElement("td");
td.style.borderWidth = "0";
const text = document.createTextNode(body as any)
const text = document.createTextNode(body as any);
td.appendChild(span)
td.appendChild(text)
tr.appendChild(td)
tableBody.appendChild(tr)
})
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector('table')
const tableRoot = tooltipEl.querySelector("table");
// Remove old children
while (tableRoot?.firstChild) {
tableRoot.firstChild.remove()
tableRoot.firstChild.remove();
}
// Add new children
tableRoot?.appendChild(tableHead)
tableRoot?.appendChild(tableBody)
tableRoot?.appendChild(tableHead);
tableRoot?.appendChild(tableBody);
}
// Display, position, and set styles for font
tooltipEl.style.opacity = '1'
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px'
tooltipEl.style.opacity = "1";
tooltipEl.style.padding =
tooltip.options.padding + "px " + tooltip.options.padding + "px";
computePosition(chart.canvas.parentNode! as HTMLElement, tooltipEl!, {
placement: 'top',
placement: "top",
middleware: [flip(), shift()],
}).then(({ x, y }) => {
Object.assign(tooltipEl!.style, {
left: `${x}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 = {
scales: {
@ -21,10 +21,10 @@ export const defaultConfig = {
},
plugins: {
legend: {
position: 'bottom',
position: "bottom",
labels: {
font: {
color: '#34495e',
color: "#34495e",
family: DEFAULT_FONT_FAMILY,
size: 14,
},
@ -41,23 +41,23 @@ export const defaultConfig = {
},
datasets: {
line: {
fill: 'origin',
fill: "origin",
tension: 0.3,
borderColor: 'transparent',
borderColor: "transparent",
},
bubble: {
borderColor: 'transparent',
borderColor: "transparent",
},
bar: {
borderColor: 'transparent',
borderColor: "transparent",
},
},
maintainAspectRatio: false,
animation: true,
}
};
export const doughnutConfig = {
cutout: '80%',
cutout: "80%",
scales: {
x: {
display: false,
@ -82,26 +82,38 @@ export const doughnutConfig = {
},
datasets: {
line: {
fill: 'origin',
fill: "origin",
tension: 0.3,
borderColor: 'transparent',
borderColor: "transparent",
},
bubble: {
borderColor: 'transparent',
borderColor: "transparent",
},
bar: {
borderColor: 'transparent',
borderColor: "transparent",
},
},
maintainAspectRatio: false,
animation: true,
}
};
export const chartTypesMap = {
pie: markRaw(defineAsyncComponent(() => import('./chart-types/PieChart.vue'))),
doughnut: markRaw(defineAsyncComponent(() => import('./chart-types/DoughnutChart.vue'))),
bubble: markRaw(defineAsyncComponent(() => import('./chart-types/BubbleChart.vue'))),
line: markRaw(defineAsyncComponent(() => import('./chart-types/LineChart.vue'))),
bar: markRaw(defineAsyncComponent(() => import('./chart-types/BarChart.vue'))),
'horizontal-bar': markRaw(defineAsyncComponent(() => import('./chart-types/HorizontalBarChart.vue'))),
}
pie: markRaw(
defineAsyncComponent(() => import("./chart-types/PieChart.vue")),
),
doughnut: markRaw(
defineAsyncComponent(() => import("./chart-types/DoughnutChart.vue")),
),
bubble: markRaw(
defineAsyncComponent(() => import("./chart-types/BubbleChart.vue")),
),
line: markRaw(
defineAsyncComponent(() => import("./chart-types/LineChart.vue")),
),
bar: markRaw(
defineAsyncComponent(() => import("./chart-types/BarChart.vue")),
),
"horizontal-bar": markRaw(
defineAsyncComponent(() => import("./chart-types/HorizontalBarChart.vue")),
),
};

View File

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

View File

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

View File

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

View File

@ -1,30 +1,30 @@
import { TBarChartData } from '../types'
import { TBarChartData } from "../types";
export const barChartData: TBarChartData = {
labels: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
datasets: [
{
label: 'Last year',
backgroundColor: 'primary',
label: "Last year",
backgroundColor: "primary",
data: [50, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11],
},
{
label: 'Current year',
backgroundColor: 'info',
label: "Current year",
backgroundColor: "info",
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 = {
datasets: [
{
label: 'USA',
backgroundColor: 'danger',
label: "USA",
backgroundColor: "danger",
data: [
{
x: 23,
@ -49,8 +49,8 @@ export const bubbleChartData: TBubbleChartData = {
],
},
{
label: 'Russia',
backgroundColor: 'primary',
label: "Russia",
backgroundColor: "primary",
data: [
{
x: 0,
@ -95,8 +95,8 @@ export const bubbleChartData: TBubbleChartData = {
],
},
{
label: 'Canada',
backgroundColor: 'warning',
label: "Canada",
backgroundColor: "warning",
data: [
{
x: 10,
@ -136,8 +136,8 @@ export const bubbleChartData: TBubbleChartData = {
],
},
{
label: 'Belarus',
backgroundColor: 'info',
label: "Belarus",
backgroundColor: "info",
data: [
{
x: 35,
@ -182,8 +182,8 @@ export const bubbleChartData: TBubbleChartData = {
],
},
{
label: 'Ukraine',
backgroundColor: 'success',
label: "Ukraine",
backgroundColor: "success",
data: [
{
x: 25,
@ -228,4 +228,4 @@ export const bubbleChartData: TBubbleChartData = {
],
},
],
}
};

View File

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

View File

@ -1,20 +1,28 @@
import { computed, ComputedRef } from 'vue'
import { useChartColors } from './useChartColors'
import { TChartData } from '../../types'
import { computed, ComputedRef } from "vue";
import { useChartColors } from "./useChartColors";
import { TChartData } from "../../types";
export function useChartData<T extends TChartData>(data: T, alfa?: number): ComputedRef<T> {
const datasetsColors = data.datasets.map((dataset) => dataset.backgroundColor as string)
export function useChartData<T extends TChartData>(
data: T,
alfa?: number,
): ComputedRef<T> {
const datasetsColors = data.datasets.map(
(dataset) => dataset.backgroundColor as string,
);
const datasetsThemesColors = datasetsColors.map(
(colors) => useChartColors(colors, alfa)[alfa ? 'generatedHSLAColors' : 'generatedColors'],
)
(colors) =>
useChartColors(colors, alfa)[
alfa ? "generatedHSLAColors" : "generatedColors"
],
);
return computed(() => {
const datasets = data.datasets.map((dataset, idx) => ({
...dataset,
backgroundColor: datasetsThemesColors[idx].value,
}))
}));
return { ...data, datasets } as T
})
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 expensesBackground = '#fff'
export const earningsBackground = '#ECF0F1'
export const profitBackground = "#154EC1";
export const expensesBackground = "#fff";
export const earningsBackground = "#ECF0F1";
export const doughnutChartData: TDoughnutChartData = {
labels: ['Profit', 'Expenses'],
labels: ["Profit", "Expenses"],
datasets: [
{
label: 'Yearly Breakdown',
label: "Yearly Breakdown",
backgroundColor: [profitBackground, earningsBackground],
data: [432, 167],
},
],
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,18 @@
import type { ChartData } from 'chart.js'
import type { ChartData } from "chart.js";
export type ColorThemes = {
[key: string]: string
}
[key: string]: string;
};
export type TLineChartData = ChartData<'line', any, any>
export type TBarChartData = ChartData<'bar', any, any>
export type TBubbleChartData = ChartData<'bubble', any, any>
export type TDoughnutChartData = ChartData<'doughnut', any, any>
export type TPieChartData = ChartData<'pie', any, any>
export type TLineChartData = ChartData<"line", any, any>;
export type TBarChartData = ChartData<"bar", any, any>;
export type TBubbleChartData = ChartData<"bubble", any, any>;
export type TDoughnutChartData = ChartData<"doughnut", any, any>;
export type TPieChartData = ChartData<"pie", any, any>;
export type TChartData = TLineChartData | TBarChartData | TBubbleChartData | TDoughnutChartData | TPieChartData
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,
})
});
const messages: { [P: string]: Record<string, string> } = {}
const messages: { [P: string]: Record<string, string> } = {};
Object.entries(fileNameToLocaleModuleDict)
.map(([fileName, localeModule]) => {
const fileNameParts = fileName.split('/')
const fileNameWithoutPath = fileNameParts[fileNameParts.length - 1]
const localeName = fileNameWithoutPath.split('.json')[0]
const fileNameParts = fileName.split("/");
const fileNameWithoutPath = fileNameParts[fileNameParts.length - 1];
const localeName = fileNameWithoutPath.split(".json")[0];
return [localeName, localeModule.default] as const
return [localeName, localeModule.default] as const;
})
.forEach((localeNameLocaleMessagesTuple) => {
messages[localeNameLocaleMessagesTuple[0]] = localeNameLocaleMessagesTuple[1]
})
messages[localeNameLocaleMessagesTuple[0]] =
localeNameLocaleMessagesTuple[1];
});
export default createI18n({
legacy: false,
locale: 'ru',
fallbackLocale: 'ru',
locale: "ru",
fallbackLocale: "ru",
messages,
})
});

View File

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

View File

@ -1,7 +1,12 @@
<template>
<VaLayout
: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"
>
<template #top>
@ -9,13 +14,25 @@
</template>
<template #left>
<AppSidebar :minimized="isSidebarMinimized" :animated="!isMobile" :mobile="isMobile" />
<AppSidebar
:minimized="isSidebarMinimized"
:animated="!isMobile"
:mobile="isMobile"
/>
</template>
<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">
<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>
<AppLayoutNavigation v-if="!isMobile" class="p-4" />
@ -29,57 +46,59 @@
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { onBeforeRouteUpdate } from 'vue-router'
import { useBreakpoint } from 'vuestic-ui'
import { onBeforeUnmount, onMounted, ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { onBeforeRouteUpdate } from "vue-router";
import { useBreakpoint } from "vuestic-ui";
import { useGlobalStore } from '../stores/global-store'
import { useGlobalStore } from "../stores/global-store";
import AppLayoutNavigation from '../components/app-layout-navigation/AppLayoutNavigation.vue'
import AppNavbar from '../components/navbar/AppNavbar.vue'
import AppSidebar from '../components/sidebar/AppSidebar.vue'
import AppLayoutNavigation from "../components/app-layout-navigation/AppLayoutNavigation.vue";
import AppNavbar from "../components/navbar/AppNavbar.vue";
import AppSidebar from "../components/sidebar/AppSidebar.vue";
const GlobalStore = useGlobalStore()
const GlobalStore = useGlobalStore();
const breakpoints = useBreakpoint()
const breakpoints = useBreakpoint();
const sidebarWidth = ref('16rem')
const sidebarMinimizedWidth = ref(undefined)
const sidebarWidth = ref("16rem");
const sidebarMinimizedWidth = ref(undefined);
const isMobile = ref(false)
const isTablet = ref(false)
const { isSidebarMinimized } = storeToRefs(GlobalStore)
const isMobile = ref(false);
const isTablet = ref(false);
const { isSidebarMinimized } = storeToRefs(GlobalStore);
const onResize = () => {
isSidebarMinimized.value = breakpoints.mdDown
isMobile.value = breakpoints.smDown
isTablet.value = breakpoints.mdDown
sidebarMinimizedWidth.value = isMobile.value ? '0' : '4.5rem'
sidebarWidth.value = isTablet.value ? '100%' : '16rem'
}
isSidebarMinimized.value = breakpoints.mdDown;
isMobile.value = breakpoints.smDown;
isTablet.value = breakpoints.mdDown;
sidebarMinimizedWidth.value = isMobile.value ? "0" : "4.5rem";
sidebarWidth.value = isTablet.value ? "100%" : "16rem";
};
onMounted(() => {
window.addEventListener('resize', onResize)
onResize()
})
window.addEventListener("resize", onResize);
onResize();
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
})
window.removeEventListener("resize", onResize);
});
onBeforeRouteUpdate(() => {
if (breakpoints.mdDown) {
// 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 = () => {
isSidebarMinimized.value = true
}
isSidebarMinimized.value = true;
};
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

@ -1,10 +1,12 @@
<script lang="ts" setup>
import VuesticLogo from '../components/VuesticLogo.vue'
import NotFoundImage from '../components/NotFoundImage.vue'
import VuesticLogo from "../components/VuesticLogo.vue";
import NotFoundImage from "../components/NotFoundImage.vue";
</script>
<template>
<div class="flex flex-col justify-between h-screen items-center bg-[var(--va-background-secondary)]">
<div
class="flex flex-col justify-between h-screen items-center bg-[var(--va-background-secondary)]"
>
<RouterLink to="/">
<VuesticLogo :gradient="false" class="my-8 h-5" />
</RouterLink>
@ -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>
<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>
<div class="flex flex-col sm:flex-row gap-4">
<VaButton to="/">Go to homepage</VaButton>
<VaButton href="https://github.com/epicmaxco/vuestic-admin/issues/new" preset="secondary" target="_blank"
<VaButton
href="https://github.com/epicmaxco/vuestic-admin/issues/new"
preset="secondary"
target="_blank"
>Create a GitHub issue
</VaButton>
</div>

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
<template>
<VaCard>
<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>
<VaCardContent>
<div class="p-1 bg-black rounded absolute right-4 top-4">
@ -16,22 +18,27 @@
</p>
</section>
<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>
</VaCardContent>
</VaCard>
</template>
<script setup lang="ts">
import { VaCard } from 'vuestic-ui'
import VaChart from '../../../../components/va-charts/VaChart.vue'
import { useChartData } from '../../../../data/charts/composables/useChartData'
import { lineChartData } from '../../../../data/charts/lineChartData'
import { ChartOptions } from 'chart.js'
import { VaCard } from "vuestic-ui";
import VaChart from "../../../../components/va-charts/VaChart.vue";
import { useChartData } from "../../../../data/charts/composables/useChartData";
import { lineChartData } from "../../../../data/charts/lineChartData";
import { ChartOptions } from "chart.js";
const chartData = useChartData(lineChartData)
const chartData = useChartData(lineChartData);
const options: ChartOptions<'line'> = {
const options: ChartOptions<"line"> = {
scales: {
x: {
display: false,
@ -51,7 +58,7 @@ const options: ChartOptions<'line'> = {
},
interaction: {
intersect: false,
mode: 'index',
mode: "index",
},
plugins: {
legend: {
@ -61,5 +68,5 @@ const options: ChartOptions<'line'> = {
enabled: true,
},
},
}
};
</script>

View File

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

View File

@ -1,13 +1,22 @@
<template>
<VaCard>
<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>
<VaCardContent class="flex flex-col gap-1">
<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>
<VaDataTable
@ -18,67 +27,72 @@
]"
:items="data"
>
<template #cell(revenue)="{ rowData }"> ${{ rowData[`revenue${selectedPeriod}`] }} </template>
<template #cell(revenue)="{ rowData }">
${{ rowData[`revenue${selectedPeriod}`] }}
</template>
</VaDataTable>
</VaCardContent>
</VaCard>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { downloadAsCSV } from '../../../../services/toCSV'
import { ref } from "vue";
import { downloadAsCSV } from "../../../../services/toCSV";
const selectedPeriod = ref('Today')
const periods = ['Today', 'Week', 'Month'].map((period) => ({ label: period, value: period }))
const selectedPeriod = ref("Today");
const periods = ["Today", "Week", "Month"].map((period) => ({
label: period,
value: period,
}));
const data = [
{
name: 'Japan',
revenueToday: '4,748,454',
revenueWeek: '30,000,000',
revenueMonth: '120,000,000',
name: "Japan",
revenueToday: "4,748,454",
revenueWeek: "30,000,000",
revenueMonth: "120,000,000",
},
{
name: 'United Kingdom',
revenueToday: '405,748',
revenueWeek: '2,500,000',
revenueMonth: '10,000,000',
name: "United Kingdom",
revenueToday: "405,748",
revenueWeek: "2,500,000",
revenueMonth: "10,000,000",
},
{
name: 'United States',
revenueToday: '308,536',
revenueWeek: '1,800,000',
revenueMonth: '8,000,000',
name: "United States",
revenueToday: "308,536",
revenueWeek: "1,800,000",
revenueMonth: "8,000,000",
},
{
name: 'China',
revenueToday: '250,963',
revenueWeek: '1,600,000',
revenueMonth: '7,000,000',
name: "China",
revenueToday: "250,963",
revenueWeek: "1,600,000",
revenueMonth: "7,000,000",
},
{
name: 'Canada',
revenueToday: '29,415',
revenueWeek: '180,000',
revenueMonth: '800,000',
name: "Canada",
revenueToday: "29,415",
revenueWeek: "180,000",
revenueMonth: "800,000",
},
{
name: 'Australia',
revenueToday: '15,000',
revenueWeek: '100,000',
revenueMonth: '500,000',
name: "Australia",
revenueToday: "15,000",
revenueWeek: "100,000",
revenueMonth: "500,000",
},
{
name: 'India',
revenueToday: '10,000',
revenueWeek: '50,000',
revenueMonth: '200,000',
name: "India",
revenueToday: "10,000",
revenueWeek: "50,000",
revenueMonth: "200,000",
},
]
];
const exportAsCSV = () => {
downloadAsCSV(data, 'region-revenue')
}
downloadAsCSV(data, "region-revenue");
};
</script>
<style lang="scss" scoped>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
<template>
<VaCard>
<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>
<VaCardContent class="flex flex-row gap-1">
<section class="w-1/2">
@ -13,11 +15,17 @@
</p>
<div class="my-4 gap-2 flex flex-col">
<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>
</div>
<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>
</div>
</div>
@ -36,27 +44,37 @@
</template>
<script setup lang="ts">
import { VaCard } from 'vuestic-ui'
import VaChart from '../../../../components/va-charts/VaChart.vue'
import { useChartData } from '../../../../data/charts/composables/useChartData'
import { doughnutChartData, profitBackground, 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'
import { VaCard } from "vuestic-ui";
import VaChart from "../../../../components/va-charts/VaChart.vue";
import { useChartData } from "../../../../data/charts/composables/useChartData";
import {
doughnutChartData,
profitBackground,
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,
plugins: {
...doughnutConfig.plugins,
tooltip: {
// Chart to small to show tooltips
enabled: false,
position: 'nearest',
position: "nearest",
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,35 +4,51 @@
<VaIcon color="secondary" name="mso-search" />
</template>
</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">
<VaCard class="col-span-3 md:col-span-1 min-h-[146px]" href="#">
<VaCardContent class="leading-5 text-sm">
<VaIcon :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>
<VaIcon
: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>
</VaCardContent>
</VaCard>
</template>
</section>
<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>
</template>
<script lang="ts" setup>
import categories from '../data/popularCategories.json'
import { ref, computed } from 'vue'
import categories from "../data/popularCategories.json";
import { ref, computed } from "vue";
const searchValue = ref('')
const searchValue = ref("");
const filteredCategories = computed(() => {
const value = searchValue.value.trim().toLowerCase()
const value = searchValue.value.trim().toLowerCase();
if (value.length === 0) {
return categories
return categories;
}
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>

View File

@ -4,7 +4,11 @@
<div v-for="section in navSections" :key="section" class="mb-6 md:mb-0">
<h3 class="h5 mb-4">{{ section }}</h3>
<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>
</li>
</ul>
@ -14,10 +18,10 @@
</template>
<script setup>
import { computed } from 'vue'
import navigation from '../data/navigationLinks.json'
import { computed } from "vue";
import navigation from "../data/navigationLinks.json";
const navSections = computed(() => {
return Object.keys(navigation)
})
return Object.keys(navigation);
});
</script>

View File

@ -2,22 +2,31 @@
<VaCard class="mb-4">
<VaCardContent>
<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?">
<article class="max-w-3xl leading-5">
<p class="mb-2">
Get ready for some page-refreshing wisdom! We're about to dive into the magical world of reloading web
pages. Here are some techniques:
Get ready for some page-refreshing wisdom! We're about to dive
into the magical world of reloading web pages. Here are some
techniques:
</p>
<ul class="list-disc list-inside leading-5 ml-4">
<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>
Click the reload button in your browser. It usually looks like a circular arrow and is typically located
in the address bar.
Click the reload button in your browser. It usually looks like a
circular arrow and is typically located in the address bar.
</li>
<li>
Use JavaScript to reload the page programmatically:
location.reload();
</li>
<li>Use JavaScript to reload the page programmatically: location.reload();</li>
</ul>
</article>
</VaCollapse>
@ -25,16 +34,19 @@
<VaCollapse header="What is a Secure Key?">
<article class="max-w-3xl text-sm">
<p class="mb-4">
A Secure Key is an extra layer of security that helps look after you and your money by generating a
unique, single use security code every time you log on or authorise a payment. There are two types of
Secure Key - a Digital version that works as part of the 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.
A Secure Key is an extra layer of security that helps look after
you and your money by generating a unique, single use security
code every time you log on or authorise a payment. There are two
types of Secure Key - a Digital version that works as part of the
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 class="mb-4">
We recommend setting up a Digital Secure Key. Its built into the first direct Banking App, and if you
have the right type of smartphone you can use your fingerprint or face recognition to seamlessly log on or
authorise payments.
We recommend setting up a Digital Secure Key. Its built into the
first direct Banking App, and if you have the right type of
smartphone you can use your fingerprint or face recognition to
seamlessly log on or authorise payments.
</p>
<a class="va-link font-semibold" href="#"
>Find out more about Secure Keys
@ -46,68 +58,87 @@
<VaCollapse header="How do I report fraud?">
<article class="max-w-3xl text-sm">
<p class="mb-3">
Reporting fraud is a serious matter, and it's important to take appropriate steps to address and prevent
fraudulent activities. The specific process for reporting fraud may vary based on your location and the
nature of the fraud, but here's a general guide:
Reporting fraud is a serious matter, and it's important to take
appropriate steps to address and prevent fraudulent activities.
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>
<ul class="list-disc list-inside leading-6 ml-4 mb-4">
<li>
<span class="font-semibold">Report to Relevant Authorities:</span> 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.
<span class="font-semibold"
>Report to Relevant Authorities:</span
>
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>
<span class="font-semibold">Document All Details:</span> Make sure to document all the relevant details
about the fraud, including dates, times, names of individuals involved (if known), financial
transactions, and any communication related to the fraud.
<span class="font-semibold">Document All Details:</span> Make
sure to document all the relevant details about the fraud,
including dates, times, names of individuals involved (if
known), financial transactions, and any communication related to
the fraud.
</li>
<li>
<span class="font-semibold">Report to Internet Crime Organizations:</span> 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.
<span class="font-semibold"
>Report to Internet Crime Organizations:</span
>
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>
</ul>
<p>
Remember to act promptly and follow the specific reporting procedures relevant to your location and the
type of fraud you've encountered. Taking swift action can help mitigate potential damage and prevent
further fraudulent activities.
Remember to act promptly and follow the specific reporting
procedures relevant to your location and the type of fraud you've
encountered. Taking swift action can help mitigate potential
damage and prevent further fraudulent activities.
</p>
</article>
</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">
<p class="mb-3">
Downloading statements can vary depending on the type of statements you're referring to (e.g., bank
statements, credit card statements, utility statements). Below are general steps to download statements
from various platforms:
Downloading statements can vary depending on the type of
statements you're referring to (e.g., bank statements, credit card
statements, utility statements). Below are general steps to
download statements from various platforms:
</p>
<ul class="list-disc list-inside leading-6 ml-4 mb-4">
<li>
<span class="font-semibold">Ensure Secure Connection:</span> Always access and download statements from
a secure and trusted network to protect your sensitive financial information.
<span class="font-semibold">Ensure Secure Connection:</span>
Always access and download statements from a secure and trusted
network to protect your sensitive financial information.
</li>
<li>
<span class="font-semibold">Regularly Check Statements:</span> Review your statements regularly to
verify transactions, detect errors, or identify any suspicious activity promptly.
<span class="font-semibold">Regularly Check Statements:</span>
Review your statements regularly to verify transactions, detect
errors, or identify any suspicious activity promptly.
</li>
<li>
<span class="font-semibold">Verify Account Details:</span> Ensure that the statements you download
correspond to the correct account, including account numbers and account names.
<span class="font-semibold">Verify Account Details:</span>
Ensure that the statements you download correspond to the
correct account, including account numbers and account names.
</li>
<li>
<span class="font-semibold">Use Official Channels:</span> Download statements only from official and
authorized platforms, such as your bank's official website or the secure online portals of service
providers.
<span class="font-semibold">Use Official Channels:</span>
Download statements only from official and authorized platforms,
such as your bank's official website or the secure online
portals of service providers.
</li>
</ul>
<p>
Always ensure that you follow the specific steps and instructions provided by the respective service
provider to download statements securely and accurately. If you encounter any difficulties or have
specific questions, consider reaching out to the customer support of the respective institution or
service.
Always ensure that you follow the specific steps and instructions
provided by the respective service provider to download statements
securely and accurately. If you encounter any difficulties or have
specific questions, consider reaching out to the customer support
of the respective institution or service.
</p>
</article>
</VaCollapse>
@ -117,7 +148,7 @@
</template>
<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>

View File

@ -3,7 +3,9 @@
<div>
<VaCardContent>
<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>
<VaCardActions align="left">
<VaButton @click="showModal = !showModal">Request a demo</VaButton>
@ -11,15 +13,25 @@
</div>
<img alt="Send a message" src="../request-demo.svg" />
</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">
<h3 class="va-h3">Request free demo</h3>
<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>
<VaInput
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"
label="Email"
type="email"
@ -29,24 +41,24 @@
</template>
<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 email = ref('')
const { validate } = useForm('form')
const { init } = useToast()
const showModal = ref(false);
const email = ref("");
const { validate } = useForm("form");
const { init } = useToast();
const submit = async () => {
if (!validate()) {
return
return;
}
init({
title: 'Demo Request Submitted!',
message: 'An expert will get in touch soon',
color: 'success',
})
showModal.value = false
}
title: "Demo Request Submitted!",
message: "An expert will get in touch soon",
color: "success",
});
showModal.value = false;
};
</script>

View File

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

View File

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

View File

@ -1,14 +1,20 @@
<template>
<div 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" />
<div
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" />
</div>
</template>
<script setup lang="ts">
import { PaymentSystemType } from '../types'
import { PaymentSystemType } from "../types";
const props = defineProps<{
type: PaymentSystemType
}>()
type: PaymentSystemType;
}>();
</script>

View File

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

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