@@ -32,9 +32,14 @@ import {
3232} from "@sergeant/finyk-domain/domain/budget" ;
3333import { filterStatTransactions } from "@sergeant/finyk-domain/domain/transactions" ;
3434import { Skeleton } from "@shared/components/ui/Skeleton" ;
35+ import {
36+ DataState ,
37+ type DataStateQueryLike ,
38+ } from "@shared/components/ui/DataState" ;
3539import { safeReadStringLS , safeWriteLS } from "@shared/lib/storage/storage" ;
3640import { THEME_HEX } from "@shared/lib/ui/themeHex" ;
3741import { SyncStatusBadge } from "../components/SyncStatusBadge" ;
42+ import type { Transaction } from "@sergeant/finyk-domain/domain/types" ;
3843
3944import { FirstInsightBanner } from "./overview/FirstInsightBanner" ;
4045import { HeroCard } from "./overview/HeroCard" ;
@@ -348,18 +353,26 @@ export function Overview({
348353 [ subscriptionFlows , debtOutFlows , debtInFlows , currentYear , currentMonth ] ,
349354 ) ;
350355
351- if ( loadingTx && realTx . length === 0 ) {
352- return (
353- < div className = "flex-1 overflow-y-auto" >
354- < div className = "px-4 pt-4 page-tabbar-pad space-y-4 max-w-4xl mx-auto" >
355- < Skeleton className = "h-[168px] rounded-3xl" />
356- < Skeleton className = "h-[120px] opacity-80 rounded-2xl" />
357- < Skeleton className = "h-[110px] opacity-60 rounded-2xl" />
358- < Skeleton className = "h-[90px] opacity-40 rounded-2xl" />
359- </ div >
356+ // DataState contract: `data === undefined` triggers the skeleton slot.
357+ // We treat the very first month load (no realTx yet) as loading;
358+ // subsequent background refetches keep `data` defined so the page stays
359+ // visible while a stale-revalidate happens. Mirrors the prior
360+ // `if (loadingTx && realTx.length === 0)` early-return guard exactly.
361+ const overviewQuery : DataStateQueryLike < readonly Transaction [ ] > = {
362+ data : loadingTx && realTx . length === 0 ? undefined : realTx ,
363+ isLoading : loadingTx ,
364+ } ;
365+
366+ const overviewLoadingSkeleton = (
367+ < div className = "flex-1 overflow-y-auto" >
368+ < div className = "px-4 pt-4 page-tabbar-pad space-y-4 max-w-4xl mx-auto" >
369+ < Skeleton className = "h-[168px] rounded-3xl" />
370+ < Skeleton className = "h-[120px] opacity-80 rounded-2xl" />
371+ < Skeleton className = "h-[110px] opacity-60 rounded-2xl" />
372+ < Skeleton className = "h-[90px] opacity-40 rounded-2xl" />
360373 </ div >
361- ) ;
362- }
374+ </ div >
375+ ) ;
363376
364377 const recurringOutThisMonth = monthFlows
365378 . filter (
@@ -400,78 +413,82 @@ export function Overview({
400413 } ) ;
401414
402415 return (
403- < div className = "flex-1 overflow-y-auto overscroll-contain" >
404- < div className = "px-4 pt-4 page-tabbar-pad space-y-4 max-w-4xl mx-auto" >
405- { ( clientInfo ||
406- syncState ?. status === "error" ||
407- syncState ?. status === "loading" ||
408- monoError ) && (
409- < SyncStatusBadge
410- syncState = { syncState }
411- lastUpdated = { lastUpdated }
412- error = { monoError }
413- onRetry = { monoRefresh }
414- loading = { loadingTx }
415- />
416- ) }
417-
418- { showFirstInsight && hasAnyData && (
419- < FirstInsightBanner
420- onSetBudget = { handleSetBudgetFromInsight }
421- onDismiss = { dismissFirstInsight }
422- />
423- ) }
424-
425- < HeroCard
426- networth = { networth }
427- monoTotal = { monoTotal }
428- totalDebt = { totalDebt }
429- daysInMonth = { daysInMonth }
430- daysPassed = { daysPassed }
431- dayBudget = { dayBudget }
432- hasExpensePlan = { hasExpensePlan }
433- spendPlanRatio = { spendPlanRatio }
434- showBalance = { showBalance }
435- />
436-
437- < MonthPulseCard
438- dateLabel = { dateLabel }
439- daysPassed = { daysPassed }
440- spent = { spent }
441- income = { income }
442- showBalance = { showBalance }
443- showMonthForecast = { showMonthForecast }
444- projectedSpend = { projectedSpend }
445- hasExpensePlan = { hasExpensePlan }
446- spendPlanRatio = { spendPlanRatio }
447- planExpense = { planExpense }
448- forecastTrendPct = { forecastTrendPct }
449- forecastBarClass = { forecastBarClass }
450- recurringOutThisMonth = { recurringOutThisMonth }
451- recurringInThisMonth = { recurringInThisMonth }
452- unknownOutCount = { unknownOutCount }
453- />
454-
455- < NetworthSection networthHistory = { networthHistory } />
456-
457- < BudgetAlertsList
458- budgetAlerts = { budgetAlerts }
459- statTx = { statTx }
460- txCategories = { txCategories }
461- txSplits = { txSplits }
462- customCategories = { customCategories }
463- />
464-
465- < PlannedFlowsCard
466- plannedFlows = { plannedFlows }
467- onNavigate = { onNavigate ?? ( ( ) => { } ) }
468- showBalance = { showBalance }
469- />
470-
471- { loadingTx && (
472- < p className = "text-center text-xs text-subtle py-4" > Оновлення…</ p >
473- ) }
474- </ div >
475- </ div >
416+ < DataState query = { overviewQuery } skeleton = { overviewLoadingSkeleton } >
417+ { ( ) => (
418+ < div className = "flex-1 overflow-y-auto overscroll-contain" >
419+ < div className = "px-4 pt-4 page-tabbar-pad space-y-4 max-w-4xl mx-auto" >
420+ { ( clientInfo ||
421+ syncState ?. status === "error" ||
422+ syncState ?. status === "loading" ||
423+ monoError ) && (
424+ < SyncStatusBadge
425+ syncState = { syncState }
426+ lastUpdated = { lastUpdated }
427+ error = { monoError }
428+ onRetry = { monoRefresh }
429+ loading = { loadingTx }
430+ />
431+ ) }
432+
433+ { showFirstInsight && hasAnyData && (
434+ < FirstInsightBanner
435+ onSetBudget = { handleSetBudgetFromInsight }
436+ onDismiss = { dismissFirstInsight }
437+ />
438+ ) }
439+
440+ < HeroCard
441+ networth = { networth }
442+ monoTotal = { monoTotal }
443+ totalDebt = { totalDebt }
444+ daysInMonth = { daysInMonth }
445+ daysPassed = { daysPassed }
446+ dayBudget = { dayBudget }
447+ hasExpensePlan = { hasExpensePlan }
448+ spendPlanRatio = { spendPlanRatio }
449+ showBalance = { showBalance }
450+ />
451+
452+ < MonthPulseCard
453+ dateLabel = { dateLabel }
454+ daysPassed = { daysPassed }
455+ spent = { spent }
456+ income = { income }
457+ showBalance = { showBalance }
458+ showMonthForecast = { showMonthForecast }
459+ projectedSpend = { projectedSpend }
460+ hasExpensePlan = { hasExpensePlan }
461+ spendPlanRatio = { spendPlanRatio }
462+ planExpense = { planExpense }
463+ forecastTrendPct = { forecastTrendPct }
464+ forecastBarClass = { forecastBarClass }
465+ recurringOutThisMonth = { recurringOutThisMonth }
466+ recurringInThisMonth = { recurringInThisMonth }
467+ unknownOutCount = { unknownOutCount }
468+ />
469+
470+ < NetworthSection networthHistory = { networthHistory } />
471+
472+ < BudgetAlertsList
473+ budgetAlerts = { budgetAlerts }
474+ statTx = { statTx }
475+ txCategories = { txCategories }
476+ txSplits = { txSplits }
477+ customCategories = { customCategories }
478+ />
479+
480+ < PlannedFlowsCard
481+ plannedFlows = { plannedFlows }
482+ onNavigate = { onNavigate ?? ( ( ) => { } ) }
483+ showBalance = { showBalance }
484+ />
485+
486+ { loadingTx && (
487+ < p className = "text-center text-xs text-subtle py-4" > Оновлення…</ p >
488+ ) }
489+ </ div >
490+ </ div >
491+ ) }
492+ </ DataState >
476493 ) ;
477494}
0 commit comments