refactor(miniapp): simplify dashboard layout and controls

This commit is contained in:
2026-03-11 20:57:44 +04:00
parent e36d3b5d66
commit 523b5144d8
7 changed files with 148 additions and 267 deletions

View File

@@ -1,4 +1,4 @@
import { Match, Switch, createMemo, createSignal, onMount } from 'solid-js'
import { Match, Show, Switch, createMemo, createSignal, onMount } from 'solid-js'
import { dictionary, type Locale } from './i18n'
import {
@@ -38,10 +38,8 @@ import {
type MiniAppDashboard,
type MiniAppPendingMember
} from './miniapp-api'
import { Button, Field, Modal } from './components/ui'
import { HeroBanner } from './components/layout/hero-banner'
import { Button, Field, MiniChip, Modal } from './components/ui'
import { NavigationTabs } from './components/layout/navigation-tabs'
import { ProfileCard } from './components/layout/profile-card'
import { TopBar } from './components/layout/top-bar'
import { BlockedState } from './components/session/blocked-state'
import { LoadingState } from './components/session/loading-state'
@@ -2337,10 +2335,7 @@ function App() {
currentMemberLine={currentMemberLine()}
utilityTotalMajor={utilityTotalMajor()}
purchaseTotalMajor={purchaseTotalMajor()}
memberBalanceVisuals={memberBalanceVisuals()}
purchaseChart={purchaseInvestmentChart()}
memberBaseDueMajor={memberBaseDueMajor}
memberRemainingClass={memberRemainingClass}
ledgerTitle={ledgerTitle}
ledgerPrimaryAmount={ledgerPrimaryAmount}
ledgerSecondaryAmount={ledgerSecondaryAmount}
@@ -2426,25 +2421,30 @@ function App() {
</Match>
<Match when={session().status === 'ready'}>
<HeroBanner
badges={[
readySession()?.mode === 'demo' ? copy().demoBadge : copy().liveBadge,
readySession()?.member.isAdmin ? copy().adminTag : copy().residentTag,
readySession()?.member.status
<section class="app-context-row">
<div class="app-context-meta">
<MiniChip>
{readySession()?.mode === 'demo' ? copy().demoBadge : copy().liveBadge}
</MiniChip>
<MiniChip muted>
{readySession()?.member.isAdmin ? copy().adminTag : copy().residentTag}
</MiniChip>
<MiniChip muted>
{readySession()?.member.status
? memberStatusLabel(readySession()!.member.status)
: copy().memberStatusActive
]}
title={`${copy().welcome}, ${readySession()?.telegramUser.firstName ?? readySession()?.member.displayName}`}
body={copy().overviewBody}
action={
readySession()?.mode === 'live'
? {
label: copy().manageProfileAction,
onClick: () => setProfileEditorOpen(true)
}
: undefined
}
/>
: copy().memberStatusActive}
</MiniChip>
</div>
<Show when={readySession()?.mode === 'live'}>
<Button
variant="ghost"
class="app-context-row__action"
onClick={() => setProfileEditorOpen(true)}
>
{copy().manageProfileAction}
</Button>
</Show>
</section>
<NavigationTabs
items={
@@ -2459,21 +2459,7 @@ function App() {
onChange={setActiveNav}
/>
<section class="content-grid">
<ProfileCard
displayName={readySession()?.member.displayName ?? ''}
roleLabel={readySession()?.member.isAdmin ? copy().adminTag : copy().residentTag}
statusSummary={copy().memberStatusSummary.replace(
'{status}',
readySession()?.member.status
? memberStatusLabel(readySession()!.member.status)
: copy().memberStatusActive
)}
modeBadge={readySession()?.mode === 'demo' ? copy().demoBadge : copy().liveBadge}
localeBadge={locale().toUpperCase()}
/>
<div class="content-stack">{renderPanel()}</div>
</section>
<section class="content-stack">{renderPanel()}</section>
<Modal
open={profileEditorOpen()}
title={copy().displayNameLabel}

View File

@@ -13,7 +13,7 @@ type Props = {
export function TopBar(props: Props) {
return (
<section class="topbar">
<div>
<div class="topbar__copy">
<p class="eyebrow">{props.subtitle}</p>
<h1>{props.title}</h1>
</div>

View File

@@ -1,6 +1,8 @@
import * as Dialog from '@kobalte/core/dialog'
import { Show, type JSX, type ParentProps } from 'solid-js'
import { XIcon } from './icons'
export function Modal(
props: ParentProps<{
open: boolean
@@ -24,8 +26,8 @@ export function Modal(
{(description) => <Dialog.Description>{description()}</Dialog.Description>}
</Show>
</div>
<Dialog.CloseButton class="ui-button ui-button--icon">
<span aria-hidden="true">x</span>
<Dialog.CloseButton class="ui-button ui-button--icon modal-close-button">
<XIcon />
<span class="sr-only">{props.closeLabel}</span>
</Dialog.CloseButton>
</header>

View File

@@ -65,3 +65,12 @@ export function GlobeIcon(props: IconProps) {
</svg>
)
}
export function XIcon(props: IconProps) {
return (
<svg {...iconProps(props)}>
<path d="m6 6 12 12" />
<path d="M18 6 6 18" />
</svg>
)
}

View File

@@ -82,7 +82,12 @@ button {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
gap: 12px;
margin-bottom: 10px;
}
.topbar__copy {
min-width: 0;
}
.topbar h1,
@@ -94,7 +99,8 @@ button {
}
.topbar h1 {
font-size: clamp(2rem, 5vw, 3rem);
font-size: clamp(1.55rem, 4.4vw, 2.35rem);
line-height: 0.96;
}
.eyebrow {
@@ -117,14 +123,15 @@ button {
.locale-switch--compact {
justify-items: end;
min-width: 0;
align-self: flex-start;
}
.locale-switch__buttons {
display: inline-grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
padding: 4px;
gap: 4px;
padding: 3px;
border: 1px solid rgb(255 255 255 / 0.1);
border-radius: 999px;
background: rgb(255 255 255 / 0.04);
@@ -135,8 +142,8 @@ button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
width: 24px;
height: 24px;
color: rgb(247 179 137 / 0.88);
}
@@ -154,13 +161,17 @@ button {
.locale-switch__buttons button {
border-radius: 999px;
min-width: 42px;
padding: 7px 10px;
font-size: 0.78rem;
min-width: 36px;
padding: 6px 8px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
}
.locale-switch__buttons--inline {
margin-top: 6px;
}
.locale-switch__buttons button.is-active,
.nav-grid button.is-active {
border-color: rgb(247 179 137 / 0.7);
@@ -263,15 +274,22 @@ button {
}
.ui-button--primary {
border-color: rgb(247 179 137 / 0.42);
background: rgb(247 179 137 / 0.16);
border-color: rgb(247 179 137 / 0.52);
background: rgb(247 179 137 / 0.18);
color: #fff4ea;
}
.ui-button--secondary,
.ui-button--secondary {
background: rgb(255 255 255 / 0.04);
}
.ui-button--ghost,
.ui-button--icon {
background: rgb(255 255 255 / 0.04);
background: transparent;
}
.ui-button--ghost {
border-color: rgb(255 255 255 / 0.08);
}
.ui-button--danger {
@@ -296,13 +314,19 @@ button {
.nav-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-top: 18px;
gap: 6px;
margin-top: 12px;
padding: 4px;
border: 1px solid rgb(255 255 255 / 0.08);
border-radius: 18px;
background: rgb(255 255 255 / 0.03);
}
.nav-grid button {
border-radius: 18px;
padding: 14px 8px;
border: none;
border-radius: 14px;
min-height: 38px;
padding: 9px 10px;
}
.content-grid {
@@ -314,12 +338,13 @@ button {
.content-stack {
display: grid;
gap: 12px;
margin-top: 12px;
}
.summary-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
gap: 10px;
}
.panel {
@@ -342,9 +367,11 @@ button {
.stat-card,
.activity-row,
.utility-bill-row {
display: grid;
gap: 10px;
border: 1px solid rgb(255 255 255 / 0.08);
border-radius: 18px;
padding: 14px;
padding: 16px;
background: rgb(255 255 255 / 0.03);
}
@@ -367,7 +394,7 @@ button {
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 12px;
margin-bottom: 10px;
margin-bottom: 0;
}
.balance-item strong,
@@ -382,7 +409,7 @@ button {
.ledger-item p,
.activity-row p,
.utility-bill-row p {
margin-top: 6px;
margin: 0;
}
.balance-status.is-credit {
@@ -409,7 +436,6 @@ button {
.balance-breakdown {
display: grid;
gap: 10px;
margin-top: 12px;
}
.member-visual-list {
@@ -596,32 +622,8 @@ button {
margin-top: 12px;
}
.section-switch {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.section-switch button {
border: 1px solid rgb(255 255 255 / 0.12);
border-radius: 16px;
min-height: 44px;
padding: 10px 12px;
background: rgb(255 255 255 / 0.04);
color: inherit;
}
.section-switch button.is-active {
border-color: rgb(247 179 137 / 0.7);
background: rgb(247 179 137 / 0.14);
}
.admin-layout {
gap: 18px;
}
.admin-hero {
gap: 16px;
gap: 12px;
}
.admin-summary-grid,
@@ -636,24 +638,6 @@ button {
gap: 12px;
}
.admin-section__header {
display: flex;
align-items: end;
justify-content: space-between;
gap: 12px;
}
.admin-section__header h3 {
margin: 0;
font-family: 'Space Grotesk', 'IBM Plex Sans', sans-serif;
letter-spacing: -0.04em;
font-size: 1.15rem;
}
.admin-section__header p {
margin-top: 6px;
}
.admin-sublist {
margin-top: 12px;
}
@@ -728,10 +712,9 @@ button {
.panel-toolbar {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
justify-content: flex-start;
gap: 10px;
margin-top: 14px;
margin-bottom: 14px;
margin-top: 8px;
}
.ledger-compact-card {
@@ -806,7 +789,7 @@ button {
overflow: auto;
border: 1px solid rgb(255 255 255 / 0.1);
border-radius: 24px;
padding: 18px;
padding: 16px;
background:
linear-gradient(180deg, rgb(255 255 255 / 0.07), rgb(255 255 255 / 0.03)), rgb(18 26 36 / 0.96);
box-shadow: 0 28px 80px rgb(0 0 0 / 0.35);
@@ -815,7 +798,7 @@ button {
.modal-sheet__header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
gap: 10px;
align-items: start;
}
@@ -827,17 +810,25 @@ button {
}
.modal-sheet__header p {
margin-top: 8px;
margin-top: 6px;
}
.modal-sheet__body {
display: grid;
gap: 16px;
margin-top: 18px;
gap: 14px;
margin-top: 14px;
}
.modal-sheet__footer {
margin-top: 18px;
margin-top: 14px;
}
.modal-close-button {
align-self: start;
width: 36px;
min-width: 36px;
min-height: 36px;
border-color: rgb(255 255 255 / 0.08);
}
.editor-grid {
@@ -952,6 +943,29 @@ button {
overflow-wrap: anywhere;
}
.balance-item > p + .ledger-list,
.balance-item > p + .balance-list {
margin-top: 4px;
}
.app-context-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.app-context-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.app-context-row__action {
margin-left: auto;
}
@media (min-width: 760px) {
.shell {
max-width: 920px;
@@ -976,10 +990,6 @@ button {
align-self: stretch;
}
.admin-summary-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -999,10 +1009,6 @@ button {
.balance-item--wide {
grid-column: 1 / -1;
}
.section-switch {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (min-width: 980px) {
@@ -1025,21 +1031,17 @@ button {
}
.topbar {
flex-direction: column;
flex-direction: row;
align-items: flex-start;
}
.locale-switch {
justify-items: stretch;
width: 100%;
width: auto;
min-width: 0;
}
.locale-switch--compact {
justify-items: start;
}
.locale-switch__buttons {
justify-self: start;
justify-items: end;
}
.nav-grid {
@@ -1054,10 +1056,6 @@ button {
grid-template-columns: minmax(0, 1fr);
}
.admin-section__header {
align-items: start;
}
.activity-row header,
.ledger-compact-card header,
.ledger-item header,

View File

@@ -1,7 +1,6 @@
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 = {
@@ -12,32 +11,7 @@ type Props = {
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
@@ -91,17 +65,7 @@ export function HomeScreen(props: Props) {
</Show>
</div>
<Show
when={props.currentMemberLine}
fallback={
<article class="balance-item">
<header>
<strong>{props.copy.overviewTitle ?? ''}</strong>
</header>
<p>{props.copy.overviewBody ?? ''}</p>
</article>
}
>
<Show when={props.currentMemberLine}>
{(member) => (
<article class="balance-item balance-item--accent">
<header>
@@ -110,7 +74,6 @@ export function HomeScreen(props: Props) {
{member().remainingMajor} {props.dashboard!.currency}
</span>
</header>
<p>{props.copy.yourBalanceBody ?? ''}</p>
<p>
{props.copy.shareRent ?? ''}: {props.dashboard!.rentSourceAmountMajor}{' '}
{props.dashboard!.rentSourceCurrency}
@@ -154,23 +117,6 @@ export function HomeScreen(props: Props) {
)}
</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>

View File

@@ -10,6 +10,7 @@ import {
PlusIcon,
SettingsIcon
} from '../components/ui'
import { NavigationTabs } from '../components/layout/navigation-tabs'
import type {
MiniAppAdminCycleState,
MiniAppAdminSettingsPayload,
@@ -175,63 +176,21 @@ export function HouseScreen(props: Props) {
return (
<div class="admin-layout">
<article class="balance-item balance-item--accent admin-hero">
<header>
<strong>{props.copy.householdSettingsTitle ?? ''}</strong>
<span>{props.adminSettings?.settings.settlementCurrency ?? '—'}</span>
</header>
<p>{props.copy.householdSettingsBody ?? ''}</p>
<div class="admin-summary-grid">
<article class="stat-card">
<span>{props.copy.billingCycleTitle ?? ''}</span>
<strong>{props.cycleState?.cycle?.period ?? props.copy.billingCycleEmpty ?? ''}</strong>
</article>
<article class="stat-card">
<span>{props.copy.settlementCurrency ?? ''}</span>
<strong>{props.adminSettings?.settings.settlementCurrency ?? '—'}</strong>
</article>
<article class="stat-card">
<span>{props.copy.membersCount ?? ''}</span>
<strong>{String(props.adminSettings?.members.length ?? 0)}</strong>
</article>
<article class="stat-card">
<span>{props.copy.pendingRequests ?? ''}</span>
<strong>{String(props.pendingMembers.length)}</strong>
</article>
</div>
</article>
<div class="section-switch">
<For
each={
<NavigationTabs
items={
[
['billing', props.copy.houseSectionBilling],
['utilities', props.copy.houseSectionUtilities],
['members', props.copy.houseSectionMembers],
['topics', props.copy.houseSectionTopics]
{ key: 'billing', label: props.copy.houseSectionBilling ?? '' },
{ key: 'utilities', label: props.copy.houseSectionUtilities ?? '' },
{ key: 'members', label: props.copy.houseSectionMembers ?? '' },
{ key: 'topics', label: props.copy.houseSectionTopics ?? '' }
] as const
}
>
{([key, label]) => (
<button
classList={{ 'is-active': props.activeHouseSection === key }}
type="button"
onClick={() => props.onChangeHouseSection(key)}
>
{label}
</button>
)}
</For>
</div>
active={props.activeHouseSection}
onChange={props.onChangeHouseSection}
/>
<Show when={props.activeHouseSection === 'billing'}>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{props.copy.billingCycleTitle ?? ''}</h3>
<p>{props.copy.billingSettingsTitle ?? ''}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item">
<header>
@@ -310,8 +269,7 @@ export function HouseScreen(props: Props) {
<strong>{props.copy.householdLanguage ?? ''}</strong>
<span>{props.householdDefaultLocale.toUpperCase()}</span>
</header>
<p>{props.copy.householdSettingsBody ?? ''}</p>
<div class="locale-switch__buttons">
<div class="locale-switch__buttons locale-switch__buttons--inline">
<button
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
type="button"
@@ -530,12 +488,6 @@ export function HouseScreen(props: Props) {
<Show when={props.activeHouseSection === 'utilities'}>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{props.copy.utilityCategoriesTitle ?? ''}</h3>
<p>{props.copy.utilityCategoriesBody ?? ''}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item">
<header>
@@ -861,12 +813,6 @@ export function HouseScreen(props: Props) {
<Show when={props.activeHouseSection === 'members'}>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{props.copy.adminsTitle ?? ''}</h3>
<p>{props.copy.adminsBody ?? ''}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item admin-card--wide">
<header>
@@ -1118,12 +1064,6 @@ export function HouseScreen(props: Props) {
<Show when={props.activeHouseSection === 'topics'}>
<section class="admin-section">
<header class="admin-section__header">
<div>
<h3>{props.copy.topicBindingsTitle ?? ''}</h3>
<p>{props.copy.topicBindingsBody ?? ''}</p>
</div>
</header>
<div class="admin-grid">
<article class="balance-item admin-card--wide">
<header>