delete needless code
Gitea Actions Demo / build_and_push (push) Successful in 3m31s Details

This commit is contained in:
artem 2025-03-04 20:27:13 +03:00
parent ba9bc6420f
commit dcc16aa373
108 changed files with 0 additions and 50872 deletions

View File

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

View File

@ -1,237 +0,0 @@
<template>
<VaContent class="typography content">
<div class="grid grid-cols-12 gap-6">
<VaCard class="col-span-12">
<VaCardTitle>Primary text styles</VaCardTitle>
<VaCardContent>
<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.
</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.
</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.
</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.
</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.
</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.
</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.
</div>
</div>
<div class="mb-8">
<pre class="code-snippet">
&lt;p class=code-snippet>
This is a wonderful example.
&lt;a href=# onClick=>Read more&lt;/a>
&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.
</p>
</div>
</VaCardContent>
</VaCard>
<VaCard class="col-span-12">
<VaCardTitle>Secondary text styles</VaCardTitle>
<VaCardContent>
<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.
</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">
<li>Coffee</li>
<li>
Tea
<ol class="va-ordered">
<li>
Black tea
<ol class="va-ordered">
<li>Brooke Bond</li>
<li>Lipton</li>
</ol>
</li>
<li>
Green tea
<ol class="va-ordered">
<li>Greenfield</li>
<li>Tess</li>
</ol>
</li>
</ol>
</li>
<li>Milk</li>
</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.
</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">
<li>Coffee</li>
<li>
Tea
<ul class="va-unordered">
<li>
Black tea
<ul class="va-unordered">
<li>Brooke Bond</li>
<li>Lipton</li>
</ul>
</li>
<li>
Green tea
<ul class="va-unordered">
<li>Greenfield</li>
<li>Tess</li>
</ul>
</li>
</ul>
</li>
<li>Milk</li>
</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>
</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.
</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.
</p>
<p>
<i> Mister Lebowski</i>
</p>
</blockquote>
</div>
<div class="mb-8">
<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
>
</div>
</div>
<div class="mb-8">
<table class="va-table">
<thead>
<tr>
<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"
>
<td v-for="(itemData, colIndex) in rowData" :key="colIndex">
{{ itemData }}
</td>
</tr>
</tbody>
</table>
</div>
</VaCardContent>
</VaCard>
</div>
</VaContent>
</template>
<script lang="ts" setup>
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"],
]);
</script>
<style lang="scss" scoped>
.text--secondary {
color: var(--va-secondary);
}
</style>

View File

@ -1,57 +0,0 @@
<template>
<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";
defineOptions({
name: "VaChart",
});
const props = defineProps<{
data: ChartData<T>;
options?: ChartOptions<T>;
type: T;
}>();
const chartComponent = chartTypesMap[props.type];
const chartOptions = computed<ChartOptions<T>>(() => ({
...(defaultConfig as any),
...props.options,
}));
</script>
<style lang="scss">
.va-chart {
min-width: 100%;
min-height: 100%;
display: flex;
align-items: center;
justify-content: center;
> * {
height: 100%;
width: 100%;
}
canvas {
width: 100%;
height: auto;
min-height: 20px;
}
}
</style>

View File

@ -1,31 +0,0 @@
<template>
<Bar :data="data" :options="options" />
</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";
ChartJS.register(
Title,
Tooltip,
Legend,
BarElement,
LinearScale,
CategoryScale,
);
defineProps<{
data: any;
options?: ChartOptions<"bar">;
}>();
</script>

View File

@ -1,24 +0,0 @@
<template>
<Bubble :data="props.data" :options="options" />
</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";
ChartJS.register(Title, Tooltip, Legend, PointElement, LinearScale);
const props = defineProps<{
data: TBubbleChartData;
options?: ChartOptions<"bubble">;
}>();
</script>

View File

@ -1,24 +0,0 @@
<template>
<Doughnut :data="props.data" :options="options" />
</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";
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale);
const props = defineProps<{
data: TDoughnutChartData;
options?: ChartOptions<"doughnut">;
}>();
</script>

View File

@ -1,41 +0,0 @@
<template>
<Bar :data="props.data" :options="{ ...options, ...horizontalBarOptions }" />
</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";
ChartJS.register(
Title,
Tooltip,
Legend,
BarElement,
LinearScale,
CategoryScale,
);
const horizontalBarOptions = {
indexAxis: "y" as "x" | "y",
elements: {
bar: {
borderWidth: 1,
},
},
};
const props = defineProps<{
data: TBarChartData;
options?: ChartOptions<"bar">;
}>();
</script>

View File

@ -1,81 +0,0 @@
<template>
<Line ref="chart" :data="computedChartData" :options="options" />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { Line } from "vue-chartjs";
import type { ChartOptions } from "chart.js";
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
Filler,
} 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,
);
const chart = ref<typeof Line>();
const props = defineProps<{
data: TLineChartData;
options?: ChartOptions<"line">;
}>();
const ctx = computed(() => {
if (!chart.value) {
return null;
}
return chart.value.chart?.ctx ?? null;
});
const { setHSLAColor, getColor } = useColors();
const colors = ["primary", "success", "danger", "warning"];
const computedChartData = computed<TLineChartData>(() => {
if (!ctx.value) {
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 datasets = props.data.datasets.map((dataset, index) => {
const color = getColor(colors[index % colors.length]);
return {
...dataset,
fill: true,
backgroundColor: makeGradient(color),
borderColor: color,
pointRadius: 0,
borderWidth: 2,
};
});
return { ...props.data, datasets };
});
</script>

View File

@ -1,79 +0,0 @@
<template>
<canvas ref="canvas" style="max-width: 100%" />
</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";
ChartJS.register(
Title,
Tooltip,
Legend,
ArcElement,
CategoryScale,
ChoroplethController,
ProjectionScale,
ColorScale,
GeoFeature,
);
const canvas = ref<HTMLCanvasElement | null>(null);
function getColor(revenue: number) {
return revenue >= 0.9 ? "#63A6F8" : revenue > 0.4 ? "#8FC0FA" : "#EDF0F1";
}
const props = defineProps<{
options?: ChartOptions<"choropleth">;
data: ChartData<"choropleth", { feature: any; value: number }[], string>;
}>();
watchEffect(() => {
if (canvas.value === null) {
return;
}
new ChartJS(canvas.value.getContext("2d")!, {
type: "choropleth",
data: props.data,
options: {
plugins: {
legend: {
display: false,
},
},
scales: {
projection: {
axis: "x",
projection: "mercator",
projectionScale: 1.6,
},
color: {
axis: "x",
quantize: 5,
display: false,
interpolate: getColor,
},
},
animation: false,
},
});
});
</script>

View File

@ -1,24 +0,0 @@
<template>
<Pie :data="props.data" :options="options" />
</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";
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale);
const props = defineProps<{
data: TPieChartData;
options?: ChartOptions<"pie">;
}>();
</script>

View File

@ -1,121 +0,0 @@
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");
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.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";
const table = document.createElement("table");
table.style.margin = "0px";
tooltipEl.appendChild(table);
chart.canvas.parentNode?.appendChild(tooltipEl);
}
return tooltipEl;
};
export const externalTooltipHandler = (context: {
chart: Chart;
tooltip: TooltipModel<any>;
}) => {
// Tooltip Element
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = "0";
return;
}
// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map((b) => b.lines);
const tableHead = document.createElement("thead");
titleLines.forEach((title) => {
const tr = document.createElement("tr");
tr.style.borderWidth = "0";
const th = document.createElement("th");
th.style.borderWidth = "0";
const text = document.createTextNode(title);
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement("tbody");
bodyLines.forEach((body, 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 tr = document.createElement("tr");
tr.style.backgroundColor = "inherit";
tr.style.borderWidth = "0";
const td = document.createElement("td");
td.style.borderWidth = "0";
const text = document.createTextNode(body as any);
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector("table");
// Remove old children
while (tableRoot?.firstChild) {
tableRoot.firstChild.remove();
}
// Add new children
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";
computePosition(chart.canvas.parentNode! as HTMLElement, tooltipEl!, {
placement: "top",
middleware: [flip(), shift()],
}).then(({ x, y }) => {
Object.assign(tooltipEl!.style, {
left: `${x}px`,
top: `${y}px`,
});
});
};

View File

@ -1,119 +0,0 @@
import { defineAsyncComponent, markRaw } from "vue";
const DEFAULT_FONT_FAMILY = "'Inter', sans-serif";
export const defaultConfig = {
scales: {
x: {
ticks: {
font: {
family: DEFAULT_FONT_FAMILY,
},
},
},
y: {
ticks: {
font: {
family: DEFAULT_FONT_FAMILY,
},
},
},
},
plugins: {
legend: {
position: "bottom",
labels: {
font: {
color: "#34495e",
family: DEFAULT_FONT_FAMILY,
size: 14,
},
usePointStyle: true,
},
},
tooltip: {
bodyFont: {
size: 14,
family: DEFAULT_FONT_FAMILY,
},
boxPadding: 4,
},
},
datasets: {
line: {
fill: "origin",
tension: 0.3,
borderColor: "transparent",
},
bubble: {
borderColor: "transparent",
},
bar: {
borderColor: "transparent",
},
},
maintainAspectRatio: false,
animation: true,
};
export const doughnutConfig = {
cutout: "80%",
scales: {
x: {
display: false,
grid: {
display: false, // Disable X-axis grid lines ("net")
},
},
y: {
display: false,
grid: {
display: false, // Disable Y-axis grid lines ("net")
},
ticks: {
display: false, // Hide Y-axis values
},
},
},
plugins: {
legend: {
display: false,
},
},
datasets: {
line: {
fill: "origin",
tension: 0.3,
borderColor: "transparent",
},
bubble: {
borderColor: "transparent",
},
bar: {
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")),
),
};

View File

@ -1,191 +0,0 @@
<template>
<div ref="editorElement" class="va-medium-editor content">
<slot />
</div>
</template>
<script lang="ts" setup>
import { ref, Ref, onMounted, onBeforeUnmount } from "vue";
import MediumEditor from "medium-editor";
const props = withDefaults(
defineProps<{
editorOptions?: {
buttonLabels: string;
autoLink: boolean;
toolbar: {
buttons: string[];
};
};
}>(),
{
editorOptions: () => ({
buttonLabels: "fontawesome",
autoLink: true,
toolbar: {
buttons: ["bold", "italic", "underline", "anchor", "h1", "h2", "h3"],
},
}),
},
);
const emit = defineEmits<{
(e: "initialized", editor: typeof MediumEditor): void;
}>();
const editorElement: Ref<null | HTMLElement> = ref(null);
let editor: typeof MediumEditor | null = null;
onMounted(() => {
if (!editorElement.value) {
return;
}
editor = new MediumEditor(editorElement.value, props.editorOptions);
emit("initialized", editor);
});
onBeforeUnmount(() => {
if (editor) {
editor.destroy();
}
});
</script>
<style lang="scss">
@import "medium-editor/src/sass/medium-editor";
@import "variables";
$medium-editor-shadow: var(--va-box-shadow);
$medium-editor-background-color: var(--va-divider);
$medium-editor-text-color: var(--va-dark);
$medium-editor-active-background-color: var(--va-primary);
$medium-editor-active-text-color: var(--va-white);
.va-medium-editor {
margin-bottom: var(--va-medium-editor-margin-bottom);
min-width: var(--va-medium-editor-min-width);
max-width: var(--va-medium-editor-max-width);
&:focus {
outline: none;
}
&.content {
i {
font-style: italic;
}
}
}
// isn't a part of the .va-medium-editor, so can't be places inside it
.medium-editor-toolbar,
.medium-editor-toolbar-form,
.medium-editor-toolbar-actions,
.medium-editor-toolbar-anchor-preview {
box-shadow: $medium-editor-shadow;
background-color: $medium-editor-background-color;
border-radius: 1.5rem;
height: 44px;
line-height: 42px;
}
.medium-editor-toolbar-anchor-preview {
a {
padding: 0 2rem;
margin: 0;
line-height: 44px;
}
}
.medium-editor-toolbar {
box-shadow: $medium-editor-shadow;
.medium-editor-toolbar-actions {
overflow: hidden;
height: 44px;
}
.medium-editor-action {
margin: 0;
border: 0;
padding: 0.375rem 1rem;
height: 44px;
background-color: $medium-editor-background-color;
box-shadow: none;
border-radius: 0;
i {
color: $medium-editor-text-color;
}
&.medium-editor-button-active {
background-color: $medium-editor-active-background-color;
color: $medium-editor-active-text-color;
i {
color: $medium-editor-active-text-color;
}
}
}
& > .medium-editor-action:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
& > .medium-editor-action + .medium-editor-action {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
}
}
.medium-editor-toolbar-form {
color: $medium-editor-text-color;
overflow: hidden;
a {
color: $medium-editor-text-color;
transform: translateY(1px);
}
input {
margin-left: 4px !important;
transform: translateY(-2px);
border-radius: 13px;
}
.medium-editor-toolbar-close {
margin-right: 1rem;
}
}
.medium-toolbar-arrow-under::after {
border-color: $medium-editor-background-color transparent transparent
transparent;
top: 100%;
}
.medium-toolbar-arrow-over::before {
border-color: transparent transparent var(--va-primary) transparent;
}
.medium-editor-toolbar-anchor-preview {
// @include va-button($btn-padding-y-nrm, $btn-padding-x-nrm, $btn-font-size-nrm, $btn-line-height-nrm, $btn-border-radius-nrm);
.medium-editor-toolbar-anchor-preview {
margin: 0;
}
}
.medium-editor-anchor-preview {
max-width: 50%;
a {
color: $medium-editor-text-color;
text-decoration: none;
}
}
</style>

View File

@ -1,9 +0,0 @@
:root {
--va-medium-editor-margin-bottom: 2.25rem;
--va-medium-editor-min-width: 6rem;
--va-medium-editor-max-width: 600px;
/* Toolbar */
--va-medium-editor-toolbar-max-width: 90%;
--va-medium-editor-toolbar-box-shadow: none;
}

View File

@ -1,243 +0,0 @@
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",
"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",
"Korea, Democratic People's Republic of",
"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",
];

View File

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

View File

@ -1,231 +0,0 @@
import { TBubbleChartData } from "../types";
export const bubbleChartData: TBubbleChartData = {
datasets: [
{
label: "USA",
backgroundColor: "danger",
data: [
{
x: 23,
y: 25,
r: 15,
},
{
x: 40,
y: 10,
r: 10,
},
{
x: 30,
y: 22,
r: 30,
},
{
x: 7,
y: 43,
r: 40,
},
{
x: 23,
y: 27,
r: 12,
},
{
x: 20,
y: 15,
r: 11,
},
{
x: 7,
y: 10,
r: 35,
},
{
x: 10,
y: 20,
r: 40,
},
],
},
{
label: "Russia",
backgroundColor: "primary",
data: [
{
x: 0,
y: 30,
r: 15,
},
{
x: 20,
y: 20,
r: 20,
},
{
x: 15,
y: 15,
r: 50,
},
{
x: 31,
y: 46,
r: 30,
},
{
x: 20,
y: 14,
r: 25,
},
{
x: 34,
y: 17,
r: 30,
},
{
x: 44,
y: 44,
r: 10,
},
{
x: 39,
y: 25,
r: 35,
},
],
},
{
label: "Canada",
backgroundColor: "warning",
data: [
{
x: 10,
y: 30,
r: 45,
},
{
x: 10,
y: 50,
r: 20,
},
{
x: 5,
y: 5,
r: 30,
},
{
x: 40,
y: 30,
r: 20,
},
{
x: 33,
y: 15,
r: 18,
},
{
x: 40,
y: 20,
r: 40,
},
{
x: 33,
y: 33,
r: 40,
},
],
},
{
label: "Belarus",
backgroundColor: "info",
data: [
{
x: 35,
y: 30,
r: 45,
},
{
x: 25,
y: 40,
r: 35,
},
{
x: 5,
y: 5,
r: 30,
},
{
x: 5,
y: 20,
r: 40,
},
{
x: 10,
y: 40,
r: 15,
},
{
x: 3,
y: 10,
r: 10,
},
{
x: 15,
y: 40,
r: 40,
},
{
x: 7,
y: 15,
r: 10,
},
],
},
{
label: "Ukraine",
backgroundColor: "success",
data: [
{
x: 25,
y: 10,
r: 40,
},
{
x: 17,
y: 40,
r: 40,
},
{
x: 35,
y: 10,
r: 20,
},
{
x: 3,
y: 40,
r: 10,
},
{
x: 40,
y: 40,
r: 40,
},
{
x: 20,
y: 10,
r: 10,
},
{
x: 10,
y: 27,
r: 35,
},
{
x: 7,
y: 26,
r: 40,
},
],
},
],
};

View File

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

View File

@ -1,28 +0,0 @@
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,
);
const datasetsThemesColors = datasetsColors.map(
(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;
});
}

View File

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

View File

@ -1,30 +0,0 @@
import { TBarChartData } from "../types";
export const horizontalBarChartData: TBarChartData = {
labels: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
datasets: [
{
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",
data: [20, 30, 20, 40, 50, 40, 15, 60, 30, 20, 42, 53],
},
],
};

View File

@ -1,8 +0,0 @@
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 +0,0 @@
import { TLineChartData } from "../types";
export const lineChartData: TLineChartData = {
labels: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
datasets: [
{
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 +0,0 @@
import { TLineChartData } from "../types";
export const pieChartData: TLineChartData = {
labels: ["Africa", "Asia", "Europe"],
datasets: [
{
label: "Population (millions)",
backgroundColor: ["primary", "warning", "danger"],
data: [2478, 5267, 734],
},
],
};

View File

@ -1,49 +0,0 @@
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 type Revenues = {
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);
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 formatMoney = (amount: number, currency = "USD"): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
};

File diff suppressed because it is too large Load Diff

View File

@ -1,162 +0,0 @@
[
{
"id": 0,
"project_name": "Vuestic",
"project_owner": 13,
"team": [13, 5, 28, 14, 17, 28, 23, 11, 16, 19, 12, 28, 11],
"status": "in progress",
"creation_date": "20 Nov 2023"
},
{
"id": 1,
"project_name": "Mood board",
"project_owner": 28,
"team": [28, 10, 12, 28, 14, 27, 5, 4, 8, 23, 19, 18, 24, 11, 18, 12, 28],
"status": "important",
"creation_date": "16 Oct 2023"
},
{
"id": 2,
"project_name": "Jenkins",
"project_owner": 3,
"team": [3, 21, 7, 19, 4, 4, 7, 24],
"status": "important",
"creation_date": "1 Oct 2023"
},
{
"id": 3,
"project_name": "Springfield media",
"project_owner": 17,
"team": [17, 25, 21, 9, 18, 12, 15, 0, 7, 2, 7],
"status": "important",
"creation_date": "19 Sept 2023"
},
{
"id": 4,
"project_name": "Galileo",
"project_owner": 7,
"team": [7, 1, 28, 19, 3],
"status": "completed",
"creation_date": "23 Sept 2023"
},
{
"id": 5,
"project_name": "Website redesign",
"project_owner": 24,
"team": [24, 19, 1, 8, 9],
"status": "completed",
"creation_date": "9 Sept 2023"
},
{
"id": 6,
"project_name": "Toolset landing",
"project_owner": 15,
"team": [15, 16, 8, 6, 11, 21, 3, 20],
"status": "archived",
"creation_date": "17 Aug 2023"
},
{
"id": 7,
"project_name": "Complete product redesign",
"project_owner": 25,
"team": [25, 18, 24, 13, 5, 3, 4, 16, 25, 12, 18, 9, 22],
"status": "completed",
"creation_date": "11 Aug 2023"
},
{
"id": 8,
"project_name": "Design team project",
"project_owner": 17,
"team": [17, 6, 21, 17, 7, 6, 14, 13, 27, 7, 20],
"status": "archived",
"creation_date": "9 Aug 2023"
},
{
"id": 9,
"project_name": "Regular logistics",
"project_owner": 3,
"team": [3, 26, 8, 15, 21, 23, 18, 11, 22, 6, 20, 9],
"status": "archived",
"creation_date": "2 Aug 2023"
},
{
"id": 10,
"project_name": "Aurora Analytics",
"project_owner": 12,
"team": [12, 7, 18, 24, 13, 5],
"status": "in progress",
"creation_date": "10 Dec 2023"
},
{
"id": 11,
"project_name": "Quantum Leap",
"project_owner": 14,
"team": [14, 17, 3, 21, 11, 20],
"status": "planning",
"creation_date": "22 Nov 2023"
},
{
"id": 12,
"project_name": "Deep Dive Research",
"project_owner": 8,
"team": [8, 15, 9, 3, 27, 6],
"status": "important",
"creation_date": "15 Nov 2023"
},
{
"id": 13,
"project_name": "Sky High Architecture",
"project_owner": 21,
"team": [21, 2, 17, 18, 4],
"status": "completed",
"creation_date": "1 Nov 2023"
},
{
"id": 14,
"project_name": "Tech Horizon",
"project_owner": 9,
"team": [9, 19, 24, 1, 22],
"status": "in progress",
"creation_date": "28 Oct 2023"
},
{
"id": 15,
"project_name": "Edge of Innovation",
"project_owner": 16,
"team": [16, 11, 5, 14, 23],
"status": "planning",
"creation_date": "21 Oct 2023"
},
{
"id": 16,
"project_name": "Crypto Ventures",
"project_owner": 20,
"team": [20, 7, 15, 26, 12],
"status": "important",
"creation_date": "10 Oct 2023"
},
{
"id": 17,
"project_name": "Blockchain Basics",
"project_owner": 4,
"team": [4, 8, 3, 22, 27],
"status": "archived",
"creation_date": "5 Oct 2023"
},
{
"id": 18,
"project_name": "Virtual Reality Exploration",
"project_owner": 26,
"team": [26, 2, 14, 20, 9],
"status": "in progress",
"creation_date": "29 Sept 2023"
},
{
"id": 19,
"project_name": "AI in Daily Life",
"project_owner": 2,
"team": [2, 13, 24, 5, 11],
"status": "completed",
"creation_date": "22 Sept 2023"
}
]

View File

@ -1,119 +0,0 @@
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;
};
export type Sorting = {
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 === "team") {
return obj.team.map((user: any) => user.fullname).join(", ");
}
if (sortBy === "creation_date") {
return new Date(obj[sortBy]);
}
return obj[sortBy];
};
export const getProjects = async (options: Sorting & Pagination) => {
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][],
}));
if (options.sortBy && options.sortingOrder) {
projects.sort((a, b) => {
a = getSortItem(a, options.sortBy!);
b = getSortItem(b, options.sortBy!);
if (a < b) {
return options.sortingOrder === "asc" ? -1 : 1;
}
if (a > b) {
return options.sortingOrder === "asc" ? 1 : -1;
}
return 0;
});
}
const normalizedProjects = projects.slice(
(options.page - 1) * options.perPage,
options.page * options.perPage,
);
return {
data: normalizedProjects,
pagination: {
page: options.page,
perPage: options.perPage,
total: projectsDb.length,
},
};
};
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",
}),
};
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][],
};
};
export const updateProject = async (project: (typeof projectsDb)[number]) => {
await sleep(1000);
const index = projectsDb.findIndex((p) => p.id === project.id);
projectsDb[index] = project;
return project;
};
export const removeProject = async (project: (typeof projectsDb)[number]) => {
await sleep(1000);
const index = projectsDb.findIndex((p) => p.id === project.id);
projectsDb.splice(index, 1);
return project;
};

View File

@ -1,292 +0,0 @@
[
{
"id": 1,
"fullname": "Patrik Radkow",
"email": "magicpan@example.gg",
"username": "magicpan",
"role": "user",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 2,
"fullname": "Martin Hoff",
"email": "niceadmin@mail.com",
"username": "admin",
"role": "admin",
"avatar": "😍",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 3,
"fullname": "Liz Macintosh",
"email": "ebrown@gmail.com",
"username": "ebrown",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/1.jpg",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 4,
"fullname": "M2",
"email": "mrm@gmail.com",
"username": "mrm",
"role": "owner",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 5,
"fullname": "Kevin Smith",
"email": "kevin@gmail.com",
"username": "kevin13",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/2.jpg",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 6,
"fullname": "Martin Hoff",
"email": "martin@gmail.com",
"username": "martin3",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/3.jpg",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 7,
"fullname": "John Doe",
"email": "john@mail.com",
"username": "john",
"role": "user",
"avatar": "",
"active": true,
"notes": ""
},
{
"id": 8,
"fullname": "Maksim Nedo",
"email": "maksim@epic.com",
"username": "maksim",
"role": "admin",
"avatar": "https://avatars.githubusercontent.com/u/23530004?v=4",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 9,
"fullname": "Dmitry Kuzmenko",
"email": "dd@pp.com",
"username": "dd",
"role": "user",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 10,
"fullname": "Rayan Gosling",
"email": "rayan@u.ua",
"username": "rayan",
"role": "user",
"avatar": "",
"active": true,
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 11,
"active": true,
"fullname": "Laura Smith",
"email": "laura@example.gg",
"username": "bbb",
"role": "user",
"avatar": "",
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 12,
"active": true,
"fullname": "Ted Mosby",
"email": "tedmosby@mail.com",
"username": "gamer777",
"role": "user",
"avatar": "😭",
"notes": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum."
},
{
"id": 13,
"active": true,
"fullname": "Forrest Schmidt Jr.",
"email": "Willard23@gmail.com",
"username": "Clementine72",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/4.jpg",
"notes": "sed asperiores sed"
},
{
"id": 14,
"active": true,
"fullname": "Emilio Bruen",
"email": "Amya51@hotmail.com",
"username": "Madalyn_Brekke55",
"role": "user",
"avatar": "https://randomuser.me/api/portraits/men/5.jpg",
"notes": "architecto amet deleniti"
},
{
"id": 15,
"active": false,
"fullname": "Jenny Heathcote",
"email": "Granville_Lebsack38@yahoo.com",
"role": "user",
"username": "Vivienne98",
"avatar": "https://randomuser.me/api/portraits/men/6.jpg",
"notes": "provident ipsam recusandae"
},
{
"id": 16,
"active": true,
"fullname": "Sonya Cummerata III",
"email": "Toni2@yahoo.com",
"role": "user",
"username": "Norwood79",
"avatar": "https://randomuser.me/api/portraits/men/7.jpg",
"notes": "aut quaerat totam"
},
{
"id": 17,
"active": true,
"fullname": "Ruben Mitchell",
"email": "Lisette41@yahoo.com",
"role": "user",
"username": "Dariana_Schulist",
"avatar": "https://randomuser.me/api/portraits/men/8.jpg",
"notes": "minima harum ut"
},
{
"id": 18,
"active": true,
"fullname": "Blake Hudson I",
"email": "Israel88@hotmail.com",
"role": "user",
"username": "Crystal.Brakus29",
"avatar": "https://randomuser.me/api/portraits/men/9.jpg",
"notes": "sint culpa voluptatem"
},
{
"id": 19,
"active": true,
"fullname": "Alison Mueller",
"email": "Darien_Mayer@gmail.com",
"role": "user",
"username": "Cordie.Grant",
"avatar": "https://randomuser.me/api/portraits/men/10.jpg",
"notes": "officia autem aliquam"
},
{
"id": 20,
"active": false,
"fullname": "Miss Angelina Jenkins",
"email": "Cristal.Sauer@yahoo.com",
"role": "user",
"username": "Peggie.Runolfsdottir",
"avatar": "https://randomuser.me/api/portraits/men/11.jpg",
"notes": "rerum rerum rerum"
},
{
"id": 21,
"active": true,
"fullname": "Mack Boyle",
"email": "Shanny30@gmail.com",
"role": "user",
"username": "Phoebe67",
"avatar": "https://randomuser.me/api/portraits/men/12.jpg",
"notes": "voluptatibus et soluta"
},
{
"id": 22,
"active": true,
"fullname": "Raymond Simonis",
"email": "Tressie.Bruen45@gmail.com",
"role": "user",
"username": "Percy37",
"avatar": "https://randomuser.me/api/portraits/men/13.jpg",
"notes": "aut id molestiae"
},
{
"id": 23,
"active": true,
"fullname": "Janice Sporer",
"email": "Anastasia85@hotmail.com",
"role": "user",
"username": "Kali84",
"avatar": "https://randomuser.me/api/portraits/men/14.jpg",
"notes": "magnam eum aliquam"
},
{
"id": 24,
"active": true,
"fullname": "Francis Schowalter",
"email": "Tess56@gmail.com",
"role": "user",
"username": "Robyn.Kris",
"avatar": "https://randomuser.me/api/portraits/men/0.jpg",
"notes": "similique architecto in"
},
{
"id": 25,
"active": true,
"fullname": "Emilio Hoppe",
"email": "Bruce49@yahoo.com",
"role": "user",
"username": "Clemmie.Kutch",
"avatar": "https://randomuser.me/api/portraits/men/16.jpg",
"notes": "rerum quae dolorem"
},
{
"id": 26,
"active": true,
"fullname": "Janice Harber",
"email": "Jude38@hotmail.com",
"role": "user",
"username": "Neal70",
"avatar": "https://randomuser.me/api/portraits/men/17.jpg",
"notes": "iure dolor provident"
},
{
"id": 27,
"fullname": "Evelyn Morar",
"email": "Laverne.Roberts@hotmail.com",
"role": "user",
"username": "Neal_Thompson84",
"active": true,
"avatar": "https://randomuser.me/api/portraits/men/18.jpg",
"notes": "quae eos placeat"
},
{
"id": 28,
"fullname": "Antoinette Schneider",
"email": "Ambrose_Stehr25@gmail.com",
"role": "user",
"username": "Esta.Hickle",
"active": true,
"avatar": "https://randomuser.me/api/portraits/men/19.jpg",
"notes": "qui cumque unde"
},
{
"id": 29,
"fullname": "Daniel Ebony",
"email": "Nyah44@hotmail.com",
"role": "user",
"username": "Jade.Kuhlman90",
"active": true,
"avatar": "https://randomuser.me/api/portraits/men/20.jpg",
"notes": "exercitationem velit consectetur"
}
]

View File

@ -1,110 +0,0 @@
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[];
const getUserProjects = (userId: number | string) => {
return projectsDb
.filter((project) => project.team.includes(Number(userId)))
.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"],
}));
};
// Simulate API calls
export type Pagination = {
page: number;
perPage: number;
total: number;
};
export type Sorting = {
sortBy: keyof User | undefined;
sortingOrder: "asc" | "desc" | null;
};
export type Filters = {
isActive: boolean;
search: string;
};
const getSortItem = (obj: any, sortBy: string) => {
if (sortBy === "projects") {
return obj.projects.map((project: any) => project.project_name).join(", ");
}
return obj[sortBy];
};
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);
if (search) {
filteredUsers = filteredUsers.filter((user) =>
user.fullname.toLowerCase().includes(search.toLowerCase()),
);
}
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);
if (first > second) {
return sortingOrder === "asc" ? 1 : -1;
}
if (first < second) {
return sortingOrder === "asc" ? -1 : 1;
}
return 0;
});
}
const { page = 1, perPage = 10 } = filters || {};
return {
data: filteredUsers.slice((page - 1) * perPage, page * perPage),
pagination: {
page,
perPage,
total: filteredUsers.length,
},
};
};
export const addUser = async (user: 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;
};
export const removeUser = async (user: User) => {
await sleep(1000);
users.splice(
users.findIndex((u) => u.id === user.id),
1,
);
};

View File

@ -1,18 +0,0 @@
import type { ChartData } from "chart.js";
export type ColorThemes = {
[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 TChartData =
| TLineChartData
| TBarChartData
| TBubbleChartData
| TDoughnutChartData
| TPieChartData;

View File

@ -1,386 +0,0 @@
[
{
"id": "5d2c865e9a0bae79a6ef7cfa",
"firstName": "Ashley",
"lastName": "Mcdaniel",
"fullName": "Ashley Mcdaniel",
"email": "ashleymcdaniel@nebulean.com",
"country": "Cayman Islands",
"starred": true,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865ec73341e16e5f2251",
"firstName": "Sellers",
"lastName": "Todd",
"fullName": "Todd Sellers",
"email": "sellerstodd@nebulean.com",
"country": "Togo",
"starred": false,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "none",
"color": "primary",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e38800c5ce28f2f6b",
"firstName": "Sherman",
"lastName": "Knowles",
"fullName": "Sherman Knowles",
"email": "shermanknowles@nebulean.com",
"country": "Central African Republic",
"starred": true,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "none",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e957cd150b82e17a6",
"firstName": "Vasquez",
"lastName": "Lawson",
"fullName": "Vasquez Lawson",
"email": "vasquezlawson@nebulean.com",
"country": "Bouvet Island",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "down",
"color": "warning",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e9194dbe2faf99227",
"firstName": "April",
"lastName": "Sykes",
"fullName": "April Sykes",
"email": "aprilsykes@nebulean.com",
"country": "Saint Vincent and The Grenadines",
"starred": false,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "down",
"color": "primary",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e1ed74d83f6b26934",
"firstName": "Hodges",
"lastName": "Garrison",
"fullName": "Hodges Garrison",
"email": "hodgesgarrison@nebulean.com",
"country": "Zimbabwe",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "none",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e0ef31380880c3de5",
"firstName": "Therese",
"lastName": "Stokes",
"fullName": "Therese Stokes",
"email": "theresestokes@nebulean.com",
"country": "Mali",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e4b5ab4727e5c8b69",
"firstName": "Goodwin",
"lastName": "Brewer",
"fullName": "Goodwin Brewer",
"email": "goodwinbrewer@nebulean.com",
"country": "Iraq",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "none",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e4c4d675787cfe1c0",
"firstName": "Gomez",
"lastName": "Wise",
"fullName": "Gomez Wise",
"email": "gomezwise@nebulean.com",
"country": "Portugal",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "none",
"color": "primary",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e1017c3229017fc68",
"firstName": "Laverne",
"lastName": "Ayers",
"fullName": "Laverne Ayers",
"email": "laverneayers@nebulean.com",
"country": "Micronesia",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865ee66676fd7464f8b9",
"firstName": "Stewart",
"lastName": "Leon",
"fullName": "Stewart Leon",
"email": "stewartleon@nebulean.com",
"country": "Seychelles",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e644d8acbed1e0e97",
"firstName": "Lindsey",
"lastName": "Hopkins",
"fullName": "Lindsey Hopkins",
"email": "lindseyhopkins@nebulean.com",
"country": "Costa Rica",
"starred": false,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "up",
"color": "primary",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865ef2b732c74dc3d6a2",
"firstName": "Head",
"lastName": "Lloyd",
"fullName": "Head Lloyd",
"email": "headlloyd@nebulean.com",
"country": "Turkey",
"starred": true,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e4ee4f09e92ead2e7",
"firstName": "Fisher",
"lastName": "Bradford",
"fullName": "Fisher Bradford",
"email": "fisherbradford@nebulean.com",
"country": "Ethiopia",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e88d46a9e9049a549",
"firstName": "Aurora",
"lastName": "Bird",
"fullName": "Aurora Bird",
"email": "aurorabird@nebulean.com",
"country": "Burkina Faso",
"starred": false,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e44bf14ea96d6e752",
"firstName": "Bonita",
"lastName": "Shields",
"fullName": "Bonita Shields",
"email": "bonitashields@nebulean.com",
"country": "Cote D'Ivoire (Ivory Coast)",
"starred": true,
"hasReport": true,
"status": "warning",
"checked": false,
"trend": "down",
"color": "primary",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e2a8be26f6ac4369c",
"firstName": "Ethel",
"lastName": "Underwood",
"fullName": "Ethel Underwood",
"email": "ethelunderwood@nebulean.com",
"country": "Vanuatu",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e5e0aea40111c37f8",
"firstName": "Parker",
"lastName": "May",
"fullName": "Parker May",
"email": "parkermay@nebulean.com",
"country": "Pakistan",
"starred": true,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e7e0c05ecc2d0c186",
"firstName": "Hillary",
"lastName": "Waters",
"fullName": "Hillary Waters",
"email": "hillarywaters@nebulean.com",
"country": "Comoros",
"starred": true,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "down",
"color": "primary",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e80a72eeda016b169",
"firstName": "Raquel",
"lastName": "Ferrell",
"fullName": "Raquel Ferrell",
"email": "raquelferrell@nebulean.com",
"country": "China",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "down",
"color": "info",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865eafacadd378add679",
"firstName": "Pickett",
"lastName": "Page",
"fullName": "Pickett Page",
"email": "pickettpage@nebulean.com",
"country": "Bermuda",
"starred": true,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865e772b1a75bb0a07b5",
"firstName": "Alyson",
"lastName": "Bailey",
"fullName": "Alyson Bailey",
"email": "alysonbailey@nebulean.com",
"country": "United Arab Emirates",
"starred": false,
"hasReport": false,
"status": "warning",
"checked": false,
"trend": "up",
"color": "warning",
"graph": "M 5 20 C 10 5, 15 5, 30 30 S 20 20, 70 20",
"graphColor": "#4ae387"
},
{
"id": "5d2c865e137c19a76b56210c",
"firstName": "Farley",
"lastName": "Meyers",
"fullName": "Farley Meyers",
"email": "farleymeyers@nebulean.com",
"country": "Christmas Island",
"starred": false,
"hasReport": false,
"status": "info",
"checked": false,
"trend": "up",
"color": "warning",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
},
{
"id": "5d2c865eb0ba37a27aa9afe0",
"firstName": "Hinton",
"lastName": "Avery",
"fullName": "Hinton Avery",
"email": "hintonavery@nebulean.com",
"country": "Liechtenstein",
"starred": false,
"hasReport": true,
"status": "info",
"checked": false,
"trend": "up",
"color": "info",
"graph": "M 5 30 C 10 5, 30 10, 40 30 S 30 30, 90 40",
"graphColor": "#e34a4a"
}
]

View File

@ -1,32 +0,0 @@
<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";
</script>
<template>
<h1 class="page-title font-bold">Статистика</h1>
<section class="flex flex-col 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">
<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> -->
</section>
</template>

View File

@ -1,80 +0,0 @@
<template>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<DataSectionItem
v-for="metric in dashboardMetrics"
:key="metric.id"
:title="metric.title"
:value="metric.value"
:change-text="metric.changeText"
:up="metric.changeDirection === 'up'"
:icon-background="metric.iconBackground"
:icon-color="metric.iconColor"
>
<template #icon>
<VaIcon :name="metric.icon" size="large" />
</template>
</DataSectionItem>
</div>
</template>
<script lang="ts" setup>
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;
}
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: "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: "newProfit",
title: "New profit",
value: "27%",
icon: "mso-grade",
changeText: "4%",
changeDirection: "up",
iconBackground: getColor("warning"),
iconColor: getColor("on-warning"),
},
]);
</script>

View File

@ -1,50 +0,0 @@
<template>
<VaCard>
<VaCardContent>
<section>
<header class="flex items-center justify-between">
<div class="text-lg font-semibold grow">{{ value }}</div>
<div
class="p-1 rounded"
:style="{
backgroundColor: iconBackground,
color: iconColor,
}"
>
<slot name="icon"></slot>
</div>
</header>
<div>
<p class="mb-2">{{ title }}</p>
<p class="text-xs text-secondary">
<span :class="changeClass">
<template v-if="up"></template>
<template v-else></template>
{{ changeText }}
</span>
since last month
</p>
</div>
</section>
</VaCardContent>
</VaCard>
</template>
<script lang="ts" setup>
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;
}>();
const changeClass = computed(() => ({
"text-success": props.up,
"text-red-600": !props.up,
}));
</script>

View File

@ -1,72 +0,0 @@
<template>
<VaCard>
<VaCardTitle>
<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">
<VaIcon name="mso-attach_money" color="#fff" size="large" />
</div>
<section>
<div class="text-xl font-bold mb-2">$6,820</div>
<p class="text-xs text-success">
<VaIcon name="arrow_upward" />
25.36%
<span class="text-secondary"> last month</span>
</p>
</section>
<div class="w-full flex items-center">
<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";
const chartData = useChartData(lineChartData);
const options: ChartOptions<"line"> = {
scales: {
x: {
display: false,
grid: {
display: false, // Disable X-axis grid lines ("net")
},
},
y: {
display: false,
grid: {
display: false, // Disable Y-axis grid lines ("net")
},
ticks: {
display: false, // Hide Y-axis values
},
},
},
interaction: {
intersect: false,
mode: "index",
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: true,
},
},
};
</script>

View File

@ -1,83 +0,0 @@
<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";
const columns = defineVaDataTableColumns([
{ 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 { 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];
};
</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
>
</VaCardTitle>
<VaCardContent>
<div v-if="projects.length > 0">
<VaDataTable
v-model:sort-by="sorting.sortBy"
v-model:sorting-order="sorting.sortingOrder"
:items="projects"
:columns="columns"
:loading="isLoading"
>
<template #cell(project_name)="{ rowData }">
<div class="ellipsis max-w-[230px] lg:max-w-[450px]">
{{ rowData.project_name }}
</div>
</template>
<template #cell(project_owner)="{ rowData }">
<div class="flex items-center gap-2 ellipsis max-w-[230px]">
<UserAvatar :user="rowData.project_owner" size="small" />
{{ rowData.project_owner.fullname }}
</div>
</template>
<template #cell(team)="{ rowData: project }">
<VaAvatarGroup
size="small"
:options="
(project as Project).team.map((user) => ({
label: user.fullname,
src: user.avatar,
fallbackText: user.fullname[0],
color: avatarColor(user.fullname),
}))
"
:max="2"
/>
</template>
<template #cell(status)="{ rowData: project }">
<ProjectStatusBadge :status="project.status" />
</template>
</VaDataTable>
</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,106 +0,0 @@
<template>
<VaCard>
<VaCardTitle class="flex justify-between">
<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"
/>
<VaButton preset="primary" size="small" @click="exportAsCSV">
Export
</VaButton>
</div>
<VaDataTable
class="region-revenue-table"
:columns="[
{ key: 'name', label: 'Top Region' },
{ key: 'revenue', label: 'Revenue', align: 'right' },
]"
:items="data"
>
<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";
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: "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: "China",
revenueToday: "250,963",
revenueWeek: "1,600,000",
revenueMonth: "7,000,000",
},
{
name: "Canada",
revenueToday: "29,415",
revenueWeek: "180,000",
revenueMonth: "800,000",
},
{
name: "Australia",
revenueToday: "15,000",
revenueWeek: "100,000",
revenueMonth: "500,000",
},
{
name: "India",
revenueToday: "10,000",
revenueWeek: "50,000",
revenueMonth: "200,000",
},
];
const exportAsCSV = () => {
downloadAsCSV(data, "region-revenue");
};
</script>
<style lang="scss" scoped>
.region-revenue-table {
::v-deep(tbody) {
tr {
border-top: 1px solid var(--va-background-border);
}
}
}
</style>

View File

@ -1,86 +0,0 @@
<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>
</VaCardTitle>
<VaCardContent class="flex-1 flex overflow-hidden">
<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"
/>
</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";
const getRevenue = (countryName: string) => {
if (
[
"United States of America",
"Canada",
"United Kingdom",
"China",
"Japan",
].includes(countryName)
) {
return 10;
}
if (["Antarctica", "Greenland"].includes(countryName)) {
return 0;
}
return Math.random() * 10;
};
const geoJson = ref<typeof countriesGeoJSON | null>(null);
onMounted(async () => {
geoJson.value = (await import("../../../../data/geo.json")).default;
});
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),
})),
},
],
};
});
</script>
<style lang="scss" scoped>
.va-card--flex {
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,99 +0,0 @@
<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>
<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
>
</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"
>
<div>
<p class="text-xl font-semibold">{{ formatMoney(totalEarnings) }}</p>
<p class="whitespace-nowrap mt-2">Total earnings</p>
</div>
<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="text-secondary">Earnings this month</span>
</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="text-secondary">Expense this month</span>
</div>
<div class="mt-2 text-xl font-semibold">
{{ formatMoney(earningsForSelectedMonth.expenses) }}
</div>
</div>
</div>
</section>
<RevenueReportChart
class="w-2/3 md:w-3/5 lg:w-3/4 h-full min-h-72 sm:min-h-32 pt-4"
:revenues="revenues"
:months="months"
/>
</VaCardContent>
</VaCard>
</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 {
earningsColor,
expensesColor,
months,
generateRevenues,
getRevenuePerMonth,
formatMoney,
} from "../../../../data/charts/revenueChartData";
const revenues = generateRevenues(months);
const currentYear = new Date().getFullYear();
const monthsWithCurrentYear = months.map((month) => `${month} ${currentYear}`);
const selectedMonth = ref(monthsWithCurrentYear[0]);
const earningsForSelectedMonth = computed(() =>
getRevenuePerMonth(selectedMonth.value.split(" ")[0], revenues),
);
const totalEarnings = computed(() => {
return (
earningsForSelectedMonth.value.earning +
earningsForSelectedMonth.value.expenses
);
});
const exportAsCSV = () => {
downloadAsCSV(revenues, "revenue-report");
};
</script>

View File

@ -1,122 +0,0 @@
<template>
<div class="flex justify-center w-full h-full overflow-hidden relative">
<canvas ref="canvas" style="max-width: 100%"></canvas>
</div>
</template>
<script lang="ts" setup>
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";
const { revenues, months } = defineProps<{
months: string[];
revenues: Revenues[];
}>();
Chart.register(...registerables);
const BR_THICKNESS = 4;
Chart.register([
{
id: "background-color",
beforeDatasetDraw: function (chart) {
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;
// 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,
);
});
}
});
},
},
]);
const canvas = ref<HTMLCanvasElement | null>(null);
const doShowChart = ref(false);
onMounted(() => {
if (canvas.value) {
const ctx = canvas.value.getContext("2d");
if (ctx) {
new Chart(ctx, {
type: "bar",
data: {
labels: months,
datasets: [
{
// Show relative expenses ratio
data: revenues.map(
({ earning, expenses }) => (expenses / earning) * 100,
),
backgroundColor: expensesColor,
barThickness: BR_THICKNESS,
},
],
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
stacked: true,
grid: {
display: false,
},
border: {
width: 0,
},
},
y: {
display: false,
beginAtZero: true,
ticks: {
callback: function (value) {
return formatMoney(Number(value));
},
},
},
},
},
});
}
}
nextTick(() => {
doShowChart.value = true;
});
});
</script>
<style lang="scss" scoped>
canvas {
position: absolute;
height: 100%;
width: 100%;
}
</style>

View File

@ -1,68 +0,0 @@
<script setup lang="ts">
import VaTimelineItem from "../../../../components/va-timeline-item.vue";
</script>
<template>
<VaCard>
<VaCardTitle class="flex justify-between">
<h1 class="card-title text-secondary font-bold uppercase">Timeline</h1>
</VaCardTitle>
<VaCardContent>
<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
</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
</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
</VaTimelineItem>
<VaTimelineItem date="3d ago">
<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
</VaTimelineItem>
<VaTimelineItem date="Nov 14, 2023">
<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
</VaTimelineItem>
</tbody>
</table>
</VaCardContent>
</VaCard>
</template>

View File

@ -1,80 +0,0 @@
<template>
<VaCard>
<VaCardTitle class="pb-0!">
<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">
<div class="text-xl font-bold mb-2">$36,358</div>
<p class="text-xs text-success whitespace-nowrap">
<VaIcon name="arrow_outward" />
+2,5%
<span class="text-secondary"> last year</span>
</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="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="text-secondary">Profit</span>
</div>
</div>
</section>
<div class="w-1/2 flex items-center h-full flex-1 lg:pl-16 pl-2 -mr-1">
<VaChart
v-if="chartData"
:data="chartData"
class="chart chart--donut h-[90px] w-[90px]"
type="doughnut"
: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 {
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 options: ChartOptions<"doughnut"> = {
...doughnutConfig,
plugins: {
...doughnutConfig.plugins,
tooltip: {
// Chart to small to show tooltips
enabled: false,
position: "nearest",
external: externalTooltipHandler,
},
},
circumference:
360 *
(chartData.value.datasets[0].data.reduce(
(acc: number, d: number) => acc + d,
0,
) /
800),
};
</script>

View File

@ -1,25 +0,0 @@
<template>
<h1 class="h1">Billing information</h1>
<VaSkeletonGroup v-if="cardStore.loading">
<VaSkeleton class="mb-4" height="160px" variant="squared" />
<VaSkeleton class="mb-4" height="160px" variant="squared" />
<VaSkeleton height="360px" variant="squared" />
</VaSkeletonGroup>
<template v-else>
<MembeshipTier />
<PaymentInfo />
<Invoices />
</template>
</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";
const cardStore = usePaymentCardsStore();
cardStore.load();
</script>

View File

@ -1,111 +0,0 @@
<template>
<VaCard class="mb-6">
<VaCardContent>
<h2 class="page-sub-title">Invoices</h2>
<template v-for="(item, index) in itemsInView" :key="item.id">
<div class="flex items-center justify-between md:justify-items-stretch">
<div class="flex items-center w-48">
{{ item.date }}
</div>
<div class="w-20">
{{ item.amount }}
</div>
<div>
<VaButton preset="primary" @click="download">Download</VaButton>
</div>
</div>
<VaDivider v-if="index !== itemsInView.length - 1" />
</template>
</VaCardContent>
<VaCardActions vertical class="flex flex-wrap content-center mt-4">
<VaButton
v-if="numberOfInvoicesInVIew < maxNumberOfInvoices"
preset="primary"
@click="increaseNumberOfInvoices()"
>Show more
</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";
const { init } = useToast();
const { locale } = useI18n();
const minNumberOfInvoices = 7;
const maxNumberOfInvoices = 20;
const numberOfInvoicesInVIew = ref(minNumberOfInvoices);
const increaseNumberOfInvoices = (step = 10) => {
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);
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",
};
return countryCodeToLanguageCodeMapping[locale.value] || "en-GB";
}
function getRandomDateString(): string {
const startDate = "2020-01-01";
const endDate = "2023-12-01";
const dateFormatOptions: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
};
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);
});
const download = () => {
init({
message:
"Request received. We'll email your invoice once we've completed data collection.",
color: "success",
});
};
</script>

View File

@ -1,118 +0,0 @@
<template>
<VaCard class="mb-6">
<VaCardContent>
<h2 class="page-sub-title">Membership tier</h2>
<template v-for="(plan, index) in plans" :key="plan.id">
<div class="flex items-center justify-between md:justify-items-stretch">
<div
class="flex grow flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-1 justify-between items-start md:items-center"
>
<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"
/>
</div>
<div class="md:w-48">
<p class="mb-1">{{ plan.padletsTotal }} padlets</p>
<p>{{ plan.uploadLimit }}&nbsp;/upload</p>
</div>
<div class="md:w-48">
<template v-if="plan.priceMonth">
<p class="mb-1">{{ plan.priceMonth }}&nbsp;/month</p>
<p>{{ plan.priceYear }}&nbsp;/year</p>
</template>
<p v-else>Free</p>
</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>
</div>
<VaDivider v-if="index !== plans.length - 1" />
</template>
</VaCardContent>
</VaCard>
</template>
<script lang="ts" setup>
import { useToast } from "vuestic-ui";
import { reactive } from "vue";
const { init } = useToast();
type MembershipTier = {
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",
padletsUsed: 0,
padletsTotal: "Unlimited",
priceMonth: "$9.99",
priceYear: "$99.99",
uploadLimit: "500MB",
},
{
id: "2",
name: "Gold",
type: "current",
padletsUsed: 19,
padletsTotal: "20",
priceMonth: "$6.99",
priceYear: "$69.99",
uploadLimit: "100MB",
},
{
id: "3",
name: "Neon",
type: "downgrade",
padletsUsed: 0,
padletsTotal: "3",
priceMonth: undefined,
priceYear: undefined,
uploadLimit: "20MB",
},
]);
const switchPlan = (planId: string) => {
plans.forEach((item, index) => {
if (item.id === planId) {
// Set the selected plan to '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";
}
});
init({
message: "You've successfully changed the membership tier",
color: "success",
});
};
</script>

View File

@ -1,100 +0,0 @@
<template>
<VaCard class="mb-6">
<VaCardContent>
<h2 class="page-sub-title">Payment info</h2>
<div class="flex items-center justify-between md:justify-items-stretch">
<div
class="flex grow flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-1 justify-between items-start md:items-center"
>
<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" }}
</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" }}
</VaButton>
<div v-if="!paymentPlan.isYearly" class="mt-2 text-regularSmall">
<span>{{ paymentPlan.priceYear }}&nbsp;/year</span>
<span class="text-danger ml-1">save 16%</span>
</div>
</div>
</div>
<template v-if="paymentCard">
<VaDivider />
<div class="flex items-center justify-between md:justify-items-stretch">
<div
class="flex grow flex-col space-y-2 md:flex-row md:space-y-0 md:space-x-1 justify-between items-start md:items-center"
>
<div class="md:w-48">
<p class="mb-1">Payment method</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
>
</div>
</div>
</template>
</VaCardContent>
</VaCard>
<ChangeYourPaymentPlan
v-if="isChangeYourPaymentPlanModalOpen"
:yearly-plan="paymentPlan.isYearly"
@confirm="updatePaymentPlan"
@cancel="togglePaymentPlanModal"
/>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { usePaymentCardsStore } from "../../stores/payment-cards";
import ChangeYourPaymentPlan from "./modals/ChangeYourPaymentPlan.vue";
const paymentPlan = ref({
id: "1",
name: "Gold",
isYearly: false,
type: "current",
padletsUsed: 19,
padletsTotal: "20",
priceMonth: "$6.99",
priceYear: "$69.99",
switchToYearlySave: "16%",
uploadLimit: "100MB",
});
const cardStore = usePaymentCardsStore();
const isChangeYourPaymentPlanModalOpen = ref(false);
const paymentCard = computed(() => cardStore.currentPaymentCard);
const togglePaymentPlanModal = () => {
isChangeYourPaymentPlanModalOpen.value =
!isChangeYourPaymentPlanModalOpen.value;
};
const updatePaymentPlan = () => {
paymentPlan.value.isYearly = !paymentPlan.value.isYearly;
isChangeYourPaymentPlanModalOpen.value = false;
};
</script>

View File

@ -1,59 +0,0 @@
<template>
<VaModal
:mobile-fullscreen="false"
size="small"
max-width="380px"
hide-default-actions
model-value
close-button
@update:modelValue="emits('cancel')"
>
<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>
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>
</div>
</VaModal>
</template>
<script lang="ts" setup>
import { useToast } from "vuestic-ui";
const { init } = useToast();
defineProps({
yearlyPlan: {
type: Boolean,
required: true,
},
});
const emits = defineEmits(["cancel", "confirm"]);
const confirm = () => {
init({
message: "You've successfully changed your payment plan",
color: "success",
});
emits("confirm");
};
</script>
<style lang="scss">
.va-modal__inner {
min-width: 326px;
}
</style>

View File

@ -1,14 +0,0 @@
<template>
<h1 class="page-title">How can we help you?</h1>
<Categories />
<RequestDemo />
<Questions />
<Navigation />
</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";
</script>

View File

@ -1,98 +0,0 @@
{
"Rising cost of living": [
{
"name": "Fraud and Security"
},
{
"name": "Secure Key help"
},
{
"name": "Fraud and common scams"
},
{
"name": "What we need to keep you safe"
},
{
"name": "How we keep you safe"
},
{
"name": "How to keep yourself safe"
}
],
"Bank accounts": [
{
"name": "General bank account help"
},
{
"name": "Switching to first direct"
},
{
"name": "Statements and balances"
},
{
"name": "Standing orders and Direct Debits"
},
{
"name": "Debit cards"
},
{
"name": "Overdrafts"
},
{
"name": "Managing personal details"
}
],
"Product support": [
{
"name": "Personal loans help"
},
{
"name": "Credit card help"
},
{
"name": "Savings help"
},
{
"name": "Sharedealing help"
},
{
"name": "First Directory help"
}
],
"Mobile and Online Banking": [
{
"name": "Register for Mobile and Online Banking"
},
{
"name": "Get help logging on"
},
{
"name": "Move your App to a new phone"
}
],
"Help with money worries": [
{
"name": "Budgetting and money management"
},
{
"name": "Dealing with financial difficulty"
}
],
"Life events": [
{
"name": "Help with bereavement"
},
{
"name": "Domestic and financial abuse"
},
{
"name": "Mental health and support"
},
{
"name": "Separation and your banking"
},
{
"name": "Someone else managing finances"
}
]
}

View File

@ -1,56 +0,0 @@
[
{
"id": 1,
"icon": "diversity_1",
"name": "Getting Started",
"intro": "Start using Service easily with our actionable tips."
},
{
"id": 2,
"icon": "check_box",
"name": "How-to Articles",
"intro": "Check out ready workflows tailored to your needs."
},
{
"id": 3,
"icon": "tv_signin",
"name": "Billing and Account",
"intro": "Adjust your subscription and limits to your liking."
},
{
"id": 4,
"icon": "page_info",
"name": "SEO",
"intro": "Increase traffic and boost search rankings with the help of 20+ tools."
},
{
"id": 5,
"icon": "currency_exchange",
"name": "Advertising",
"intro": "Research your competitors' advertising campaigns and launch your own."
},
{
"id": 6,
"icon": "feed",
"name": "Social Media",
"intro": "Schedule, post, and track performance across all key social platforms."
},
{
"id": 7,
"icon": "content_paste_go",
"name": "Content Marketing",
"intro": "Create a content plan, find gaps, and research, write and audit content."
},
{
"id": 8,
"icon": "chart_data",
"name": "Trends",
"intro": "Analyze the market, benchmark against competitors,and follow emerging trends."
},
{
"id": 9,
"icon": "manage_accounts",
"name": "Management",
"intro": "Keep all your marketing plans and activities under control. Automate reporting."
}
]

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -1,54 +0,0 @@
<template>
<VaInput v-model="searchValue" class="mb-4" placeholder="Search">
<template #appendInner>
<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"
>
<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>
<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.
</VaAlert>
</template>
<script lang="ts" setup>
import categories from "../data/popularCategories.json";
import { ref, computed } from "vue";
const searchValue = ref("");
const filteredCategories = computed(() => {
const value = searchValue.value.trim().toLowerCase();
if (value.length === 0) {
return categories;
}
return categories.filter((category) => {
return (
category.intro.toLowerCase().includes(value) ||
category.name.toLowerCase().includes(value)
);
});
});
</script>

View File

@ -1,27 +0,0 @@
<template>
<VaCard class="mb-4 p-6">
<section class="grid sm:grid-cols-2 lg:grid-cols-3 sm:gap-5 lg:gap-9">
<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"
>
<a class="va-link" href="#">{{ item.name }}</a>
</li>
</ul>
</div>
</section>
</VaCard>
</template>
<script setup>
import { computed } from "vue";
import navigation from "../data/navigationLinks.json";
const navSections = computed(() => {
return Object.keys(navigation);
});
</script>

View File

@ -1,154 +0,0 @@
<template>
<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"
>
<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:
</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.
</li>
<li>
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>
</ul>
</article>
</VaCollapse>
<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.
</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.
</p>
<a class="va-link font-semibold" href="#"
>Find out more about Secure Keys
<VaIcon :size="18" name="chevron_right" />
</a>
</article>
</VaCollapse>
<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:
</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.
</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.
</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.
</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.
</p>
</article>
</VaCollapse>
<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:
</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.
</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.
</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.
</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.
</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.
</p>
</article>
</VaCollapse>
</VaAccordion>
</VaCardContent>
</VaCard>
</template>
<script setup>
import { reactive } from "vue";
const accordionState = reactive([false, true, false, false]);
</script>

View File

@ -1,64 +0,0 @@
<template>
<VaCard class="mb-5 pr-4 flex justify-between">
<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>
</VaCardContent>
<VaCardActions align="left">
<VaButton @click="showModal = !showModal">Request a demo</VaButton>
</VaCardActions>
</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"
>
<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! 🔥
</p>
<VaInput
v-model="email"
:rules="[
(v) => !!v || 'Email field is required',
(v) => /.+@.+\..+/.test(v) || 'Email should be valid',
]"
class="mb-4"
label="Email"
type="email"
/>
</VaForm>
</VaModal>
</template>
<script setup>
import { ref } from "vue";
import { useForm, useToast } from "vuestic-ui";
const showModal = ref(false);
const email = ref("");
const { validate } = useForm("form");
const { init } = useToast();
const submit = async () => {
if (!validate()) {
return;
}
init({
title: "Demo Request Submitted!",
message: "An expert will get in touch soon",
color: "success",
});
showModal.value = false;
};
</script>

View File

@ -1,32 +0,0 @@
<template>
<h1 class="page-title">Payment methods</h1>
<VaCard class="mb-6">
<VaCardContent>
<div class="text-2xl font-bold leading-5 mb-6">My cards</div>
<PaymentCardList />
</VaCardContent>
</VaCard>
<VaCard class="mb-6">
<VaCardContent>
<div class="text-2xl font-bold leading-5 mb-6">Billing address</div>
<BillingAddressList />
<div class="space-y-2 mt-6">
<div class="text-lg font-bold mb-2">Tax location</div>
<div class="space-y-1">
<div class="text-sm text-gray-500">United States - 10% VAT</div>
<div class="text-sm text-primary underline">More info</div>
</div>
</div>
</VaCardContent>
</VaCard>
</template>
<script lang="ts" setup>
import PaymentCardList from "./widgets/my-cards/PaymentCardList.vue";
import BillingAddressList from "./widgets/billing-address/BillingAddressList.vue";
</script>

View File

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

View File

@ -1,20 +0,0 @@
<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"
/>
<img v-if="props.type === 'visa'" alt="Visa Logo" src="./visa.png" />
</div>
</template>
<script setup lang="ts">
import { PaymentSystemType } from "../types";
const props = defineProps<{
type: PaymentSystemType;
}>();
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,26 +0,0 @@
export enum PaymentSystemType {
Visa = "visa",
MasterCard = "mastercard",
}
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
}
export interface BillingAddress {
id: string;
name: string;
isPrimary: boolean; // show Primary badge
street: string;
city: string;
state: string;
postalCode: string;
country: string;
}

View File

@ -1,23 +0,0 @@
import BillingAddressCreateModal from "./BillingAddressCreateModal.vue";
export default {
components: { BillingAddressCreateModal },
title: "BillingAddressCreateModal",
component: BillingAddressCreateModal,
tags: ["autodocs"],
};
export const Default = () => ({
components: { BillingAddressCreateModal },
data() {
return {
showModal: false,
};
},
template: `
<va-button @click="showModal = !showModal">
Show modal
</va-button>
<BillingAddressCreateModal v-if="showModal" @close="showModal = false"/>
`,
});

View File

@ -1,52 +0,0 @@
<template>
<VaModal
hide-default-actions
model-value
size="small"
close-button
@cancel="emits('close')"
>
<h3 class="va-h4 mb-4">Add Billing Address</h3>
<BillingAddressEdit
:billing-address="billingAddress"
submit-text="Add Address"
@cancel="emits('close')"
@save="update"
/>
</VaModal>
</template>
<script lang="ts" setup>
import { ref, reactive } from "vue";
import BillingAddressEdit from "./BillingAddressEdit.vue";
import { BillingAddress } from "../../types";
import { useToast } from "vuestic-ui";
import { useBillingAddressesStore } from "../../../../stores/billing-addresses";
const isModalOpen = ref(false);
const emits = defineEmits(["close"]);
const store = useBillingAddressesStore();
const { init } = useToast();
const billingAddress = reactive({
id: Math.ceil(Math.random() * 100) + "",
name: "",
isPrimary: false,
street: "",
city: "",
state: "",
postalCode: "",
country: "",
} satisfies BillingAddress);
const update = (address: BillingAddress) => {
isModalOpen.value = false;
store.create(address);
init({
message: "You've successfully created a new Billing Address",
color: "success",
});
emits("close");
};
</script>

View File

@ -1,59 +0,0 @@
import BillingAddressEdit from "./BillingAddressEdit.vue";
import { BillingAddress } from "../../types";
export default {
title: "BillingAddressEdit",
component: BillingAddressEdit,
tags: ["autodocs"],
};
export const Default = () => ({
components: { BillingAddressEdit },
data() {
return {
lastEvent: "",
billingAddress: {
id: "1",
name: "Name",
isPrimary: false,
street: "Ap #285-7193 Ullamcorper Avenue",
city: "Amesbury",
state: "HI",
postalCode: "93373",
country: "US",
} satisfies BillingAddress,
};
},
template: `
<BillingAddressEdit
:billingAddress="billingAddress"
submitText="Update"
@save="lastEvent = 'save'"
@cancel="lastEvent = 'cancel'"
/>
<br>
<p>Last event: <span data-testid="lastEvent">{{ lastEvent }}</span></p>`,
});
export const Empty = () => ({
components: { BillingAddressEdit },
data() {
return {
billingAddress: {
id: "1",
name: "",
isPrimary: false,
street: "",
city: "",
state: "",
postalCode: "",
country: "",
} satisfies BillingAddress,
};
},
template: `
<BillingAddressEdit
:billingAddress="billingAddress"
submitText="Create"
/>`,
});

View File

@ -1,81 +0,0 @@
<template>
<VaForm ref="form" @submit.prevent="submit">
<VaInput
v-model="localBillingAddress.name"
:rules="[(v) => !!v || 'Name field is required']"
class="mb-4"
label="Name"
/>
<VaCheckbox
v-model="localBillingAddress.isPrimary"
class="mb-4"
label="Primary Address"
/>
<VaInput
v-model="localBillingAddress.street"
:rules="[(v) => !!v || 'Street field is required']"
class="mb-4"
label="Street"
/>
<VaInput
v-model="localBillingAddress.city"
:rules="[(v) => !!v || 'City field is required']"
class="mb-4"
label="City"
/>
<VaInput
v-model="localBillingAddress.state"
:rules="[(v) => !!v || 'State field is required']"
class="mb-4"
label="State"
/>
<VaInput
v-model="localBillingAddress.postalCode"
:rules="[(v) => !!v || 'Postal Code field is required']"
class="mb-4"
label="Postal Code"
/>
<VaInput
v-model="localBillingAddress.country"
:rules="[(v) => !!v || 'Country field is required']"
class="mb-4"
label="Country"
/>
<div class="flex justify-end gap-3">
<VaButton color="secondary" preset="secondary" @click="emits('cancel')"
>Cancel</VaButton
>
<VaButton @click="submit">{{ submitText }}</VaButton>
</div>
</VaForm>
</template>
<script lang="ts" setup>
import { useForm } from "vuestic-ui";
import { BillingAddress } from "../../types";
import { watch, ref } from "vue";
const { validate } = useForm("form");
const emits = defineEmits(["save", "cancel"]);
const props = defineProps<{
billingAddress: BillingAddress;
submitText: string;
}>();
const localBillingAddress = ref<BillingAddress>({ ...props.billingAddress });
watch(
() => props.billingAddress,
(value) => {
localBillingAddress.value = { ...value };
},
{ deep: true },
);
const submit = () => {
if (validate()) {
emits("save", localBillingAddress.value);
}
};
</script>

View File

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

View File

@ -1,86 +0,0 @@
<template>
<div class="grid md:grid-cols-2 grid-cols-1 gap-4">
<template v-if="loading">
<div
v-for="i in 4"
:key="i"
class="min-h-[114px] p-4 rounded-lg border border-dashed border-backgroundBorder flex flex-row items-center gap-6"
>
<div class="flex flex-col gap-2 flex-grow">
<VaSkeleton class height="1.5rem" variant="text" width="10rem" />
<div class="flex gap-4">
<VaSkeleton height="3rem" variant="rounded" width="5rem" />
<VaSkeleton :lines="2" variant="text" />
</div>
</div>
</div>
</template>
<template v-else>
<CardListItem
v-for="billingAddress in list"
:key="billingAddress.id"
:billing-address="billingAddress"
@edit="addressToEdit = billingAddress"
@remove="remove(billingAddress)"
/>
<div
class="sm:min-h-[114px] p-4 rounded-lg border border-dashed border-primary flex flex-col sm:flex-row items-start sm:items-center gap-4"
:style="{ backgroundColor: colorToRgba(getColor('primary'), 0.07) }"
>
<div class="flex flex-col gap-2 flex-grow">
<div class="text-lg font-bold leading-relaxed">Important note</div>
<div class="text-secondary text-sm leading-tight">
Please ensure the provided billing address matches the information
on file with your financial institution to avoid any processing
delays.
</div>
</div>
<VaButton class="flex-none w-full sm:w-auto" @click="showCreate = true"
>New address</VaButton
>
</div>
</template>
</div>
<AddressCreateModal v-if="showCreate" @close="showCreate = false" />
<AddressUpdateModal
v-if="addressToEdit"
:billing-address="addressToEdit"
@close="addressToEdit = undefined"
/>
</template>
<script lang="ts" setup>
import CardListItem from "./BillingAddressListItem.vue";
import { computed, ref } from "vue";
import { useModal, useToast } from "vuestic-ui";
import AddressCreateModal from "./BillingAddressCreateModal.vue";
import AddressUpdateModal from "./BillingAddressUpdateModal.vue";
import { useBillingAddressesStore } from "../../../../stores/billing-addresses";
import { BillingAddress } from "../../types";
import { useColors } from "vuestic-ui";
const store = useBillingAddressesStore();
const list = computed(() => store.allBillingAddresses);
const loading = computed(() => store.loading);
const { confirm } = useModal();
const showCreate = ref<boolean>(false);
const addressToEdit = ref<BillingAddress>();
const { init } = useToast();
store.load();
const remove = async (card: BillingAddress) => {
confirm({
message: "Are you really sure you want to delete this address?",
size: "small",
maxWidth: "380px",
}).then((ok) => {
if (!ok) return;
store.remove(card.id);
init({ message: "Billing Address has been deleted", color: "success" });
});
};
const { getColor, colorToRgba } = useColors();
</script>

View File

@ -1,36 +0,0 @@
import BillingAddressListItem from "./BillingAddressListItem.vue";
import { BillingAddress } from "../../types";
export default {
title: "BillingAddressListItem",
component: BillingAddressListItem,
tags: ["autodocs"],
};
export const Default = () => ({
components: { BillingAddressListItem },
data() {
return {
address: {
id: "1",
name: "Home Address",
isPrimary: false,
street: "Ap #285-7193 Ullamcorper Avenue",
city: "Amesbury",
state: "HI",
postalCode: "93373",
country: "US",
} satisfies BillingAddress,
lastEvent: "___",
};
},
template: `
<BillingAddressListItem
:billingAddress="address"
@remove="lastEvent = 'remove'"
@edit="lastEvent = 'edit'"
/>
<br>
<p>Last event: <span data-testid="lastEvent">{{ lastEvent }}</span></p>
`,
});

View File

@ -1,49 +0,0 @@
<template>
<div
class="min-h-[114px] p-4 rounded-lg border border-dashed border-backgroundBorder flex flex-col sm:flex-row items-start sm:items-center gap-6"
>
<div class="flex flex-col gap-2 flex-grow">
<div class="flex items-center">
<div class="text-lg font-bold">{{ billingAddress.name }}</div>
<VaBadge
v-if="billingAddress.isPrimary"
class="ml-2"
color="danger"
text="Primary"
/>
</div>
<div class="text-secondary leading-5">
<div>{{ billingAddress.street }}</div>
<div>
{{ billingAddress.city }}, {{ billingAddress.state }}
{{ billingAddress.postalCode }}
</div>
<div>{{ billingAddress.country }}</div>
</div>
</div>
<div class="w-full sm:w-auto flex-none flex sm:block">
<VaButton class="mr-2 flex-grow" preset="primary" @click="emits('edit')"
>Edit</VaButton
>
<VaButton
icon="mso-delete"
preset="primary"
aria-label="Remove"
@click="emits('remove')"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { BillingAddress } from "../../types";
const emits = defineEmits(["edit", "remove"]);
const props = defineProps<{
billingAddress: BillingAddress;
}>();
const billingAddress = computed(() => props.billingAddress);
</script>

View File

@ -1,34 +0,0 @@
import BillingAddressUpdateModal from "./BillingAddressUpdateModal.vue";
import { BillingAddress } from "../../types";
export default {
components: { BillingAddressUpdateModal },
title: "BillingAddressUpdateModal",
component: BillingAddressUpdateModal,
tags: ["autodocs"],
};
export const Default = () => ({
components: { BillingAddressUpdateModal },
data() {
return {
showModal: false,
billingAddress: {
id: "1",
name: "",
isPrimary: false,
street: "",
city: "",
state: "",
postalCode: "",
country: "",
} satisfies BillingAddress,
};
},
template: `
<va-button @click="showModal = !showModal">
Show modal
</va-button>
<BillingAddressUpdateModal :billingAddress="billingAddress" v-if="showModal" @close="showModal = false"/>
`,
});

View File

@ -1,46 +0,0 @@
<template>
<VaModal
hide-default-actions
model-value
size="small"
close-button
@cancel="emits('close')"
>
<h3 class="va-h4 mb-4">Add Billing Address</h3>
<AddressEdit
:billing-address="billingAddress"
submit-text="Save Address"
@cancel="emits('close')"
@save="updateCard"
/>
</VaModal>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import AddressEdit from "./BillingAddressEdit.vue";
import { BillingAddress } from "../../types";
import { useToast } from "vuestic-ui";
import { useBillingAddressesStore } from "../../../../stores/billing-addresses";
const isModalOpen = ref(false);
const { init } = useToast();
const props = defineProps<{
billingAddress: BillingAddress;
}>();
const emits = defineEmits(["close"]);
const store = useBillingAddressesStore();
const billingAddress = ref({ ...props.billingAddress });
const updateCard = (address: BillingAddress) => {
isModalOpen.value = false;
store.update(address);
init({
message: "You've successfully updated a Billing Address",
color: "success",
});
emits("close");
};
</script>

View File

@ -1,23 +0,0 @@
import PaymentCardCreateModal from "./PaymentCardCreateModal.vue";
export default {
components: { PaymentCardCreateModal },
title: "PaymentCardCreateModal",
component: PaymentCardCreateModal,
tags: ["autodocs"],
};
export const Default = () => ({
components: { PaymentCardCreateModal },
data() {
return {
showModal: false,
};
},
template: `
<va-button @click="showModal = !showModal">
Show modal
</va-button>
<PaymentCardCreateModal v-if="showModal" @close="showModal = false"/>
`,
});

View File

@ -1,50 +0,0 @@
<template>
<VaModal
hide-default-actions
model-value
size="small"
close-button
@cancel="emits('close')"
>
<h3 class="va-h4 mb-4">Add payment card</h3>
<PaymentCardEdit
:payment-card="paymentCard"
submit-text="Add Card"
@cancel="emits('close')"
@save="updateCard"
/>
</VaModal>
</template>
<script lang="ts" setup>
import { ref, reactive } from "vue";
import PaymentCardEdit from "./PaymentCardEdit.vue";
import { PaymentCard, PaymentSystemType } from "../../types";
import { usePaymentCardsStore } from "../../../../stores/payment-cards";
import { useToast } from "vuestic-ui";
const isModalOpen = ref(false);
const emits = defineEmits(["close"]);
const store = usePaymentCardsStore();
const { init } = useToast();
const paymentCard = reactive({
id: Math.ceil(Math.random() * 100) + "",
name: "",
isPrimary: false,
paymentSystem: PaymentSystemType.Visa,
cardNumberMasked: "",
expirationDate: "",
} satisfies PaymentCard);
const updateCard = (card: PaymentCard) => {
isModalOpen.value = false;
store.create(card);
init({
message: "You've successfully created a new payment card",
color: "success",
});
emits("close");
};
</script>

View File

@ -1,55 +0,0 @@
import PaymentCardEdit from "./PaymentCardEdit.vue";
import { PaymentSystemType, PaymentCard } from "../../types";
export default {
title: "PaymentCardEdit",
component: PaymentCardEdit,
tags: ["autodocs"],
};
export const Default = () => ({
components: { PaymentCardEdit },
data() {
return {
lastEvent: "",
paymentCard: {
id: "1",
name: "Main card",
isPrimary: true,
paymentSystem: PaymentSystemType.Visa,
cardNumberMasked: "****1679",
expirationDate: "09/24",
} satisfies PaymentCard,
};
},
template: `
<PaymentCardEdit
:paymentCard="paymentCard"
submitText="Update Card"
@save="lastEvent = 'save'"
@cancel="lastEvent = 'cancel'"
/>
<br>
<p>Last event: <span data-testid>{{ lastEvent }}</span></p>`,
});
export const Empty = () => ({
components: { PaymentCardEdit },
data() {
return {
paymentCard: {
id: "",
name: "",
isPrimary: false,
paymentSystem: PaymentSystemType.Visa,
cardNumberMasked: "",
expirationDate: "",
} satisfies PaymentCard,
};
},
template: `
<PaymentCardEdit
:paymentCard="paymentCard"
submitText="Create Card"
/>`,
});

View File

@ -1,87 +0,0 @@
<template>
<VaForm ref="form" @submit.prevent="submit">
<VaInput
v-model="paymentCardLocal.name"
:rules="[(v) => !!v || 'Card Name field is required']"
class="mb-4"
label="Card Name"
/>
<VaCheckbox
v-model="paymentCardLocal.isPrimary"
class="mb-4"
label="Primary Card"
/>
<VaSelect
v-model="paymentCardLocal.paymentSystem"
:options="paymentSystemTypeOptions"
:rules="[(v) => !!v || 'Payment System field is required']"
class="mb-4"
label="Payment System"
/>
<VaInput
v-model="paymentCardLocal.cardNumberMasked"
:rules="[(v) => !!v || 'Card Number field is required']"
class="mb-4"
label="Card Number"
mask="creditCard"
placeholder="#### #### #### ####"
/>
<VaInput
v-model="paymentCardLocal.expirationDate"
:mask="{
date: true,
datePattern: ['m', 'y'],
}"
:rules="[
(v) => !!v || 'Expiration Date field is required',
(v) => /^\d{4}$/.test(v) || 'Expiration Date must be in MM/YY format',
]"
class="mb-4"
label="Expiration Date"
/>
<div class="flex justify-end gap-3">
<VaButton color="secondary" preset="secondary" @click="emits('cancel')"
>Cancel</VaButton
>
<VaButton @click="submit">{{ submitText }}</VaButton>
</div>
</VaForm>
</template>
<script lang="ts" setup>
import { useForm } from "vuestic-ui";
import { PaymentCard, PaymentSystemType } from "../../types";
import { watch, ref } from "vue";
const { validate } = useForm("form");
const emits = defineEmits(["save", "cancel"]);
const props = defineProps<{
paymentCard: PaymentCard;
submitText: string;
}>();
const paymentSystemTypeOptions = Object.values(PaymentSystemType);
const paymentCardLocal = ref({ ...props.paymentCard });
watch(
() => props.paymentCard,
(value) => {
paymentCardLocal.value = { ...value };
},
{ deep: true },
);
const submit = () => {
if (validate()) {
emits("save", {
...paymentCardLocal.value,
cardNumberMasked: paymentCardLocal.value.cardNumberMasked.replace(
/\d{12}(.*)/g,
"****$1",
),
});
}
};
</script>

View File

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

View File

@ -1,85 +0,0 @@
<template>
<div class="grid md:grid-cols-2 grid-cols-1 gap-4">
<template v-if="loading">
<div
v-for="i in 4"
:key="i"
class="min-h-[114px] p-4 rounded-lg border border-dashed border-backgroundBorder flex flex-row items-center gap-6"
>
<div class="flex flex-col gap-2 flex-grow">
<VaSkeleton class height="1.5rem" variant="text" width="10rem" />
<div class="flex gap-4">
<VaSkeleton height="3rem" variant="rounded" width="5rem" />
<VaSkeleton :lines="2" variant="text" />
</div>
</div>
</div>
</template>
<template v-else>
<CardListItem
v-for="paymentCard in list"
:key="paymentCard.id"
:card="paymentCard"
@edit="cardToEdit = paymentCard"
@remove="remove(paymentCard)"
/>
<div
class="sm:h-[114px] p-4 rounded-lg border border-dashed border-primary flex flex-col sm:flex-row items-start sm:items-center gap-4"
:style="{ backgroundColor: colorToRgba(getColor('primary'), 0.07) }"
>
<div class="flex flex-col gap-2 flex-grow">
<div class="text-lg font-bold leading-relaxed">Important note</div>
<div class="text-secondary text-sm leading-tight">
Please carefully read Product Terms before adding your new payment
card
</div>
</div>
<VaButton class="flex-none w-full sm:w-auto" @click="showCreate = true"
>Add card</VaButton
>
</div>
</template>
</div>
<PaymentCardCreateModal v-if="showCreate" @close="showCreate = false" />
<PaymentCardUpdateModal
v-if="cardToEdit"
:payment-card="cardToEdit"
@close="cardToEdit = undefined"
/>
</template>
<script lang="ts" setup>
import CardListItem from "./PaymentCardListItem.vue";
import { usePaymentCardsStore } from "../../../../stores/payment-cards";
import { computed, ref } from "vue";
import { useColors } from "vuestic-ui";
import { PaymentCard } from "../../types";
import { useModal, useToast } from "vuestic-ui";
import PaymentCardCreateModal from "./PaymentCardCreateModal.vue";
import PaymentCardUpdateModal from "./PaymentCardUpdateModal.vue";
const store = usePaymentCardsStore();
const list = computed(() => store.allPaymentCards);
const loading = computed(() => store.loading);
const { confirm } = useModal();
const showCreate = ref<boolean>(false);
const cardToEdit = ref<PaymentCard>();
const { init } = useToast();
store.load();
const remove = async (card: PaymentCard) => {
confirm({
message: "Are you really sure you want to delete this card?",
size: "small",
maxWidth: "380px",
}).then((ok) => {
if (!ok) return;
store.remove(card.id);
init({ message: "Payment card has been deleted", color: "success" });
});
};
const { getColor, colorToRgba } = useColors();
</script>

View File

@ -1,34 +0,0 @@
import CardListItem from "./PaymentCardListItem.vue";
import { PaymentSystemType, PaymentCard } from "../../types";
export default {
title: "CardListItem",
component: CardListItem,
tags: ["autodocs"],
};
export const Default = () => ({
components: { CardListItem },
data() {
return {
card: {
id: "1",
name: "Main card",
isPrimary: true,
paymentSystem: PaymentSystemType.Visa,
cardNumberMasked: "****1679",
expirationDate: "09/24",
} satisfies PaymentCard,
lastEvent: "___",
};
},
template: `
<CardListItem
:card="card"
@remove="lastEvent = 'remove'"
@edit="lastEvent = 'edit'"
/>
<br>
<p>Last event: <span data-testid>{{ lastEvent }}</span></p>
`,
});

View File

@ -1,58 +0,0 @@
<template>
<div
class="min-h-[114px] p-4 rounded-lg border border-dashed border-backgroundBorder flex flex-col sm:flex-row items-start sm:items-center gap-6"
>
<div class="flex flex-col gap-2 flex-grow">
<div class="flex items-center">
<div class="text-lg font-bold">{{ card.name }}</div>
<VaBadge
v-if="card.isPrimary"
class="ml-2"
color="danger"
text="Primary"
/>
</div>
<div class="flex gap-4 items-center">
<PaymentSystem :type="card.paymentSystem" />
<div>
<div class="text-[15px] font-bold mb-2 capitalize">
{{ card.paymentSystem }} {{ card.cardNumberMasked }}
</div>
<div class="text-secondary">
Card expires at {{ expirationDateString }}
</div>
</div>
</div>
</div>
<div class="w-full sm:w-auto flex-none flex sm:block">
<VaButton class="mr-2 flex-grow" preset="primary" @click="emits('edit')"
>Edit</VaButton
>
<VaButton
icon="mso-delete"
preset="primary"
aria-label="Remove"
@click="emits('remove')"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import PaymentSystem from "../../payment-system/PaymentSystem.vue";
import { PaymentCard } from "../../types";
const emits = defineEmits(["edit", "remove"]);
const props = defineProps<{
card: PaymentCard;
}>();
const card = computed(() => props.card);
const expirationDateString = computed(() => {
const e = props.card.expirationDate;
return `${e[0]}${e[1]}/${e[2]}${e[3]}`;
});
</script>

View File

@ -1,32 +0,0 @@
import PaymentCardUpdateModal from "./PaymentCardUpdateModal.vue";
import { PaymentSystemType, PaymentCard } from "../../types";
export default {
components: { PaymentCardUpdateModal },
title: "PaymentCardUpdateModal",
component: PaymentCardUpdateModal,
tags: ["autodocs"],
};
export const Default = () => ({
components: { PaymentCardUpdateModal },
data() {
return {
showModal: false,
paymentCard: {
id: "1",
name: "Main card",
isPrimary: true,
paymentSystem: PaymentSystemType.Visa,
cardNumberMasked: "****1679",
expirationDate: "09/24",
} satisfies PaymentCard,
};
},
template: `
<va-button @click="showModal = !showModal">
Show modal
</va-button>
<PaymentCardUpdateModal :payment-card="paymentCard" v-if="showModal" @close="showModal = false"/>
`,
});

View File

@ -1,46 +0,0 @@
<template>
<VaModal
hide-default-actions
model-value
size="small"
close-button
@cancel="emits('close')"
>
<h3 class="va-h4 mb-4">Add payment card</h3>
<PaymentCardEdit
:payment-card="paymentCard"
submit-text="Save Card"
@cancel="emits('close')"
@save="updateCard"
/>
</VaModal>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import PaymentCardEdit from "./PaymentCardEdit.vue";
import { PaymentCard } from "../../types";
import { usePaymentCardsStore } from "../../../../stores/payment-cards";
import { useToast } from "vuestic-ui";
const isModalOpen = ref(false);
const { init } = useToast();
const props = defineProps<{
paymentCard: PaymentCard;
}>();
const emits = defineEmits(["close"]);
const store = usePaymentCardsStore();
const paymentCard = ref({ ...props.paymentCard });
const updateCard = (card: PaymentCard) => {
isModalOpen.value = false;
store.update(card);
init({
message: "You've successfully updated a payment card",
color: "success",
});
emits("close");
};
</script>

View File

@ -1,129 +0,0 @@
<template>
<h1 class="page-title">Choose your plan</h1>
<div class="py-4 text-lg leading-[26px]">
If you need more info about our pricing, please check
<span class="text-primary underline">Pricing Guidelines</span>.
</div>
<div class="flex flex-col p-4 bg-backgroundSecondary">
<div class="flex justify-center">
<VaButtonToggle
v-model="selectedDuration"
color="background-element"
border-color="background-element"
:options="[
{ label: 'Monthly', value: 'Monthly' },
{ label: 'Annual', value: 'Annual' },
]"
/>
</div>
<div
class="flex flex-col md:flex-row justify-center items-center space-y-4 md:space-x-6 md:space-y-0 mt-6"
>
<VaCard
v-for="plan in pricingPlans"
:key="plan.model"
:class="{
'md:!py-10 !bg-backgroundCardSecondary': plan.model === 'Advanced',
'!bg-backgroundCardPrimary': plan.model !== 'Advanced',
'ring-2 ring-primary ring-offset-2': plan.model === selectedPlan,
}"
class="flex w-[326px] md:w-[349px] h-fit p-6 rounded-[13px]"
>
<div
:class="{ '!space-y-10': plan.model === 'Advanced' }"
class="space-y-8 md:space-y-10"
>
<div class="space-y-4 text-center">
<h2 class="pricing-plan-card-title">
{{ plan.title }}
</h2>
<VaBadge
v-for="badge in plan.badges"
:key="badge"
:style="badgeStyles"
:text="badge"
color="primary"
/>
<p class="text-lg leading-[26px] text-secondary">
{{ plan.description }}
</p>
<div
class="flex space-x-1 justify-center items-baseline text-lg leading-[26px]"
>
<span>$</span
><span
class="text-[32px] md:text-5xl leading-[110%] md:leading-[56px] font-bold"
>{{
selectedDuration === "Annual" ? plan.price : plan.priceMonth
}}</span
><span
>/ {{ selectedDuration === "Annual" ? "year" : "mo" }}</span
>
</div>
</div>
<div class="space-y-6">
<div
v-for="feature in plan.features"
:key="feature.description"
class="flex justify-between items-center text-lg leading-[26px]"
>
<p :class="{ 'text-secondary': !feature.isAvailable }">
{{ feature.description }}
</p>
<VaIcon
v-if="feature.isAvailable"
color="primary"
name="mso-check"
size="20px"
/>
<VaIcon
v-else
color="backgroundBorder"
name="mso-block"
size="20px"
/>
</div>
</div>
<div class="flex justify-center">
<VaButton
:disabled="plan.model === selectedPlan"
:style="selectButtonStyles"
@click="createModal(plan.model)"
>
Select
</VaButton>
</div>
</div>
</VaCard>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useToast, useModal } from "vuestic-ui";
import { badgeStyles, selectButtonStyles } from "./styles";
import { pricingPlans } from "./options";
const { init } = useToast();
const { init: initModal } = useModal();
const selectedDuration = ref<string>("Annual");
const selectedPlan = ref<string>();
const createModal = (planModel: string) => {
initModal({
message: "Are you sure you want to change plan?",
mobileFullscreen: false,
maxWidth: "380px",
size: "small",
onOk: () => selectPlan(planModel),
});
};
const selectPlan = (planModel: string) => {
init({ message: "You successfully changed payment plan!", color: "success" });
selectedPlan.value = planModel;
};
</script>

View File

@ -1,52 +0,0 @@
export type PricingPlans = {
title: string;
model: string;
badges?: string[];
description: string;
price: number;
priceMonth: number;
features: Feature[];
};
type Feature = {
description: string;
isAvailable: boolean;
};
const features = [
"Up to 10 Active Users",
"Up to 30 Project Integrations",
"Analytics Module",
"Finance Module",
"Accounting Module",
"Network Platform",
"Unlimited Cloud Spase",
];
export const pricingPlans: PricingPlans[] = [
{
title: "Startup",
model: "Startup",
description: "Optimal for 10+ team size and new startup",
price: 39,
priceMonth: 5,
features: features.map((d, i) => ({ description: d, isAvailable: i < 3 })),
},
{
title: "Advanced",
model: "Advanced",
description: "Optimal for 100+ team size and grown company",
price: 339,
priceMonth: 35,
features: features.map((d, i) => ({ description: d, isAvailable: i < 5 })),
badges: ["Popular choice"],
},
{
title: "Enterprise",
model: "Enterprise",
description: "Optimal for 1000+ team and enterpise",
price: 999,
priceMonth: 100,
features: features.map((d) => ({ description: d, isAvailable: true })),
},
];

View File

@ -1,13 +0,0 @@
export const badgeStyles = {
"--va-badge-text-wrapper-line-height": "14px",
"--va-badge-text-wrapper-letter-spacing": "0.4px",
"--va-badge-text-wrapper-border": "solid 1px",
"--va-badge-font-size": "9px",
};
export const selectButtonStyles = {
"--va-button-content-py": "10px",
"--va-button-content-px": "16px",
"--va-button-font-size": "18px",
"--va-button-line-height": "26px",
};

View File

@ -1,153 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import { useLocalStorage } from "@vueuse/core";
import { useProjects } from "./composables/useProjects";
import ProjectCards from "./widgets/ProjectCards.vue";
import ProjectTable from "./widgets/ProjectsTable.vue";
import EditProjectForm from "./widgets/EditProjectForm.vue";
import { Project } from "./types";
import { useModal, useToast } from "vuestic-ui";
const doShowAsCards = useLocalStorage("projects-view", true);
const { projects, update, add, isLoading, remove, pagination, sorting } =
useProjects();
const projectToEdit = ref<Project | null>(null);
const doShowProjectFormModal = ref(false);
const editProject = (project: Project) => {
projectToEdit.value = project;
doShowProjectFormModal.value = true;
};
const createNewProject = () => {
projectToEdit.value = null;
doShowProjectFormModal.value = true;
};
const { init: notify } = useToast();
const onProjectSaved = async (project: Project) => {
doShowProjectFormModal.value = false;
if ("id" in project) {
await update(project as Project);
notify({
message: "Project updated",
color: "success",
});
} else {
await add(project as Project);
notify({
message: "Project created",
color: "success",
});
}
};
const { confirm } = useModal();
const onProjectDeleted = async (project: Project) => {
const response = await confirm({
title: "Delete project",
message: `Are you sure you want to delete project "${project.project_name}"?`,
okText: "Delete",
size: "small",
maxWidth: "380px",
});
if (!response) {
return;
}
await remove(project);
notify({
message: "Project deleted",
color: "success",
});
};
const editFormRef = ref();
const beforeEditFormModalClose = async (hide: () => unknown) => {
if (editFormRef.value.isFormHasUnsavedChanges) {
const agreed = await confirm({
maxWidth: "380px",
message: "Form has unsaved changes. Are you sure you want to close it?",
size: "small",
});
if (agreed) {
hide();
}
} else {
hide();
}
};
</script>
<template>
<h1 class="page-title">Projects</h1>
<VaCard>
<VaCardContent>
<div class="flex flex-col md:flex-row gap-2 mb-2 justify-between">
<div class="flex flex-col md:flex-row gap-2 justify-start">
<VaButtonToggle
v-model="doShowAsCards"
color="background-element"
border-color="background-element"
:options="[
{ label: 'Cards', value: true },
{ label: 'Table', value: false },
]"
/>
</div>
<VaButton icon="add" @click="createNewProject">Project</VaButton>
</div>
<ProjectCards
v-if="doShowAsCards"
:projects="projects"
:loading="isLoading"
@edit="editProject"
@delete="onProjectDeleted"
/>
<ProjectTable
v-else
v-model:sort-by="sorting.sortBy"
v-model:sorting-order="sorting.sortingOrder"
v-model:pagination="pagination"
:projects="projects"
:loading="isLoading"
@edit="editProject"
@delete="onProjectDeleted"
/>
</VaCardContent>
<VaModal
v-slot="{ cancel, ok }"
v-model="doShowProjectFormModal"
size="small"
mobile-fullscreen
close-button
stateful
hide-default-actions
:before-cancel="beforeEditFormModalClose"
>
<h1 v-if="projectToEdit === null" class="va-h5 mb-4">Add project</h1>
<h1 v-else class="va-h5 mb-4">Edit project</h1>
<EditProjectForm
ref="editFormRef"
:project="projectToEdit"
:save-button-label="projectToEdit === null ? 'Add' : 'Save'"
@close="cancel"
@save="
(project) => {
onProjectSaved(project);
ok();
}
"
/>
</VaModal>
</VaCard>
</template>

View File

@ -1,26 +0,0 @@
<script setup lang="ts">
import { PropType } from "vue";
import { Project } from "../types";
defineProps({
status: {
type: String as PropType<Project["status"]>,
required: true,
},
});
const badgeColorMap: Record<Project["status"], string> = {
"in progress": "primary",
archived: "secondary",
completed: "success",
important: "warning",
};
</script>
<template>
<VaBadge
square
:color="badgeColorMap[$props.status]"
:text="$props.status.toUpperCase()"
/>
</template>

View File

@ -1,92 +0,0 @@
import { Ref, ref, unref } from "vue";
import {
getProjects,
addProject,
updateProject,
removeProject,
Sorting,
Pagination,
} from "../../../data/pages/projects";
import { Project } from "../types";
import { watchIgnorable } from "@vueuse/core";
const makePaginationRef = () =>
ref<Pagination>({ page: 1, perPage: 10, total: 0 });
const makeSortingRef = () =>
ref<Sorting>({ sortBy: "creation_date", sortingOrder: "desc" });
export const useProjects = (options?: {
sorting?: Ref<Sorting>;
pagination?: Ref<Pagination>;
}) => {
const isLoading = ref(false);
const projects = ref<Project[]>([]);
const { sorting = makeSortingRef(), pagination = makePaginationRef() } =
options ?? {};
const fetch = async () => {
isLoading.value = true;
const { data, pagination: newPagination } = await getProjects({
...unref(sorting),
...unref(pagination),
});
projects.value = data as Project[];
ignoreUpdates(() => {
pagination.value = newPagination;
});
isLoading.value = false;
};
const { ignoreUpdates } = watchIgnorable([pagination, sorting], fetch, {
deep: true,
});
fetch();
return {
isLoading,
projects,
fetch,
async add(project: Omit<Project, "id" | "creation_date">) {
isLoading.value = true;
await addProject({
...project,
project_owner: project.project_owner.id,
team: project.team.map((user) => user.id),
});
await fetch();
isLoading.value = false;
},
async update(project: Project) {
isLoading.value = true;
await updateProject({
...project,
project_owner: project.project_owner.id,
team: project.team.map((user) => user.id),
});
await fetch();
isLoading.value = false;
},
async remove(project: Project) {
isLoading.value = true;
await removeProject({
...project,
project_owner: project.project_owner.id,
team: project.team.map((user) => user.id),
});
await fetch();
isLoading.value = false;
},
pagination,
sorting,
};
};

View File

@ -1,18 +0,0 @@
import { User } from "../users/types";
export type Project = {
id: number;
project_name: string;
project_owner: Omit<User, "projects">;
team: Omit<User, "projects">[];
status: "important" | "completed" | "archived" | "in progress";
creation_date: string;
};
export type EmptyProject = Omit<
Project,
"id" | "project_owner" | "creation_date" | "status"
> & {
project_owner: Project["project_owner"] | undefined;
status: Project["status"] | undefined;
};

View File

@ -1,156 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { EmptyProject, Project } from "../types";
import { SelectOption } from "vuestic-ui";
import { useUsers } from "../../users/composables/useUsers";
import ProjectStatusBadge from "../components/ProjectStatusBadge.vue";
import UserAvatar from "../../users/widgets/UserAvatar.vue";
const props = defineProps<{
project: Project | null;
saveButtonLabel: string;
}>();
defineEmits<{
(event: "save", project: Project): void;
(event: "close"): void;
}>();
const defaultNewProject: EmptyProject = {
project_name: "",
project_owner: undefined,
team: [],
status: undefined,
};
const newProject = ref({ ...defaultNewProject });
const isFormHasUnsavedChanges = computed(() => {
return Object.keys(newProject.value).some((key) => {
if (key === "team") {
return false;
}
return (
newProject.value[key as keyof EmptyProject] !==
(props.project ?? defaultNewProject)?.[key as keyof EmptyProject]
);
});
});
defineExpose({
isFormHasUnsavedChanges,
});
watch(
() => props.project,
() => {
if (!props.project) {
return;
}
newProject.value = {
...props.project,
project_owner: props.project.project_owner,
};
},
{ immediate: true },
);
const required = (v: string | SelectOption) => !!v || "This field is required";
const { users: teamUsers, filters: teamFilters } = useUsers({
pagination: ref({ page: 1, perPage: 100, total: 10 }),
});
const { users: ownerUsers, filters: ownerFilters } = useUsers({
pagination: ref({ page: 1, perPage: 100, total: 10 }),
});
</script>
<template>
<VaForm v-slot="{ validate }" class="flex flex-col gap-2">
<VaInput
v-model="newProject.project_name"
label="Project name"
:rules="[required]"
/>
<VaSelect
v-model="newProject.project_owner"
v-model:search="ownerFilters.search"
searchable
label="Owner"
text-by="fullname"
track-by="id"
:rules="[required]"
:options="ownerUsers"
>
<template #content="{ value: user }">
<div v-if="user" :key="user.id" class="flex items-center gap-1 mr-4">
<UserAvatar :user="user" size="18px" />
{{ user.fullname }}
</div>
</template>
</VaSelect>
<VaSelect
v-model="newProject.team"
v-model:search="teamFilters.search"
label="Team"
text-by="fullname"
track-by="id"
multiple
:rules="[
(v: any) => ('length' in v && v.length > 0) || 'This field is required',
]"
:options="teamUsers"
:max-visible-options="$vaBreakpoint.mdUp ? 3 : 1"
>
<template #content="{ valueArray }">
<template v-if="valueArray">
<div
v-for="(user, index) in valueArray"
:key="user.id"
class="flex items-center gap-1 mr-2"
>
<UserAvatar :user="user" size="18px" />
{{ user.fullname }}{{ index < valueArray.length - 1 ? "," : "" }}
</div>
</template>
</template>
</VaSelect>
<VaSelect
v-model="newProject.status"
label="Status"
:rules="[required]"
track-by="value"
value-by="value"
:options="[
{ text: 'In progress', value: 'in progress' },
{ text: 'Archived', value: 'archived' },
{ text: 'Completed', value: 'completed' },
{ text: 'Important', value: 'important' },
]"
>
<template #content="{ value }">
<ProjectStatusBadge v-if="value" :status="value.value" />
</template>
</VaSelect>
<div class="flex justify-end flex-col-reverse sm:flex-row mt-4 gap-2">
<VaButton preset="secondary" color="secondary" @click="$emit('close')"
>Cancel</VaButton
>
<VaButton @click="validate() && $emit('save', newProject as Project)">{{
saveButtonLabel
}}</VaButton>
</div>
</VaForm>
</template>
<style lang="scss" scoped>
.va-select-content__autocomplete {
flex: 1;
}
.va-input-wrapper__text {
gap: 0.2rem;
}
</style>

View File

@ -1,95 +0,0 @@
<script setup lang="ts">
import { PropType } from "vue";
import { Project } from "../types";
import ProjectStatusBadge from "../components/ProjectStatusBadge.vue";
defineProps({
projects: {
type: Array as PropType<Project[]>,
required: true,
},
loading: {
type: Boolean,
required: true,
},
});
defineEmits<{
(event: "edit", project: Project): void;
(event: "delete", project: Project): void;
}>();
const avatarColor = (userName: string) => {
const colors = ["primary", "#FFD43A", "#ADFF00", "#262824", "danger"];
const index = userName.charCodeAt(0) % colors.length;
return colors[index];
};
</script>
<template>
<VaInnerLoading
v-if="projects.length > 0 || loading"
:loading="loading"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[4rem]"
>
<VaCard
v-for="project in projects"
:key="project.project_name"
:style="{
'--va-card-outlined-border': '1px solid var(--va-background-element)',
}"
outlined
>
<VaCardContent class="flex flex-col h-full">
<div class="text-[var(--va-secondary)]">
{{ project.creation_date }}
</div>
<div class="flex flex-col items-center gap-4 grow">
<h4
class="va-h4 text-center self-stretch overflow-hidden line-clamp-2 text-ellipsis"
>
{{ project.project_name }}
</h4>
<p>
<span class="text-[var(--va-secondary)]">Owner: </span>
<span>{{ project.project_owner.fullname }}</span>
</p>
<VaAvatarGroup
class="my-4"
:options="
project.team.map((user) => ({
label: user.fullname,
src: user.avatar,
fallbackText: user.fullname[0],
color: avatarColor(user.fullname),
}))
"
:max="5"
/>
<ProjectStatusBadge :status="project.status" />
</div>
<VaDivider class="my-6" />
<div class="flex justify-between">
<VaButton
preset="secondary"
icon="mso-edit"
color="secondary"
@click="$emit('edit', project)"
/>
<VaButton
preset="secondary"
icon="mso-delete"
color="danger"
@click="$emit('delete', project)"
/>
</div>
</VaCardContent>
</VaCard>
</VaInnerLoading>
<div
v-else
class="p-4 flex justify-center items-center text-[var(--va-secondary)]"
>
No projects
</div>
</template>

View File

@ -1,168 +0,0 @@
<script setup lang="ts">
import { PropType, computed } from "vue";
import { defineVaDataTableColumns } from "vuestic-ui";
import { Project } from "../types";
import UserAvatar from "../../users/widgets/UserAvatar.vue";
import ProjectStatusBadge from "../components/ProjectStatusBadge.vue";
import { Pagination, Sorting } from "../../../data/pages/projects";
import { useVModel } from "@vueuse/core";
const columns = defineVaDataTableColumns([
{ label: "Project name", key: "project_name", sortable: true },
{ label: "Project owner", key: "project_owner", sortable: true },
{ label: "Team", key: "team", sortable: true },
{ label: "Status", key: "status", sortable: true },
{ label: "Creation Date", key: "creation_date", sortable: true },
{ label: " ", key: "actions" },
]);
const props = defineProps({
projects: {
type: Array as PropType<Project[]>,
required: true,
},
loading: {
type: Boolean,
required: true,
},
sortBy: {
type: Object as PropType<Sorting["sortBy"]>,
required: true,
},
sortingOrder: {
type: Object as PropType<Sorting["sortingOrder"]>,
required: true,
},
pagination: {
type: Object as PropType<Pagination>,
required: true,
},
});
const emit = defineEmits<{
(event: "edit", project: Project): void;
(event: "delete", project: Project): void;
}>();
const avatarColor = (userName: string) => {
const colors = ["primary", "#FFD43A", "#ADFF00", "#262824", "danger"];
const index = userName.charCodeAt(0) % colors.length;
return colors[index];
};
const sortByVModel = useVModel(props, "sortBy", emit);
const sortingOrderVModel = useVModel(props, "sortingOrder", emit);
const totalPages = computed(() =>
Math.ceil(props.pagination.total / props.pagination.perPage),
);
</script>
<template>
<div>
<VaDataTable
v-model:sort-by="sortByVModel"
v-model:sorting-order="sortingOrderVModel"
:items="projects"
:columns="columns"
:loading="loading"
>
<template #cell(project_name)="{ rowData }">
<div class="ellipsis max-w-[230px] lg:max-w-[450px]">
{{ rowData.project_name }}
</div>
</template>
<template #cell(project_owner)="{ rowData }">
<div class="flex items-center gap-2 ellipsis max-w-[230px]">
<UserAvatar :user="rowData.project_owner" size="small" />
{{ rowData.project_owner.fullname }}
</div>
</template>
<template #cell(team)="{ rowData: project }">
<VaAvatarGroup
size="small"
:options="
(project as Project).team.map((user) => ({
label: user.fullname,
src: user.avatar,
fallbackText: user.fullname[0],
color: avatarColor(user.fullname),
}))
"
:max="5"
/>
</template>
<template #cell(status)="{ rowData: project }">
<ProjectStatusBadge :status="project.status" />
</template>
<template #cell(actions)="{ rowData: project }">
<div class="flex gap-2 justify-end">
<VaButton
preset="primary"
size="small"
color="primary"
icon="mso-edit"
aria-label="Edit project"
@click="$emit('edit', project as Project)"
/>
<VaButton
preset="primary"
size="small"
icon="mso-delete"
color="danger"
aria-label="Delete project"
@click="$emit('delete', project as Project)"
/>
</div>
</template>
</VaDataTable>
<div
class="flex flex-col-reverse md:flex-row gap-2 justify-between items-center py-2"
>
<div>
<b>{{ $props.pagination.total }} results.</b>
Results per page
<VaSelect
v-model="$props.pagination.perPage"
class="!w-20"
:options="[10, 50, 100]"
/>
</div>
<div v-if="totalPages > 1" class="flex">
<VaButton
preset="secondary"
icon="va-arrow-left"
aria-label="Previous page"
:disabled="$props.pagination.page === 1"
@click="$props.pagination.page--"
/>
<VaButton
class="mr-2"
preset="secondary"
icon="va-arrow-right"
aria-label="Next page"
:disabled="$props.pagination.page === totalPages"
@click="$props.pagination.page++"
/>
<VaPagination
v-model="$props.pagination.page"
buttons-preset="secondary"
:pages="totalPages"
:visible-pages="5"
:boundary-links="false"
:direction-links="false"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.va-data-table {
::v-deep(tbody .va-data-table__table-tr) {
border-bottom: 1px solid var(--va-background-border);
}
}
</style>

View File

@ -1,32 +0,0 @@
<template>
<div class="flex flex-col space-y-6 md:space-y-4">
<h1 class="page-title">Settings</h1>
<div class="flex flex-col p-4 space-y-4 bg-backgroundSecondary rounded-lg">
<h3 class="h3">Theme</h3>
<ThemeSwitcher />
</div>
<div class="flex flex-col p-4 space-y-4 bg-backgroundSecondary rounded-lg">
<h3 class="h3">General preferences</h3>
<LanguageSwitcher />
</div>
<VaAlert class="rounded-lg p-4 m-0" closeable color="info">
<template #icon>
<VaIcon size="26px" name="mso-notifications_active" />
</template>
<div class="flex flex-col space-y-2">
<p class="text-regularLarge font-bold">
Your notification settings are regrouped and simplified
</p>
<p class="text-regularMedium">
Your previous setting choices aren't changed.
</p>
</div>
</VaAlert>
<Notifications />
</div>
</template>
<script lang="ts" setup>
import LanguageSwitcher from "./language-switcher/LanguageSwitcher.vue";
import ThemeSwitcher from "./theme-switcher/ThemeSwitcher.vue";
import Notifications from "./notifications/Notifications.vue";
</script>

View File

@ -1,48 +0,0 @@
<template>
<div class="flex items-center justify-between">
<p>Language</p>
<div class="w-40">
<VaSelect v-model="model" :options="options" />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
type LanguageMap = Record<string, string>;
const { locale } = useI18n();
const languages: LanguageMap = {
english: "English",
spanish: "Spanish",
brazilian_portuguese: "Português",
simplified_chinese: "Simplified Chinese",
persian: "Persian",
};
const languageCodes: LanguageMap = {
gb: languages.english,
es: languages.spanish,
br: languages.brazilian_portuguese,
cn: languages.simplified_chinese,
ir: languages.persian,
};
const languageName: LanguageMap = Object.fromEntries(
Object.entries(languageCodes).map(([key, value]) => [value, key]),
);
const options = Object.values(languageCodes);
const model = computed({
get() {
return languageCodes[locale.value];
},
set(value) {
locale.value = languageName[value];
},
});
</script>

View File

@ -1,23 +0,0 @@
<template>
<div class="flex flex-col p-4 bg-backgroundSecondary rounded-lg">
<h3 class="h3 mb-6">Notifications you receive</h3>
<div
v-for="notification in notifications"
:key="notification.name"
class="group"
>
<div class="flex items-center justify-between overflow-x-hidden">
<p class="text-regularLarge">
{{ notification.name }}
</p>
<VaSwitch v-model="notification.isEnabled" size="small" />
</div>
<VaDivider class="py-4 group-last:hidden" />
</div>
</div>
</template>
<script lang="ts" setup>
import { useNotificationsStore } from "../../../stores/notifications";
const { notifications } = useNotificationsStore();
</script>

View File

@ -1,33 +0,0 @@
<template>
<VaButtonToggle
v-model="theme"
color="background-element"
border-color="background-element"
:options="options"
/>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useColors } from "vuestic-ui";
const { applyPreset, currentPresetName } = useColors();
const theme = computed({
get() {
return currentPresetName.value;
},
set(value) {
applyPreset(value);
},
});
const { t } = useI18n();
const options = [
{ label: t("buttonSelect.dark"), value: "dark" },
{ label: t("buttonSelect.light"), value: "light" },
];
</script>

View File

@ -1,132 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import UsersTable from "./widgets/UsersTable.vue";
import EditUserForm from "./widgets/EditUserForm.vue";
import { User } from "./types";
import { useUsers } from "./composables/useUsers";
import { useModal, useToast } from "vuestic-ui";
const doShowEditUserModal = ref(false);
const { users, isLoading, filters, sorting, pagination, ...usersApi } =
useUsers();
const userToEdit = ref<User | null>(null);
const showEditUserModal = (user: User) => {
userToEdit.value = user;
doShowEditUserModal.value = true;
};
const showAddUserModal = () => {
userToEdit.value = null;
doShowEditUserModal.value = true;
};
const { init: notify } = useToast();
const onUserSaved = async (user: User) => {
if (userToEdit.value) {
await usersApi.update(user);
notify({
message: `${user.fullname} has been updated`,
color: "success",
});
} else {
usersApi.add(user);
notify({
message: `${user.fullname} has been created`,
color: "success",
});
}
};
const onUserDelete = async (user: User) => {
await usersApi.remove(user);
notify({
message: `${user.fullname} has been deleted`,
color: "success",
});
};
const editFormRef = ref();
const { confirm } = useModal();
const beforeEditFormModalClose = async (hide: () => unknown) => {
if (editFormRef.value.isFormHasUnsavedChanges) {
const agreed = await confirm({
maxWidth: "380px",
message: "Form has unsaved changes. Are you sure you want to close it?",
size: "small",
});
if (agreed) {
hide();
}
} else {
hide();
}
};
</script>
<template>
<h1 class="page-title">Users</h1>
<VaCard>
<VaCardContent>
<div class="flex flex-col md:flex-row gap-2 mb-2 justify-between">
<div class="flex flex-col md:flex-row gap-2 justify-start">
<VaButtonToggle
v-model="filters.isActive"
color="background-element"
border-color="background-element"
:options="[
{ label: 'Active', value: true },
{ label: 'Inactive', value: false },
]"
/>
<VaInput v-model="filters.search" placeholder="Search">
<template #prependInner>
<VaIcon name="search" color="secondary" size="small" />
</template>
</VaInput>
</div>
<VaButton @click="showAddUserModal">Add User</VaButton>
</div>
<UsersTable
v-model:sort-by="sorting.sortBy"
v-model:sorting-order="sorting.sortingOrder"
:users="users"
:loading="isLoading"
:pagination="pagination"
@editUser="showEditUserModal"
@deleteUser="onUserDelete"
/>
</VaCardContent>
</VaCard>
<VaModal
v-slot="{ cancel, ok }"
v-model="doShowEditUserModal"
size="small"
mobile-fullscreen
close-button
hide-default-actions
:before-cancel="beforeEditFormModalClose"
>
<h1 class="va-h5">{{ userToEdit ? "Edit user" : "Add user" }}</h1>
<EditUserForm
ref="editFormRef"
:user="userToEdit"
:save-button-label="userToEdit ? 'Save' : 'Add'"
@close="cancel"
@save="
(user) => {
onUserSaved(user);
ok();
}
"
/>
</VaModal>
</template>

View File

@ -1,99 +0,0 @@
import { Ref, ref, unref, watch } from "vue";
import {
getUsers,
updateUser,
addUser,
removeUser,
type Filters,
Pagination,
Sorting,
} from "../../../data/pages/users";
import { User } from "../types";
import { watchIgnorable } from "@vueuse/core";
const makePaginationRef = () =>
ref<Pagination>({ page: 1, perPage: 10, total: 0 });
const makeSortingRef = () =>
ref<Sorting>({ sortBy: "fullname", sortingOrder: null });
const makeFiltersRef = () =>
ref<Partial<Filters>>({ isActive: true, search: "" });
export const useUsers = (options?: {
pagination?: Ref<Pagination>;
sorting?: Ref<Sorting>;
filters?: Ref<Partial<Filters>>;
}) => {
const isLoading = ref(false);
const users = ref<User[]>([]);
const {
filters = makeFiltersRef(),
sorting = makeSortingRef(),
pagination = makePaginationRef(),
} = options || {};
const fetch = async () => {
isLoading.value = true;
const { data, pagination: newPagination } = await getUsers({
...unref(filters),
...unref(sorting),
...unref(pagination),
});
users.value = data;
ignoreUpdates(() => {
pagination.value = newPagination;
});
isLoading.value = false;
};
const { ignoreUpdates } = watchIgnorable([pagination, sorting], fetch, {
deep: true,
});
watch(
filters,
() => {
// Reset pagination to first page when filters changed
pagination.value.page = 1;
fetch();
},
{ deep: true },
);
fetch();
return {
isLoading,
filters,
sorting,
pagination,
users,
fetch,
async add(user: User) {
isLoading.value = true;
await addUser(user);
await fetch();
isLoading.value = false;
},
async update(user: User) {
isLoading.value = true;
await updateUser(user);
await fetch();
isLoading.value = false;
},
async remove(user: User) {
isLoading.value = true;
await removeUser(user);
await fetch();
isLoading.value = false;
},
};
};

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