delete needless code
Gitea Actions Demo / build_and_push (push) Successful in 3m31s
Details
Gitea Actions Demo / build_and_push (push) Successful in 3m31s
Details
This commit is contained in:
parent
ba9bc6420f
commit
dcc16aa373
|
|
@ -1,14 +0,0 @@
|
|||
import Typography from "./Typography.vue";
|
||||
|
||||
export default {
|
||||
title: "Typography",
|
||||
component: Typography,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export const Default = () => ({
|
||||
components: { Typography },
|
||||
template: `
|
||||
<Typography/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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 it’s 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 it’s own
|
||||
satellite, the moon. When you think about it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<h3>Display 3 Heading</h3>
|
||||
<p>
|
||||
Let’s 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. It’s 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 it’s 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 it’s own satellite, the moon. When you think
|
||||
about it.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<pre class="code-snippet">
|
||||
<p class=“code-snippet”>
|
||||
This is a wonderful example.
|
||||
<a href=“#” onClick=“”>Read more</a>
|
||||
</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 it’s 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 it’s 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 it’s 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 it’s 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: Let’s 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 it’s 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -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")),
|
||||
),
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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",
|
||||
];
|
||||
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
43198
src/data/geo.json
43198
src/data/geo.json
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 }} /upload</p>
|
||||
</div>
|
||||
<div class="md:w-48">
|
||||
<template v-if="plan.priceMonth">
|
||||
<p class="mb-1">{{ plan.priceMonth }} /month</p>
|
||||
<p>{{ plan.priceYear }} /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>
|
||||
|
|
@ -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
|
||||
}} /{{ 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 }} /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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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. It’s 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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 |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
/>`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import BillingAddressList from "./BillingAddressList.vue";
|
||||
|
||||
export default {
|
||||
title: "BillingAddressList",
|
||||
component: BillingAddressList,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export const Default = () => ({
|
||||
components: { BillingAddressList },
|
||||
template: `
|
||||
<BillingAddressList/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
/>`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import PaymentCardList from "./PaymentCardList.vue";
|
||||
|
||||
export default {
|
||||
title: "PaymentCardList",
|
||||
component: PaymentCardList,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export const Default = () => ({
|
||||
components: { PaymentCardList },
|
||||
template: `
|
||||
<PaymentCardList/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"/>
|
||||
`,
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 })),
|
||||
},
|
||||
];
|
||||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
Loading…
Reference in New Issue