mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +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 { majorStringToMinor, minorToMajorString } from '../lib/money'
|
||||||
import {
|
import {
|
||||||
@@ -51,7 +58,14 @@ export type CycleFormState = {
|
|||||||
utilityAmountMajor: string
|
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 = {
|
type DashboardContextValue = {
|
||||||
dashboard: () => MiniAppDashboard | null
|
dashboard: () => MiniAppDashboard | null
|
||||||
@@ -224,7 +238,7 @@ function computePurchaseInvestmentChart(
|
|||||||
/* ── Provider ───────────────────────────────────────── */
|
/* ── Provider ───────────────────────────────────────── */
|
||||||
|
|
||||||
export function DashboardProvider(props: ParentProps) {
|
export function DashboardProvider(props: ParentProps) {
|
||||||
const { readySession } = useSession()
|
const { readySession, registerRefreshListener } = useSession()
|
||||||
const { copy } = useI18n()
|
const { copy } = useI18n()
|
||||||
|
|
||||||
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
const [dashboard, setDashboard] = createSignal<MiniAppDashboard | null>(null)
|
||||||
@@ -275,6 +289,9 @@ export function DashboardProvider(props: ParentProps) {
|
|||||||
computePurchaseInvestmentChart(dashboard(), purchaseLedger(), copy().ledgerActorFallback)
|
computePurchaseInvestmentChart(dashboard(), purchaseLedger(), copy().ledgerActorFallback)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const unregisterDashboardRefreshListener = registerRefreshListener(loadDashboardData)
|
||||||
|
onCleanup(unregisterDashboardRefreshListener)
|
||||||
|
|
||||||
async function loadDashboardData(initData: string, isAdmin: boolean) {
|
async function loadDashboardData(initData: string, isAdmin: boolean) {
|
||||||
// In demo mode, use demo data
|
// In demo mode, use demo data
|
||||||
if (!initData) {
|
if (!initData) {
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ type SessionContextValue = {
|
|||||||
handleMemberLocaleChange: (nextLocale: Locale) => Promise<void>
|
handleMemberLocaleChange: (nextLocale: Locale) => Promise<void>
|
||||||
handleHouseholdLocaleChange: (nextLocale: Locale) => Promise<void>
|
handleHouseholdLocaleChange: (nextLocale: Locale) => Promise<void>
|
||||||
refreshHouseholdData: (includeAdmin?: boolean, forceRefresh?: boolean) => Promise<void>
|
refreshHouseholdData: (includeAdmin?: boolean, forceRefresh?: boolean) => Promise<void>
|
||||||
|
registerRefreshListener: (
|
||||||
|
listener: (initData: string, isAdmin: boolean) => Promise<void>
|
||||||
|
) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SessionContext = createContext<SessionContextValue>()
|
const SessionContext = createContext<SessionContextValue>()
|
||||||
@@ -114,6 +117,17 @@ export function SessionProvider(
|
|||||||
const [displayNameDraft, setDisplayNameDraft] = createSignal('')
|
const [displayNameDraft, setDisplayNameDraft] = createSignal('')
|
||||||
const [savingOwnDisplayName, setSavingOwnDisplayName] = createSignal(false)
|
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 readySession = () => {
|
||||||
const current = session()
|
const current = session()
|
||||||
return current.status === 'ready' ? current : null
|
return current.status === 'ready' ? current : null
|
||||||
@@ -310,7 +324,11 @@ export function SessionProvider(
|
|||||||
// Delegate actual data loading to dashboard context via onReady
|
// Delegate actual data loading to dashboard context via onReady
|
||||||
const current = readySession()
|
const current = readySession()
|
||||||
if (current) {
|
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,
|
handleSaveOwnDisplayName,
|
||||||
handleMemberLocaleChange,
|
handleMemberLocaleChange,
|
||||||
handleHouseholdLocaleChange,
|
handleHouseholdLocaleChange,
|
||||||
refreshHouseholdData
|
refreshHouseholdData,
|
||||||
|
registerRefreshListener
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ export const dictionary = {
|
|||||||
emptyDashboard: 'No billing cycle is ready yet.',
|
emptyDashboard: 'No billing cycle is ready yet.',
|
||||||
latestActivityTitle: 'Latest activity',
|
latestActivityTitle: 'Latest activity',
|
||||||
latestActivityEmpty: 'Recent utility and purchase entries will appear here.',
|
latestActivityEmpty: 'Recent utility and purchase entries will appear here.',
|
||||||
|
showMoreAction: 'Show more',
|
||||||
|
showLessAction: 'Show less',
|
||||||
testingViewBadge: 'Testing view',
|
testingViewBadge: 'Testing view',
|
||||||
testingSurfaceTitle: 'Hidden QA view',
|
testingSurfaceTitle: 'Hidden QA view',
|
||||||
testingSurfaceBody:
|
testingSurfaceBody:
|
||||||
@@ -427,6 +429,8 @@ export const dictionary = {
|
|||||||
emptyDashboard: 'Пока нет готового расчётного цикла.',
|
emptyDashboard: 'Пока нет готового расчётного цикла.',
|
||||||
latestActivityTitle: 'Последняя активность',
|
latestActivityTitle: 'Последняя активность',
|
||||||
latestActivityEmpty: 'Здесь появятся последние коммунальные платежи и покупки.',
|
latestActivityEmpty: 'Здесь появятся последние коммунальные платежи и покупки.',
|
||||||
|
showMoreAction: 'Показать ещё',
|
||||||
|
showLessAction: 'Свернуть',
|
||||||
testingViewBadge: 'Тестовый вид',
|
testingViewBadge: 'Тестовый вид',
|
||||||
testingSurfaceTitle: 'Скрытый QA режим',
|
testingSurfaceTitle: 'Скрытый QA режим',
|
||||||
testingSurfaceBody:
|
testingSurfaceBody:
|
||||||
|
|||||||
@@ -1137,6 +1137,33 @@ a {
|
|||||||
font-variant-numeric: tabular-nums;
|
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 ───────────────────────────────────── */
|
/* ── Balances Route ───────────────────────────────────── */
|
||||||
|
|
||||||
.route--balances {
|
.route--balances {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Show, For } from 'solid-js'
|
import { Show, For, createSignal } from 'solid-js'
|
||||||
import { Clock } from 'lucide-solid'
|
import { Clock, ChevronDown, ChevronUp } from 'lucide-solid'
|
||||||
|
|
||||||
import { useSession } from '../contexts/session-context'
|
import { useSession } from '../contexts/session-context'
|
||||||
import { useI18n } from '../contexts/i18n-context'
|
import { useI18n } from '../contexts/i18n-context'
|
||||||
@@ -13,6 +13,7 @@ export default function HomeRoute() {
|
|||||||
const { readySession } = useSession()
|
const { readySession } = useSession()
|
||||||
const { copy } = useI18n()
|
const { copy } = useI18n()
|
||||||
const { dashboard, currentMemberLine } = useDashboard()
|
const { dashboard, currentMemberLine } = useDashboard()
|
||||||
|
const [showAllActivity, setShowAllActivity] = createSignal(false)
|
||||||
|
|
||||||
function dueStatusBadge() {
|
function dueStatusBadge() {
|
||||||
const data = dashboard()
|
const data = dashboard()
|
||||||
@@ -138,7 +139,7 @@ export default function HomeRoute() {
|
|||||||
fallback={<p class="empty-state">{copy().latestActivityEmpty}</p>}
|
fallback={<p class="empty-state">{copy().latestActivityEmpty}</p>}
|
||||||
>
|
>
|
||||||
<div class="activity-card__list">
|
<div class="activity-card__list">
|
||||||
<For each={data().ledger.slice(0, 5)}>
|
<For each={showAllActivity() ? data().ledger : data().ledger.slice(0, 5)}>
|
||||||
{(entry) => (
|
{(entry) => (
|
||||||
<div class="activity-card__item">
|
<div class="activity-card__item">
|
||||||
<span class="activity-card__title">{entry.title}</span>
|
<span class="activity-card__title">{entry.title}</span>
|
||||||
@@ -147,6 +148,25 @@ export default function HomeRoute() {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -76,11 +76,11 @@
|
|||||||
|
|
||||||
/* ── Chart palette ──────────────────────────────────── */
|
/* ── Chart palette ──────────────────────────────────── */
|
||||||
--chart-1: #3ecf8e;
|
--chart-1: #3ecf8e;
|
||||||
--chart-2: #6fd3c0;
|
--chart-2: #8a90ff;
|
||||||
--chart-3: #94a8ff;
|
--chart-3: #da8c00;
|
||||||
--chart-4: #f06a8d;
|
--chart-4: #2fb8c9;
|
||||||
--chart-5: #f3d36f;
|
--chart-5: #c26cff;
|
||||||
--chart-6: #7dc96d;
|
--chart-6: #ff5c9a;
|
||||||
|
|
||||||
/* ── Status colors ──────────────────────────────────── */
|
/* ── Status colors ──────────────────────────────────── */
|
||||||
--status-credit: #4ade80;
|
--status-credit: #4ade80;
|
||||||
|
|||||||
Reference in New Issue
Block a user