Skip to content

Commit

Permalink
Merge pull request #293 from letehaha/fix/dashboard-widget-data-flick…
Browse files Browse the repository at this point in the history
…ering

feat: Show loading state when dashboard widget data is fetching
  • Loading branch information
letehaha committed May 15, 2024
2 parents cc32b51 + 4ca97e0 commit b862ca8
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 188 deletions.
169 changes: 78 additions & 91 deletions src/components/widgets/balance-trend.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
<template>
<WidgetWrapper class="balance-trend-widget" title="Balance trend">
<div class="balance-trend-widget__details">
<div class="balance-trend-widget__details-titles">
<div
class="balance-trend-widget__details-title balance-trend-widget__details-title--today"
>
Today
</div>
<div class="balance-trend-widget__details-title">
vs previous period
</div>
<WidgetWrapper title="Balance trend" :is-fetching="isWidgetDataFetching">
<div>
<div class="flex items-center justify-between mb-1 text-xs">
<div class="font-medium tracking-tight uppercase">Today</div>
<div class="tracking-tight">vs previous period</div>
</div>
<div class="balance-trend-widget__details-values">
<div class="balance-trend-widget__today-balance">

<div class="flex items-center justify-between">
<div class="text-lg font-bold tracking-wider">
{{ formatBaseCurrency(todayBalance) }}
</div>
<div
class="balance-trend-widget__diff"
:class="{
'balance-trend-widget__diff--positive': balancesDiff > 0,
'balance-trend-widget__diff--negative': balancesDiff < 0,
'text-[var(--app-success)]': balancesDiff < 0,
'text-[var(--app-error)]': balancesDiff > 0,
}"
>
{{ `${balancesDiff}%` }}
Expand All @@ -29,7 +23,6 @@

<highcharts
v-node-resize-observer="{ callback: onChartResize }"
class="balance-trend-widget__chart"
:options="chartOptions"
/>
</WidgetWrapper>
Expand Down Expand Up @@ -76,65 +69,96 @@ defineOptions({
name: "balance-trend-widget",
});
const props = withDefaults(
defineProps<{
selectedPeriod?: { from: Date; to: Date };
}>(),
{
selectedPeriod: () => ({
from: startOfMonth(new Date()),
to: new Date(),
}),
},
);
const props = defineProps<{
selectedPeriod: { from: Date; to: Date };
}>();
const periodFrom = ref(new Date().getTime());
const currentChartWidth = ref(0);
const { formatBaseCurrency } = useFormatCurrency();
const { baseCurrency } = storeToRefs(useCurrenciesStore());
const { buildAreaChartConfig } = useHighcharts();
watch(
() => props.selectedPeriod.from,
() => {
periodFrom.value = props.selectedPeriod.from.getTime();
// We store actual and prev period separately, so when new data is loading, we
// can still show the old period, to avoid UI flickering
const actualDataPeriod = ref(props.selectedPeriod);
const prevDataPeriod = ref(props.selectedPeriod);
const periodQueryKey = computed(() => props.selectedPeriod.from.getTime());
const { data: balanceHistory, isFetching: isBalanceHistoryFetching } = useQuery(
{
queryKey: [...VUE_QUERY_CACHE_KEYS.widgetBalanceTrend, periodQueryKey],
queryFn: () => loadBalanceTrendData(props.selectedPeriod),
staleTime: Infinity,
placeholderData: (prevData) => prevData,
},
);
const { data: balanceHistory } = useQuery({
queryKey: [...VUE_QUERY_CACHE_KEYS.widgetBalanceTrend, periodFrom],
queryFn: () => loadBalanceTrendData(props.selectedPeriod),
staleTime: Infinity,
});
const { data: todayBalance } = useQuery({
queryKey: [...VUE_QUERY_CACHE_KEYS.widgetBalanceTotalBalance, periodFrom],
const { data: todayBalance, isFetching: isTodayBalanceFetching } = useQuery({
queryKey: [...VUE_QUERY_CACHE_KEYS.widgetBalanceTotalBalance, periodQueryKey],
queryFn: () => getTotalBalance({ date: props.selectedPeriod.to }),
placeholderData: 0,
staleTime: Infinity,
});
const { data: previousBalance } = useQuery({
queryKey: [...VUE_QUERY_CACHE_KEYS.widgetBalancePreviousBalance, periodFrom],
queryFn: () =>
getTotalBalance({
date: endOfMonth(subMonths(props.selectedPeriod.to, 1)),
}),
placeholderData: 0,
placeholderData: (prevData) => prevData || 0,
staleTime: Infinity,
});
const { data: previousBalance, isFetching: isPreviousBalanceFetching } =
useQuery({
queryKey: [
...VUE_QUERY_CACHE_KEYS.widgetBalancePreviousBalance,
periodQueryKey,
],
queryFn: () =>
getTotalBalance({
date: endOfMonth(subMonths(props.selectedPeriod.to, 1)),
}),
placeholderData: (prevData) => prevData || 0,
staleTime: Infinity,
});
const isWidgetDataFetching = computed(
() =>
isBalanceHistoryFetching.value ||
isTodayBalanceFetching.value ||
isPreviousBalanceFetching.value,
);
// On each "selectedPeriod" change we immediately set it as "actualDataPeriod"
// but if "isWidgetDataFetching" is also triggered, means we started loading new
// data, then we need to actually reassing "actualDataPeriod" to be as "prevDataPeriod",
// so there won't be any data flickering. Once data is fully loaded, we assign
// actual values to both of them
watch(
() => props.selectedPeriod,
(value) => {
actualDataPeriod.value = value;
},
);
watch(
isWidgetDataFetching,
(value) => {
if (value) {
actualDataPeriod.value = prevDataPeriod.value;
} else {
actualDataPeriod.value = props.selectedPeriod;
prevDataPeriod.value = props.selectedPeriod;
}
},
{ immediate: true },
);
const chartOptions = computed(() => {
const pixelsPerTick = 120;
const ticksAmount = currentChartWidth.value
? Math.round(currentChartWidth.value / pixelsPerTick)
: 5;
const fromDate = actualDataPeriod.value.from;
const toDate = actualDataPeriod.value.to;
const config = buildAreaChartConfig({
chart: {
height: 220,
marginTop: 20,
},
xAxis: {
tickPositions: generateDateSteps(ticksAmount, props.selectedPeriod.from),
tickPositions: generateDateSteps(ticksAmount, fromDate),
},
yAxis: {
tickAmount: 5,
Expand All @@ -158,15 +182,9 @@ const chartOptions = computed(() => {
]),
// fill remaining days with `null` so chart will be rendered for all
// days in the month
...Array(
getDaysInMonth(props.selectedPeriod.to) -
props.selectedPeriod.to.getDate(),
)
...Array(getDaysInMonth(toDate) - toDate.getDate())
.fill([])
.map((_, i) => [
addDays(props.selectedPeriod.to, i + 1).getTime(),
null,
]),
.map((_, i) => [addDays(toDate, i + 1).getTime(), null]),
],
},
],
Expand All @@ -188,34 +206,3 @@ const onChartResize = (entries: ResizeObserverEntry[]) => {
currentChartWidth.value = entry.contentRect.width;
};
</script>

<style lang="scss">
.balance-trend-widget__details-titles,
.balance-trend-widget__details-values {
display: flex;
justify-content: space-between;
align-items: center;
}
.balance-trend-widget__details-titles {
font-size: 12px;
margin-bottom: 4px;
}
.balance-trend-widget__details-title {
letter-spacing: -0.3px;
}
.balance-trend-widget__details-title--today {
text-transform: uppercase;
font-weight: 500;
}
.balance-trend-widget__today-balance {
font-size: 18px;
letter-spacing: 1px;
font-weight: 700;
}
.balance-trend-widget__diff--positive {
color: var(--app-success);
}
.balance-trend-widget__diff--negative {
color: var(--app-error);
}
</style>
16 changes: 14 additions & 2 deletions src/components/widgets/components/widget-wrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@
>
<CardHeader>
<slot name="header">
<div class="flex items-center justify-between">
<div
class="grid grid-cols-[minmax(0,1fr),max-content,20px] gap-1 items-center justify-between"
>
<h3>
{{ title }}
</h3>
<slot name="action" />

<div>
<slot name="action" />
</div>

<template v-if="isFetching">
<Loader2Icon class="text-white opacity-50 size-5 animate-spin" />
</template>
</div>
</slot>
</CardHeader>
Expand All @@ -23,15 +32,18 @@
</template>

<script setup lang="ts">
import { Loader2Icon } from "lucide-vue-next";
import { Card, CardContent, CardHeader } from "@/components/lib/ui/card";
withDefaults(
defineProps<{
title?: string;
higher?: boolean;
isFetching?: boolean;
}>(),
{
title: undefined,
isFetching: false,
},
);
</script>
Loading

0 comments on commit b862ca8

Please sign in to comment.