934eee793c
Home (mirrors S-4, S-3 while the partner has not joined): global balance hero with the seesaw and §9.3 wording, Settle up, active accounts with balance and this month's count, collapsed "Arkiverade (n)" kept for the session, empty, loading and error states, New expense when an active account exists. Account detail (S-5): header with default split, split bar, archived badge and balance; expenses and settlements interleaved by date, grouped by month with household spend (settlement rows carry the allocation, AC-11.7); overflow menu with Edit, Archive/Unarchive (undo snackbar, D-071) and Delete only when allowed (AC-6.5); not-found, loading and error states. Account editor (S-6): name (1-60 code points, name_taken on the field), two percent fields that always sum to 100 with two decimals, slider and presets, the "Av 100,00 kr" preview; discard dialog with a title and no body (D-080) on every navigation away; E-18/E-19 apply the returned row. The account preset for W-5 and W-7 is the query parameter `account` (proposed D-144). Tests over a shared snapshot fixture and the fake server. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
import { ButtonLink, Button } from '../../components/Button';
|
|
import { Card } from '../../components/Card';
|
|
import { EmptyState } from '../../components/EmptyState';
|
|
import { Icon } from '../../design/Icon';
|
|
import { useI18n } from '../../i18n/useI18n';
|
|
import { paths } from '../../app/routes';
|
|
import { writeErrorKey } from './accountWrites';
|
|
import styles from './PageStates.module.css';
|
|
|
|
/**
|
|
* The first snapshot pull (SPEC §15.6 "loading"): design-spec §3.6 hero `Loading` content, announced politely.
|
|
* `compact` drops the body line (account pages).
|
|
*/
|
|
export function LoadingCard({ compact = false }: { compact?: boolean }) {
|
|
const { t } = useI18n();
|
|
return (
|
|
<Card className={styles.loading}>
|
|
<div role="status" className={styles.loadingInner}>
|
|
<span className={styles.spinnerTile} aria-hidden="true">
|
|
<Icon name="sync" size={28} className={styles.spinner} />
|
|
</span>
|
|
<p className={styles.loadingTitle}>{t('home_loading')}</p>
|
|
{compact ? null : <p className={styles.muted}>{t('home_loading_body')}</p>}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
/** A failed read of the snapshot with nothing to show yet (SPEC §15.6 "error, with retry"). */
|
|
export function LoadFailedCard({ error, onRetry }: { error: unknown; onRetry: () => void }) {
|
|
const { t } = useI18n();
|
|
return (
|
|
<Card className={styles.failed}>
|
|
<div role="alert" className={styles.failedInner}>
|
|
<Icon name="warning" filled size={24} className={styles.warning} />
|
|
<div className={styles.failedText}>
|
|
<h2 className={styles.failedTitle}>{t('web_accounts_load_failed')}</h2>
|
|
<p className={styles.muted}>{t(writeErrorKey(error))}</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Button variant="tonal" icon={<Icon name="refresh" size={20} />} onClick={onRetry}>
|
|
{t('common_retry')}
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
/** An account id that is not (or no longer) in the snapshot. */
|
|
export function AccountNotFound() {
|
|
const { t } = useI18n();
|
|
return (
|
|
<EmptyState
|
|
icon="info"
|
|
title={t('error_not_found')}
|
|
body={t('web_accounts_not_found_body')}
|
|
action={
|
|
<ButtonLink to={paths.home} variant="tonal">
|
|
{t('web_not_found_action')}
|
|
</ButtonLink>
|
|
}
|
|
/>
|
|
);
|
|
}
|