mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 17:44:03 +00:00
miniapp: refresh after mutations, activity expand, squash chart palette
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { createContext, createMemo, createSignal, useContext, type ParentProps } from 'solid-js'
|
||||
import {
|
||||
createContext,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
useContext,
|
||||
type ParentProps
|
||||
} from 'solid-js'
|
||||
|
||||
import { majorStringToMinor, minorToMajorString } from '../lib/money'
|
||||
import {
|
||||
@@ -51,7 +58,14 @@ export type CycleFormState = {
|
||||
utilityAmountMajor: string
|
||||
}
|
||||
|
||||
const chartPalette = ['#3ecf8e', '#6fd3c0', '#94a8ff', '#f06a8d', '#f3d36f', '#7dc96d'] as const
|
||||
const chartPalette = [
|
||||
'var(--chart-1)',
|
||||
'var(--chart-2)',
|
||||
'var(--chart-3)',
|
||||
'var(--chart-4)',
|
||||
'var(--chart-5)',
|
||||
'var(--chart-6)'
|
||||
] as const
|
||||
|
||||
type DashboardContextValue = {
|
||||
dashboard: () => MiniAppDashboard | null
|
||||
@@ -224,7 +238,7 @@ function computePurchaseInvestmentChart(
|
||||
/* ── Provider ───────────────────────────────────────── */
|
||||
|
||||
export function DashboardProvider(props: ParentProps) {
|
||||
const { readySession } = useSession()
|
||||
const { readySession, registerRefreshListener } = useSession()
|
||||
const { copy } = useI18n()
|
||||
|
||||
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
||||
@@ -275,6 +289,9 @@ export function DashboardProvider(props: ParentProps) {
|
||||
computePurchaseInvestmentChart(dashboard(), purchaseLedger(), copy().ledgerActorFallback)
|
||||
)
|
||||
|
||||
const unregisterDashboardRefreshListener = registerRefreshListener(loadDashboardData)
|
||||
onCleanup(unregisterDashboardRefreshListener)
|
||||
|
||||
async function loadDashboardData(initData: string, isAdmin: boolean) {
|
||||
// In demo mode, use demo data
|
||||
if (!initData) {
|
||||
|
||||
@@ -62,6 +62,9 @@ type SessionContextValue = {
|
||||
handleMemberLocaleChange: (nextLocale: Locale) => Promise<void>
|
||||
handleHouseholdLocaleChange: (nextLocale: Locale) => Promise<void>
|
||||
refreshHouseholdData: (includeAdmin?: boolean, forceRefresh?: boolean) => Promise<void>
|
||||
registerRefreshListener: (
|
||||
listener: (initData: string, isAdmin: boolean) => Promise<void>
|
||||
) => () => void
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextValue>()
|
||||
@@ -114,6 +117,17 @@ export function SessionProvider(
|
||||
const [displayNameDraft, setDisplayNameDraft] = createSignal('')
|
||||
const [savingOwnDisplayName, setSavingOwnDisplayName] = createSignal(false)
|
||||
|
||||
const refreshListeners = new Set<(initData: string, isAdmin: boolean) => Promise<void>>()
|
||||
|
||||
function registerRefreshListener(
|
||||
listener: (initData: string, isAdmin: boolean) => Promise<void>
|
||||
) {
|
||||
refreshListeners.add(listener)
|
||||
return () => {
|
||||
refreshListeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
const readySession = () => {
|
||||
const current = session()
|
||||
return current.status === 'ready' ? current : null
|
||||
@@ -310,7 +324,11 @@ export function SessionProvider(
|
||||
// Delegate actual data loading to dashboard context via onReady
|
||||
const current = readySession()
|
||||
if (current) {
|
||||
await props.onReady?.(data, includeAdmin || current.member.isAdmin)
|
||||
const isAdmin = includeAdmin || current.member.isAdmin
|
||||
await Promise.all([
|
||||
props.onReady?.(data, isAdmin),
|
||||
...Array.from(refreshListeners).map((l) => l(data, isAdmin))
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +354,8 @@ export function SessionProvider(
|
||||
handleSaveOwnDisplayName,
|
||||
handleMemberLocaleChange,
|
||||
handleHouseholdLocaleChange,
|
||||
refreshHouseholdData
|
||||
refreshHouseholdData,
|
||||
registerRefreshListener
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -126,6 +126,8 @@ export const dictionary = {
|
||||
emptyDashboard: 'No billing cycle is ready yet.',
|
||||
latestActivityTitle: 'Latest activity',
|
||||
latestActivityEmpty: 'Recent utility and purchase entries will appear here.',
|
||||
showMoreAction: 'Show more',
|
||||
showLessAction: 'Show less',
|
||||
testingViewBadge: 'Testing view',
|
||||
testingSurfaceTitle: 'Hidden QA view',
|
||||
testingSurfaceBody:
|
||||
@@ -427,6 +429,8 @@ export const dictionary = {
|
||||
emptyDashboard: 'Пока нет готового расчётного цикла.',
|
||||
latestActivityTitle: 'Последняя активность',
|
||||
latestActivityEmpty: 'Здесь появятся последние коммунальные платежи и покупки.',
|
||||
showMoreAction: 'Показать ещё',
|
||||
showLessAction: 'Свернуть',
|
||||
testingViewBadge: 'Тестовый вид',
|
||||
testingSurfaceTitle: 'Скрытый QA режим',
|
||||
testingSurfaceBody:
|
||||
|
||||
@@ -1137,6 +1137,33 @@ a {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.activity-card__show-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-xs);
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm);
|
||||
margin-top: var(--spacing-xs);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--accent);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.activity-card__show-more:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.activity-card__show-more:active {
|
||||
background: var(--bg-tertiary);
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* ── Balances Route ───────────────────────────────────── */
|
||||
|
||||
.route--balances {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Show, For } from 'solid-js'
|
||||
import { Clock } from 'lucide-solid'
|
||||
import { Show, For, createSignal } from 'solid-js'
|
||||
import { Clock, ChevronDown, ChevronUp } from 'lucide-solid'
|
||||
|
||||
import { useSession } from '../contexts/session-context'
|
||||
import { useI18n } from '../contexts/i18n-context'
|
||||
@@ -13,6 +13,7 @@ export default function HomeRoute() {
|
||||
const { readySession } = useSession()
|
||||
const { copy } = useI18n()
|
||||
const { dashboard, currentMemberLine } = useDashboard()
|
||||
const [showAllActivity, setShowAllActivity] = createSignal(false)
|
||||
|
||||
function dueStatusBadge() {
|
||||
const data = dashboard()
|
||||
@@ -138,7 +139,7 @@ export default function HomeRoute() {
|
||||
fallback={<p class="empty-state">{copy().latestActivityEmpty}</p>}
|
||||
>
|
||||
<div class="activity-card__list">
|
||||
<For each={data().ledger.slice(0, 5)}>
|
||||
<For each={showAllActivity() ? data().ledger : data().ledger.slice(0, 5)}>
|
||||
{(entry) => (
|
||||
<div class="activity-card__item">
|
||||
<span class="activity-card__title">{entry.title}</span>
|
||||
@@ -147,6 +148,25 @@ export default function HomeRoute() {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={data().ledger.length > 5}>
|
||||
<button
|
||||
class="activity-card__show-more"
|
||||
onClick={() => setShowAllActivity(!showAllActivity())}
|
||||
>
|
||||
<Show
|
||||
when={showAllActivity()}
|
||||
fallback={
|
||||
<>
|
||||
<span>{copy().showMoreAction}</span>
|
||||
<ChevronDown size={14} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>{copy().showLessAction}</span>
|
||||
<ChevronUp size={14} />
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -76,11 +76,11 @@
|
||||
|
||||
/* ── Chart palette ──────────────────────────────────── */
|
||||
--chart-1: #3ecf8e;
|
||||
--chart-2: #6fd3c0;
|
||||
--chart-3: #94a8ff;
|
||||
--chart-4: #f06a8d;
|
||||
--chart-5: #f3d36f;
|
||||
--chart-6: #7dc96d;
|
||||
--chart-2: #8a90ff;
|
||||
--chart-3: #da8c00;
|
||||
--chart-4: #2fb8c9;
|
||||
--chart-5: #c26cff;
|
||||
--chart-6: #ff5c9a;
|
||||
|
||||
/* ── Status colors ──────────────────────────────────── */
|
||||
--status-credit: #4ade80;
|
||||
|
||||
Reference in New Issue
Block a user