refactor(miniapp): split home and balances screens

This commit is contained in:
2026-03-11 18:48:29 +04:00
parent ebd12eb46e
commit 63f31a46db
3 changed files with 393 additions and 278 deletions

View File

@@ -40,11 +40,11 @@ import { HeroBanner } from './components/layout/hero-banner'
import { NavigationTabs } from './components/layout/navigation-tabs'
import { ProfileCard } from './components/layout/profile-card'
import { TopBar } from './components/layout/top-bar'
import { FinanceSummaryCards } from './components/finance/finance-summary-cards'
import { FinanceVisuals } from './components/finance/finance-visuals'
import { BlockedState } from './components/session/blocked-state'
import { LoadingState } from './components/session/loading-state'
import { OnboardingState } from './components/session/onboarding-state'
import { BalancesScreen } from './screens/balances-screen'
import { HomeScreen } from './screens/home-screen'
import {
demoAdminSettings,
demoCycleState,
@@ -1854,122 +1854,17 @@ function App() {
switch (activeNav()) {
case 'balances':
return (
<div class="balance-list">
<ShowDashboard
dashboard={dashboard()}
fallback={<p>{copy().emptyDashboard}</p>}
render={(data) => (
<>
{currentMemberLine() ? (
<article class="balance-item balance-item--accent">
<header>
<strong>{copy().yourBalanceTitle}</strong>
<span>
{currentMemberLine()!.netDueMajor} {data.currency}
</span>
</header>
<p>{copy().yourBalanceBody}</p>
<div class="balance-breakdown">
<div class="stat-card">
<span>{copy().baseDue}</span>
<strong>
{memberBaseDueMajor(currentMemberLine()!)} {data.currency}
</strong>
</div>
<div class="stat-card">
<span>{copy().shareOffset}</span>
<strong>
{currentMemberLine()!.purchaseOffsetMajor} {data.currency}
</strong>
</div>
<div class="stat-card">
<span>{copy().finalDue}</span>
<strong>
{currentMemberLine()!.netDueMajor} {data.currency}
</strong>
</div>
<div class="stat-card">
<span>{copy().paidLabel}</span>
<strong>
{currentMemberLine()!.paidMajor} {data.currency}
</strong>
</div>
<div class="stat-card">
<span>{copy().remainingLabel}</span>
<strong>
{currentMemberLine()!.remainingMajor} {data.currency}
</strong>
</div>
</div>
</article>
) : null}
<div class="home-grid home-grid--summary">
<FinanceSummaryCards
dashboard={data}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
labels={{
remaining: copy().remainingLabel,
rent: copy().shareRent,
utilities: copy().shareUtilities,
purchases: copy().purchasesTitle
}}
/>
</div>
<FinanceVisuals
dashboard={data}
memberVisuals={memberBalanceVisuals()}
purchaseChart={purchaseInvestmentChart()}
remainingClass={memberRemainingClass}
labels={{
financeVisualsTitle: copy().financeVisualsTitle,
financeVisualsBody: copy().financeVisualsBody,
membersCount: copy().membersCount,
purchaseInvestmentsTitle: copy().purchaseInvestmentsTitle,
purchaseInvestmentsBody: copy().purchaseInvestmentsBody,
purchaseInvestmentsEmpty: copy().purchaseInvestmentsEmpty,
purchaseTotalLabel: copy().purchaseTotalLabel,
purchaseShareLabel: copy().purchaseShareLabel
}}
/>
<article class="balance-item">
<header>
<strong>{copy().householdBalancesTitle}</strong>
</header>
<p>{copy().householdBalancesBody}</p>
</article>
{data.members.map((member) => (
<article class="balance-item">
<header>
<strong>{member.displayName}</strong>
<span>
{member.remainingMajor} {data.currency}
</span>
</header>
<p>
{copy().baseDue}: {memberBaseDueMajor(member)} {data.currency}
</p>
<p>
{copy().shareRent}: {member.rentShareMajor} {data.currency}
</p>
<p>
{copy().shareUtilities}: {member.utilityShareMajor} {data.currency}
</p>
<p>
{copy().shareOffset}: {member.purchaseOffsetMajor} {data.currency}
</p>
<p>
{copy().paidLabel}: {member.paidMajor} {data.currency}
</p>
<p class={`balance-status ${memberRemainingClass(member)}`}>
{copy().remainingLabel}: {member.remainingMajor} {data.currency}
</p>
</article>
))}
</>
)}
/>
</div>
<BalancesScreen
copy={copy()}
dashboard={dashboard()}
currentMemberLine={currentMemberLine()}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
memberBalanceVisuals={memberBalanceVisuals()}
purchaseChart={purchaseInvestmentChart()}
memberBaseDueMajor={memberBaseDueMajor}
memberRemainingClass={memberRemainingClass}
/>
)
case 'ledger':
return (
@@ -3583,166 +3478,22 @@ function App() {
)
default:
return (
<div class="home-grid home-grid--summary">
<ShowDashboard
dashboard={dashboard()}
fallback={
<>
<article class="stat-card">
<span>{copy().remainingLabel}</span>
<strong></strong>
</article>
<article class="stat-card">
<span>{copy().shareRent}</span>
<strong></strong>
</article>
<article class="stat-card">
<span>{copy().shareUtilities}</span>
<strong></strong>
</article>
<article class="stat-card">
<span>{copy().purchasesTitle}</span>
<strong></strong>
</article>
</>
}
render={(data) => (
<FinanceSummaryCards
dashboard={data}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
labels={{
remaining: copy().remainingLabel,
rent: copy().shareRent,
utilities: copy().shareUtilities,
purchases: copy().purchasesTitle
}}
/>
)}
/>
{readySession()?.member.isAdmin ? (
<article class="stat-card">
<span>{copy().pendingRequests}</span>
<strong>{String(pendingMembers().length)}</strong>
</article>
) : null}
{currentMemberLine() ? (
<article class="balance-item balance-item--accent">
<header>
<strong>{copy().yourBalanceTitle}</strong>
<span>
{currentMemberLine()!.remainingMajor} {dashboard()?.currency ?? ''}
</span>
</header>
<p>{copy().yourBalanceBody}</p>
<ShowDashboard
dashboard={dashboard()}
fallback={null}
render={(data) => (
<p>
{copy().shareRent}: {data.rentSourceAmountMajor} {data.rentSourceCurrency}
{data.rentSourceCurrency !== data.currency
? ` -> ${data.rentDisplayAmountMajor} ${data.currency}`
: ''}
</p>
)}
/>
<div class="balance-breakdown">
<div class="stat-card">
<span>{copy().baseDue}</span>
<strong>
{memberBaseDueMajor(currentMemberLine()!)} {dashboard()?.currency ?? ''}
</strong>
</div>
<div class="stat-card">
<span>{copy().shareOffset}</span>
<strong>
{currentMemberLine()!.purchaseOffsetMajor} {dashboard()?.currency ?? ''}
</strong>
</div>
<div class="stat-card">
<span>{copy().finalDue}</span>
<strong>
{currentMemberLine()!.netDueMajor} {dashboard()?.currency ?? ''}
</strong>
</div>
<div class="stat-card">
<span>{copy().paidLabel}</span>
<strong>
{currentMemberLine()!.paidMajor} {dashboard()?.currency ?? ''}
</strong>
</div>
<div class="stat-card">
<span>{copy().remainingLabel}</span>
<strong>
{currentMemberLine()!.remainingMajor} {dashboard()?.currency ?? ''}
</strong>
</div>
</div>
</article>
) : (
<article class="balance-item">
<header>
<strong>{copy().overviewTitle}</strong>
</header>
<p>{copy().overviewBody}</p>
</article>
)}
<ShowDashboard
dashboard={dashboard()}
fallback={null}
render={(data) => (
<FinanceVisuals
dashboard={data}
memberVisuals={memberBalanceVisuals()}
purchaseChart={purchaseInvestmentChart()}
remainingClass={memberRemainingClass}
labels={{
financeVisualsTitle: copy().financeVisualsTitle,
financeVisualsBody: copy().financeVisualsBody,
membersCount: copy().membersCount,
purchaseInvestmentsTitle: copy().purchaseInvestmentsTitle,
purchaseInvestmentsBody: copy().purchaseInvestmentsBody,
purchaseInvestmentsEmpty: copy().purchaseInvestmentsEmpty,
purchaseTotalLabel: copy().purchaseTotalLabel,
purchaseShareLabel: copy().purchaseShareLabel
}}
/>
)}
/>
<article class="balance-item balance-item--wide">
<header>
<strong>{copy().latestActivityTitle}</strong>
</header>
<ShowDashboard
dashboard={dashboard()}
fallback={<p>{copy().latestActivityEmpty}</p>}
render={(data) =>
data.ledger.length === 0 ? (
<p>{copy().latestActivityEmpty}</p>
) : (
<div class="activity-list">
{data.ledger.slice(0, 3).map((entry) => (
<article class="activity-row">
<header>
<strong>{ledgerTitle(entry)}</strong>
<span>{ledgerPrimaryAmount(entry)}</span>
</header>
<Show when={ledgerSecondaryAmount(entry)}>
{(secondary) => <p>{secondary()}</p>}
</Show>
<p>{entry.actorDisplayName ?? copy().ledgerActorFallback}</p>
</article>
))}
</div>
)
}
/>
</article>
</div>
<HomeScreen
copy={copy()}
dashboard={dashboard()}
readyIsAdmin={Boolean(readySession()?.member.isAdmin)}
pendingMembersCount={pendingMembers().length}
currentMemberLine={currentMemberLine()}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
memberBalanceVisuals={memberBalanceVisuals()}
purchaseChart={purchaseInvestmentChart()}
memberBaseDueMajor={memberBaseDueMajor}
memberRemainingClass={memberRemainingClass}
ledgerTitle={ledgerTitle}
ledgerPrimaryAmount={ledgerPrimaryAmount}
ledgerSecondaryAmount={ledgerSecondaryAmount}
/>
)
}
}

View File

@@ -0,0 +1,167 @@
import { For, Show } from 'solid-js'
import { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
import { FinanceVisuals } from '../components/finance/finance-visuals'
import type { MiniAppDashboard } from '../miniapp-api'
type Props = {
copy: Record<string, string | undefined>
dashboard: MiniAppDashboard | null
currentMemberLine: MiniAppDashboard['members'][number] | null
utilityTotalMajor: string
purchaseTotalMajor: string
memberBalanceVisuals: {
member: MiniAppDashboard['members'][number]
totalMinor: bigint
barWidthPercent: number
segments: {
key: string
label: string
amountMajor: string
amountMinor: bigint
widthPercent: number
}[]
}[]
purchaseChart: {
totalMajor: string
slices: {
key: string
label: string
amountMajor: string
color: string
percentage: number
dasharray: string
dashoffset: string
}[]
}
memberBaseDueMajor: (member: MiniAppDashboard['members'][number]) => string
memberRemainingClass: (member: MiniAppDashboard['members'][number]) => string
}
export function BalancesScreen(props: Props) {
if (!props.dashboard) {
return (
<div class="balance-list">
<p>{props.copy.emptyDashboard ?? ''}</p>
</div>
)
}
return (
<div class="balance-list">
<Show when={props.currentMemberLine}>
{(member) => (
<article class="balance-item balance-item--accent">
<header>
<strong>{props.copy.yourBalanceTitle ?? ''}</strong>
<span>
{member().netDueMajor} {props.dashboard!.currency}
</span>
</header>
<p>{props.copy.yourBalanceBody ?? ''}</p>
<div class="balance-breakdown">
<article class="stat-card">
<span>{props.copy.baseDue ?? ''}</span>
<strong>
{props.memberBaseDueMajor(member())} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.shareOffset ?? ''}</span>
<strong>
{member().purchaseOffsetMajor} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.finalDue ?? ''}</span>
<strong>
{member().netDueMajor} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.paidLabel ?? ''}</span>
<strong>
{member().paidMajor} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.remainingLabel ?? ''}</span>
<strong>
{member().remainingMajor} {props.dashboard!.currency}
</strong>
</article>
</div>
</article>
)}
</Show>
<div class="home-grid home-grid--summary">
<FinanceSummaryCards
dashboard={props.dashboard}
utilityTotalMajor={props.utilityTotalMajor}
purchaseTotalMajor={props.purchaseTotalMajor}
labels={{
remaining: props.copy.remainingLabel ?? '',
rent: props.copy.shareRent ?? '',
utilities: props.copy.shareUtilities ?? '',
purchases: props.copy.purchasesTitle ?? ''
}}
/>
</div>
<FinanceVisuals
dashboard={props.dashboard}
memberVisuals={props.memberBalanceVisuals}
purchaseChart={props.purchaseChart}
remainingClass={props.memberRemainingClass}
labels={{
financeVisualsTitle: props.copy.financeVisualsTitle ?? '',
financeVisualsBody: props.copy.financeVisualsBody ?? '',
membersCount: props.copy.membersCount ?? '',
purchaseInvestmentsTitle: props.copy.purchaseInvestmentsTitle ?? '',
purchaseInvestmentsBody: props.copy.purchaseInvestmentsBody ?? '',
purchaseInvestmentsEmpty: props.copy.purchaseInvestmentsEmpty ?? '',
purchaseTotalLabel: props.copy.purchaseTotalLabel ?? '',
purchaseShareLabel: props.copy.purchaseShareLabel ?? ''
}}
/>
<article class="balance-item">
<header>
<strong>{props.copy.householdBalancesTitle ?? ''}</strong>
</header>
<p>{props.copy.householdBalancesBody ?? ''}</p>
</article>
<For each={props.dashboard.members}>
{(member) => (
<article class="balance-item">
<header>
<strong>{member.displayName}</strong>
<span>
{member.remainingMajor} {props.dashboard!.currency}
</span>
</header>
<p>
{props.copy.baseDue ?? ''}: {props.memberBaseDueMajor(member)}{' '}
{props.dashboard!.currency}
</p>
<p>
{props.copy.shareRent ?? ''}: {member.rentShareMajor} {props.dashboard!.currency}
</p>
<p>
{props.copy.shareUtilities ?? ''}: {member.utilityShareMajor}{' '}
{props.dashboard!.currency}
</p>
<p>
{props.copy.shareOffset ?? ''}: {member.purchaseOffsetMajor}{' '}
{props.dashboard!.currency}
</p>
<p>
{props.copy.paidLabel ?? ''}: {member.paidMajor} {props.dashboard!.currency}
</p>
<p class={`balance-status ${props.memberRemainingClass(member)}`}>
{props.copy.remainingLabel ?? ''}: {member.remainingMajor} {props.dashboard!.currency}
</p>
</article>
)}
</For>
</div>
)
}

View File

@@ -0,0 +1,197 @@
import { For, Show } from 'solid-js'
import { FinanceSummaryCards } from '../components/finance/finance-summary-cards'
import { FinanceVisuals } from '../components/finance/finance-visuals'
import type { MiniAppDashboard } from '../miniapp-api'
type Props = {
copy: Record<string, string | undefined>
dashboard: MiniAppDashboard | null
readyIsAdmin: boolean
pendingMembersCount: number
currentMemberLine: MiniAppDashboard['members'][number] | null
utilityTotalMajor: string
purchaseTotalMajor: string
memberBalanceVisuals: {
member: MiniAppDashboard['members'][number]
totalMinor: bigint
barWidthPercent: number
segments: {
key: string
label: string
amountMajor: string
amountMinor: bigint
widthPercent: number
}[]
}[]
purchaseChart: {
totalMajor: string
slices: {
key: string
label: string
amountMajor: string
color: string
percentage: number
dasharray: string
dashoffset: string
}[]
}
memberBaseDueMajor: (member: MiniAppDashboard['members'][number]) => string
memberRemainingClass: (member: MiniAppDashboard['members'][number]) => string
ledgerTitle: (entry: MiniAppDashboard['ledger'][number]) => string
ledgerPrimaryAmount: (entry: MiniAppDashboard['ledger'][number]) => string
ledgerSecondaryAmount: (entry: MiniAppDashboard['ledger'][number]) => string | null
}
export function HomeScreen(props: Props) {
if (!props.dashboard) {
return (
<div class="home-grid home-grid--summary">
<article class="stat-card">
<span>{props.copy.remainingLabel ?? ''}</span>
<strong></strong>
</article>
<article class="stat-card">
<span>{props.copy.shareRent ?? ''}</span>
<strong></strong>
</article>
<article class="stat-card">
<span>{props.copy.shareUtilities ?? ''}</span>
<strong></strong>
</article>
<article class="stat-card">
<span>{props.copy.purchasesTitle ?? ''}</span>
<strong></strong>
</article>
</div>
)
}
return (
<div class="home-grid home-grid--summary">
<FinanceSummaryCards
dashboard={props.dashboard}
utilityTotalMajor={props.utilityTotalMajor}
purchaseTotalMajor={props.purchaseTotalMajor}
labels={{
remaining: props.copy.remainingLabel ?? '',
rent: props.copy.shareRent ?? '',
utilities: props.copy.shareUtilities ?? '',
purchases: props.copy.purchasesTitle ?? ''
}}
/>
<Show when={props.readyIsAdmin}>
<article class="stat-card">
<span>{props.copy.pendingRequests ?? ''}</span>
<strong>{String(props.pendingMembersCount)}</strong>
</article>
</Show>
<Show
when={props.currentMemberLine}
fallback={
<article class="balance-item">
<header>
<strong>{props.copy.overviewTitle ?? ''}</strong>
</header>
<p>{props.copy.overviewBody ?? ''}</p>
</article>
}
>
{(member) => (
<article class="balance-item balance-item--accent">
<header>
<strong>{props.copy.yourBalanceTitle ?? ''}</strong>
<span>
{member().remainingMajor} {props.dashboard!.currency}
</span>
</header>
<p>{props.copy.yourBalanceBody ?? ''}</p>
<p>
{props.copy.shareRent ?? ''}: {props.dashboard!.rentSourceAmountMajor}{' '}
{props.dashboard!.rentSourceCurrency}
{props.dashboard!.rentSourceCurrency !== props.dashboard!.currency
? ` -> ${props.dashboard!.rentDisplayAmountMajor} ${props.dashboard!.currency}`
: ''}
</p>
<div class="balance-breakdown">
<article class="stat-card">
<span>{props.copy.baseDue ?? ''}</span>
<strong>
{props.memberBaseDueMajor(member())} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.shareOffset ?? ''}</span>
<strong>
{member().purchaseOffsetMajor} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.finalDue ?? ''}</span>
<strong>
{member().netDueMajor} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.paidLabel ?? ''}</span>
<strong>
{member().paidMajor} {props.dashboard!.currency}
</strong>
</article>
<article class="stat-card">
<span>{props.copy.remainingLabel ?? ''}</span>
<strong>
{member().remainingMajor} {props.dashboard!.currency}
</strong>
</article>
</div>
</article>
)}
</Show>
<FinanceVisuals
dashboard={props.dashboard}
memberVisuals={props.memberBalanceVisuals}
purchaseChart={props.purchaseChart}
remainingClass={props.memberRemainingClass}
labels={{
financeVisualsTitle: props.copy.financeVisualsTitle ?? '',
financeVisualsBody: props.copy.financeVisualsBody ?? '',
membersCount: props.copy.membersCount ?? '',
purchaseInvestmentsTitle: props.copy.purchaseInvestmentsTitle ?? '',
purchaseInvestmentsBody: props.copy.purchaseInvestmentsBody ?? '',
purchaseInvestmentsEmpty: props.copy.purchaseInvestmentsEmpty ?? '',
purchaseTotalLabel: props.copy.purchaseTotalLabel ?? '',
purchaseShareLabel: props.copy.purchaseShareLabel ?? ''
}}
/>
<article class="balance-item balance-item--wide">
<header>
<strong>{props.copy.latestActivityTitle ?? ''}</strong>
</header>
{props.dashboard.ledger.length === 0 ? (
<p>{props.copy.latestActivityEmpty ?? ''}</p>
) : (
<div class="activity-list">
<For each={props.dashboard.ledger.slice(0, 3)}>
{(entry) => (
<article class="activity-row">
<header>
<strong>{props.ledgerTitle(entry)}</strong>
<span>{props.ledgerPrimaryAmount(entry)}</span>
</header>
<Show when={props.ledgerSecondaryAmount(entry)}>
{(secondary) => <p>{secondary()}</p>}
</Show>
<p>{entry.actorDisplayName ?? props.copy.ledgerActorFallback ?? ''}</p>
</article>
)}
</For>
</div>
)}
</article>
</div>
)
}