feat(web): home dashboard + data assembly ported from iOS
This commit is contained in:
parent
2555e42f21
commit
762b572d30
6 changed files with 491 additions and 3 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { t } from "@/i18n/common";
|
||||
import { DashboardView } from "@/features/home/DashboardView";
|
||||
|
||||
// Placeholder — Task 9 replaces this with the real dashboard.
|
||||
export default function HomePage() {
|
||||
return <h1 style={{ fontWeight: 700 }}>{t.tabs.home}</h1>;
|
||||
return <DashboardView />;
|
||||
}
|
||||
|
|
|
|||
223
src/features/home/DashboardView.tsx
Normal file
223
src/features/home/DashboardView.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { Skeleton, ProgressCircleRoot, ProgressCircleTrack, ProgressCircleRange } from "@seed-design/react";
|
||||
import { Card, AmountToggle, HideAmountsToggle } from "../../ds";
|
||||
import { tugrikShortRaw } from "../../ds/money";
|
||||
import { useNetWorth, useAnalyzeMonth, useAnalyzeToday, useBudget } from "../../api/hooks/reads";
|
||||
import { buildHome, type HomeData } from "./buildHome";
|
||||
import { sample } from "./sample";
|
||||
import { homeStrings as s } from "./strings";
|
||||
|
||||
/** A number that's redacted with a Seed skeleton block while loading, and a
|
||||
* tap-to-reveal `AmountToggle` once real data has arrived. Mirrors iOS's
|
||||
* `.skeleton(loading)` view modifier, which redacts the finished layout in
|
||||
* place rather than swapping in a separate spinner. */
|
||||
function Amount({ value, loading, width = "72px" }: { value: number; loading: boolean; width?: string }) {
|
||||
if (loading) return <Skeleton height="1em" width={width} style={{ display: "inline-block" }} />;
|
||||
return <AmountToggle value={value} />;
|
||||
}
|
||||
|
||||
/** The circular "₮" badge used on every card (togrogCircle in DashboardView.swift). */
|
||||
function TugrikCircle({ bg, fg }: { bg: string; fg: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 49,
|
||||
height: 49,
|
||||
borderRadius: "50%",
|
||||
background: bg,
|
||||
color: fg,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontWeight: 700,
|
||||
fontSize: 20,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
₮
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The Нүүр (home) dashboard: header, safe-to-spend hero, daily-limit +
|
||||
* top budgets, this-month spend, and recent income/expense. Figures come
|
||||
* from `buildHome` (real where the backend has them, `sample` otherwise).
|
||||
* Ported from ios/Mercury/Features/Home/DashboardView.swift. */
|
||||
export function DashboardView() {
|
||||
const netWorthQ = useNetWorth();
|
||||
const monthQ = useAnalyzeMonth();
|
||||
const todayQ = useAnalyzeToday();
|
||||
const budgetQ = useBudget();
|
||||
|
||||
const loading = netWorthQ.isLoading || monthQ.isLoading || todayQ.isLoading || budgetQ.isLoading;
|
||||
|
||||
const built = buildHome({
|
||||
netWorth: netWorthQ.data,
|
||||
month: monthQ.data,
|
||||
today: todayQ.data,
|
||||
budget: budgetQ.data,
|
||||
});
|
||||
const data: HomeData = built ?? sample;
|
||||
|
||||
const budgetDenominator = data.availableBudget;
|
||||
const fraction = budgetDenominator > 0 ? Math.min(1, data.monthlyExpense / budgetDenominator) : 0;
|
||||
const dailyFraction = data.dailyLimitTotal > 0 ? Math.min(1, data.dailyLimitUsed / data.dailyLimitTotal) : 0;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
||||
<header style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 18, color: "var(--seed-color-fg-neutral)" }}>{s.wordmark}</span>
|
||||
<HideAmountsToggle label={s.hideAmounts} />
|
||||
</header>
|
||||
|
||||
{/* Safe-to-spend hero (green): how much is left to spend this month
|
||||
after committed obligations and what's already been spent. */}
|
||||
<Link
|
||||
href="/accounting"
|
||||
style={{
|
||||
display: "block",
|
||||
textDecoration: "none",
|
||||
color: "var(--mercury-on-brand)",
|
||||
background: "var(--mercury-balance-card)",
|
||||
borderRadius: "var(--seed-radius-r5, 20px)",
|
||||
padding: "14px 20px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<TugrikCircle bg="var(--mercury-balance-circle)" fg="var(--mercury-on-brand)" />
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span style={{ fontSize: 12 }}>{data.overspent ? s.overspent : s.safeToSpend}</span>
|
||||
<span style={{ fontSize: 26, fontWeight: 700 }}>
|
||||
<Amount value={data.safeToSpend} loading={loading} width="140px" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
height: 6,
|
||||
borderRadius: 999,
|
||||
background: "rgba(0,0,0,0.13)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: `${Math.max(2, fraction * 100)}%`,
|
||||
borderRadius: 999,
|
||||
background: data.overspent ? "var(--seed-color-bg-critical, #d92d20)" : "var(--mercury-on-brand)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: 8, display: "flex", justifyContent: "space-between", fontSize: 12 }}>
|
||||
<span>{s.spent}</span>
|
||||
<span style={{ fontWeight: 700, fontSize: 13 }}>
|
||||
<Amount value={data.monthlyExpense} loading={loading} />
|
||||
<span style={{ fontWeight: 400, fontSize: 11 }}> / {tugrikShortRaw(budgetDenominator)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Daily limit + top budgets (blue). */}
|
||||
<Link
|
||||
href="/planner"
|
||||
style={{
|
||||
display: "block",
|
||||
textDecoration: "none",
|
||||
color: "#fff",
|
||||
background: "var(--mercury-limit-card)",
|
||||
borderRadius: "var(--seed-radius-r5, 20px)",
|
||||
padding: "14px 20px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<ProgressCircleRoot value={dailyFraction * 100} size="40" tone="staticWhite">
|
||||
<ProgressCircleTrack />
|
||||
<ProgressCircleRange />
|
||||
</ProgressCircleRoot>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span style={{ fontSize: 12 }}>{s.dailyLimit}</span>
|
||||
<span style={{ fontSize: 22, fontWeight: 700 }}>
|
||||
<Amount value={data.dailyLimitUsed} loading={loading} width="60px" />
|
||||
{" / "}
|
||||
<Amount value={data.dailyLimitTotal} loading={loading} width="60px" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 4, fontSize: 11, opacity: 0.85 }}>
|
||||
{s.todaySpent}: <Amount value={data.todayExpense} loading={loading} width="50px" />
|
||||
</div>
|
||||
<hr style={{ margin: "12px 0", border: 0, borderTop: "1px solid rgba(0,0,0,0.13)" }} />
|
||||
{data.budgets.map((b) => (
|
||||
<div key={b.name} style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
|
||||
<span style={{ fontSize: 12 }}>{b.name}</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 700 }}>
|
||||
<span style={{ color: "var(--mercury-warning-chip)" }}>
|
||||
<Amount value={b.spent} loading={loading} width="50px" />
|
||||
</span>
|
||||
<span style={{ fontWeight: 700, fontSize: 11 }}> / {b.range}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</Link>
|
||||
|
||||
{/* This month's spend (white). */}
|
||||
<Card style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<span style={{ fontWeight: 700, fontSize: 14, color: "var(--seed-color-fg-placeholder)" }}>{s.thisMonth}</span>
|
||||
<div style={{ display: "flex", alignItems: "baseline", gap: 4 }}>
|
||||
{loading ? (
|
||||
<Skeleton height="1.6em" width="120px" />
|
||||
) : (
|
||||
<span style={{ fontSize: 26, fontWeight: 700, color: "var(--seed-color-fg-neutral)" }}>
|
||||
{tugrikShortRaw(data.monthlyExpense)}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: 16, fontWeight: 600, color: "var(--seed-color-fg-placeholder)" }}>
|
||||
{s.expenseLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Link
|
||||
href="/accounting"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--mercury-on-brand)",
|
||||
background: "var(--mercury-warning-chip)",
|
||||
borderRadius: 999,
|
||||
padding: "8px 12px",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
{s.viewAll} ›
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recent income / expense ledgers. */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Card>
|
||||
<span style={{ fontWeight: 700, fontSize: 14, color: "var(--seed-color-fg-placeholder)" }}>{s.income}</span>
|
||||
{data.income.map((row, i) => (
|
||||
<div key={i} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0" }}>
|
||||
<span style={{ color: "var(--seed-color-fg-neutral)" }}>{row.title}</span>
|
||||
<span style={{ color: "var(--seed-color-fg-placeholder)" }}>{row.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
<Card>
|
||||
<span style={{ fontWeight: 700, fontSize: 14, color: "var(--seed-color-fg-placeholder)" }}>{s.expense}</span>
|
||||
{data.expense.map((row, i) => (
|
||||
<div key={i} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0" }}>
|
||||
<span style={{ color: "var(--seed-color-fg-neutral)" }}>{row.title}</span>
|
||||
<span style={{ color: "var(--seed-color-fg-placeholder)" }}>{row.date}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
src/features/home/buildHome.test.ts
Normal file
97
src/features/home/buildHome.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildHome } from "./buildHome";
|
||||
import * as fx from "../../test/fixtures";
|
||||
|
||||
describe("buildHome", () => {
|
||||
it("uses monthLimit when set and discretionary spend", () => {
|
||||
const h = buildHome({ netWorth: fx.netWorth, month: fx.analyzeMonth, today: fx.analyzeMonth, budget: fx.budget })!;
|
||||
expect(h.availableBudget).toBe(900000); // monthLimit
|
||||
expect(h.monthlyExpense).toBe(800000); // discretionaryExpense
|
||||
expect(h.safeToSpend).toBe(100000);
|
||||
expect(h.overspent).toBe(false);
|
||||
expect(h.totalBalance).toBe(452200);
|
||||
expect(h.todayExpense).toBe(1200000); // today's `expense` field (not discretionary)
|
||||
});
|
||||
|
||||
it("returns null for empty account", () =>
|
||||
expect(
|
||||
buildHome({
|
||||
netWorth: { assets: "0", liabilities: "0", total: "0" },
|
||||
month: { ...fx.analyzeMonth, expense: "0", discretionaryExpense: "0", expensePayees: [] },
|
||||
today: null,
|
||||
budget: null,
|
||||
}),
|
||||
).toBeNull());
|
||||
|
||||
it("returns null when month is missing", () =>
|
||||
expect(buildHome({ netWorth: fx.netWorth, month: null, today: null, budget: null })).toBeNull());
|
||||
|
||||
it("falls back to availableIncome when monthLimit is 0", () => {
|
||||
const h = buildHome({
|
||||
netWorth: fx.netWorth,
|
||||
month: fx.analyzeMonth,
|
||||
today: null,
|
||||
budget: { ...fx.budget, monthLimit: "0" },
|
||||
})!;
|
||||
expect(h.availableBudget).toBe(2036795); // availableIncome
|
||||
});
|
||||
|
||||
it("falls back to expense when discretionaryExpense is absent", () => {
|
||||
const h = buildHome({
|
||||
netWorth: fx.netWorth,
|
||||
month: { ...fx.analyzeMonth, discretionaryExpense: null },
|
||||
today: null,
|
||||
budget: fx.budget,
|
||||
})!;
|
||||
expect(h.monthlyExpense).toBe(1200000); // expense
|
||||
});
|
||||
|
||||
it("flags overspent when monthlyExpense exceeds availableBudget", () => {
|
||||
const h = buildHome({
|
||||
netWorth: fx.netWorth,
|
||||
month: { ...fx.analyzeMonth, discretionaryExpense: "1000000" },
|
||||
today: null,
|
||||
budget: { ...fx.budget, monthLimit: "900000" },
|
||||
})!;
|
||||
expect(h.overspent).toBe(true);
|
||||
expect(h.safeToSpend).toBe(0); // floored at 0, not negative
|
||||
});
|
||||
|
||||
it("takes the top-3 day-horizon budget rows with limit > 0, sorted desc", () => {
|
||||
const h = buildHome({
|
||||
netWorth: fx.netWorth,
|
||||
month: fx.analyzeMonth,
|
||||
today: null,
|
||||
budget: {
|
||||
...fx.budget,
|
||||
report: {
|
||||
...fx.budget.report,
|
||||
day: {
|
||||
overallSpent: "15500",
|
||||
overallLimit: "35500",
|
||||
rows: [
|
||||
{ category: "Тээвэр", spent: "0", limit: "5000" },
|
||||
{ category: "Хоол", spent: "23700", limit: "27000" },
|
||||
{ category: "Coffee", spent: "23700", limit: "6000" },
|
||||
{ category: "Бусад", spent: "0", limit: "0" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})!;
|
||||
expect(h.budgets).toHaveLength(3);
|
||||
expect(h.budgets.map((b) => b.name)).toEqual(["Хоол", "Coffee", "Тээвэр"]);
|
||||
expect(h.budgets.every((b) => b.range.includes("₮"))).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to sample income/expense ledgers when payees are empty", () => {
|
||||
const h = buildHome({
|
||||
netWorth: fx.netWorth,
|
||||
month: { ...fx.analyzeMonth, incomePayees: [], expensePayees: [] },
|
||||
today: null,
|
||||
budget: fx.budget,
|
||||
})!;
|
||||
expect(h.income.length).toBeGreaterThan(0);
|
||||
expect(h.expense.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
128
src/features/home/buildHome.ts
Normal file
128
src/features/home/buildHome.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import type { NetWorth } from "../../api/schemas/networth";
|
||||
import type { Analyze } from "../../api/schemas/analyze";
|
||||
import type { Budget } from "../../api/schemas/budget";
|
||||
import { tugrik, tugrikRaw } from "../../ds/money";
|
||||
import { sample } from "./sample";
|
||||
|
||||
/** Parse a backend decimal string ("900000") into a number. Mirrors
|
||||
* `Decimal(string:) ?? 0` in HomeData.swift's local `dec` helper. */
|
||||
function dec(s?: string | null): number {
|
||||
if (!s) return 0;
|
||||
const n = Number(s);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
/** One budgeted category row on the blue limit card. */
|
||||
export interface BudgetRow {
|
||||
name: string;
|
||||
spent: number;
|
||||
/** Formatted target amount, e.g. "27,000₮". */
|
||||
range: string;
|
||||
}
|
||||
|
||||
/** One income/expense entry under the Орлого / Зарлага cards. Field names
|
||||
* mirror iOS's `LedgerRow` exactly: `title` holds the formatted amount,
|
||||
* `date` holds a label (the payee name on the real-data path, a literal
|
||||
* date string in `.sample`). */
|
||||
export interface LedgerRow {
|
||||
title: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
/** Everything the dashboard renders. Populated from the backend where
|
||||
* endpoints exist (balance, monthly spend); the rest uses representative
|
||||
* sample values until the matching backend features land. */
|
||||
export interface HomeData {
|
||||
totalBalance: number;
|
||||
todayExpense: number;
|
||||
dailyLimitUsed: number;
|
||||
dailyLimitTotal: number;
|
||||
budgets: BudgetRow[];
|
||||
monthlyExpense: number;
|
||||
/** The monthly budget denominator: the user's explicit monthly cap when
|
||||
* set (> 0), otherwise salary-derived disposable income. */
|
||||
availableBudget: number;
|
||||
income: LedgerRow[];
|
||||
expense: LedgerRow[];
|
||||
/** What's left to spend this month, floored at 0. */
|
||||
safeToSpend: number;
|
||||
/** Whether monthly spend has exceeded the available budget. */
|
||||
overspent: boolean;
|
||||
}
|
||||
|
||||
export interface BuildHomeInput {
|
||||
netWorth: NetWorth | null | undefined;
|
||||
month: Analyze | null | undefined;
|
||||
today: Analyze | null | undefined;
|
||||
budget: Budget | null | undefined;
|
||||
}
|
||||
|
||||
/** Build real dashboard data from the backend. Balance comes from /networth;
|
||||
* this-month spend, the spend breakdown, and the income/expense ledgers come
|
||||
* from /analyze (month) via real payees; today's spend from /analyze
|
||||
* (today). Returns null when the account has no data yet, so the caller
|
||||
* keeps `sample`. Ported EXACTLY from `HomeData.live` in
|
||||
* ios/Mercury/Features/Home/HomeData.swift. */
|
||||
export function buildHome({ netWorth, month, today, budget }: BuildHomeInput): HomeData | null {
|
||||
if (!month) return null;
|
||||
|
||||
const balance = dec(netWorth?.total);
|
||||
// "Spent" = real discretionary spend (excludes the loan payment + transfers),
|
||||
// so the safe-to-spend hero and the monthly figure aren't inflated by money
|
||||
// that's already committed or just moved between people/accounts.
|
||||
const monthlyExpense = dec(month.discretionaryExpense ?? month.expense);
|
||||
const payees = month.expensePayees ?? [];
|
||||
const incomePayees = month.incomePayees ?? [];
|
||||
|
||||
// An empty account → no real data; fall back to the sample.
|
||||
if (balance === 0 && monthlyExpense === 0 && payees.length === 0) return null;
|
||||
|
||||
const todayExpense = dec(today?.expense);
|
||||
|
||||
// Hero budget (the "Энэ сар зарцуулж болох" denominator): honor the user's
|
||||
// explicitly-set monthly limit when present; only fall back to salary-
|
||||
// derived disposable income (planned income − loans − savings − subs) when
|
||||
// no limit is set. Paired with the DISCRETIONARY monthlyExpense above (not
|
||||
// the budget report's all-expense overallSpent), since the cap is a
|
||||
// spending cap.
|
||||
const monthLimit = dec(budget?.monthLimit);
|
||||
const availableBudget = monthLimit > 0 ? monthLimit : dec(budget?.availableIncome);
|
||||
|
||||
// Daily limit + per-category budgets come from the budget's DAY horizon —
|
||||
// the same source as the Төлөвлөгөө hub (real limits, no payee fallback).
|
||||
const dayReport = budget?.report.day;
|
||||
const dailyLimitTotal = dec(dayReport?.overallLimit);
|
||||
const dailyLimitUsed = dec(dayReport?.overallSpent);
|
||||
// Only the categories with a real daily limit, biggest first — a proper
|
||||
// daily budget (e.g. Хоол хүнс, Кофе, Тээвэр), not the uncategorized bucket.
|
||||
const budgets: BudgetRow[] = (dayReport?.rows ?? [])
|
||||
.filter((row) => dec(row.limit) > 0)
|
||||
.sort((a, b) => dec(b.limit) - dec(a.limit))
|
||||
.slice(0, 3)
|
||||
.map((row) => ({
|
||||
name: row.category,
|
||||
spent: dec(row.spent),
|
||||
range: tugrikRaw(dec(row.limit)),
|
||||
}));
|
||||
|
||||
const income: LedgerRow[] = incomePayees
|
||||
.slice(0, 2)
|
||||
.map((p) => ({ title: tugrik(dec(p.total)), date: p.name }));
|
||||
const expense: LedgerRow[] = payees
|
||||
.slice(0, 2)
|
||||
.map((p) => ({ title: tugrik(dec(p.total)), date: p.name }));
|
||||
|
||||
return {
|
||||
totalBalance: balance,
|
||||
todayExpense,
|
||||
dailyLimitUsed,
|
||||
dailyLimitTotal,
|
||||
budgets,
|
||||
monthlyExpense,
|
||||
availableBudget,
|
||||
income: income.length === 0 ? sample.income : income,
|
||||
expense: expense.length === 0 ? sample.expense : expense,
|
||||
safeToSpend: Math.max(0, availableBudget - monthlyExpense),
|
||||
overspent: monthlyExpense > availableBudget,
|
||||
};
|
||||
}
|
||||
25
src/features/home/sample.ts
Normal file
25
src/features/home/sample.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { HomeData } from "./buildHome";
|
||||
|
||||
const monthlyExpense = 1_200_000;
|
||||
const availableBudget = 2_036_795;
|
||||
|
||||
/** Figma sample state (also the fallback before a bank is connected). Ported
|
||||
* verbatim from `HomeData.sample` in
|
||||
* ios/Mercury/Features/Home/HomeData.swift. */
|
||||
export const sample: HomeData = {
|
||||
totalBalance: 452_200,
|
||||
todayExpense: 52_000,
|
||||
dailyLimitUsed: 15_500,
|
||||
dailyLimitTotal: 35_500,
|
||||
budgets: [
|
||||
{ name: "Хоол", spent: 23_700, range: "22,000₮ - 27,000₮" },
|
||||
{ name: "Coffee", spent: 23_700, range: "4500₮ - 6000₮" },
|
||||
{ name: "Тээвэр", spent: 0, range: "5000₮" },
|
||||
],
|
||||
monthlyExpense,
|
||||
availableBudget,
|
||||
income: [{ title: "1,345,634₮", date: "04.01" }],
|
||||
expense: [{ title: "худалдан авалт", date: "04.01" }],
|
||||
safeToSpend: Math.max(0, availableBudget - monthlyExpense),
|
||||
overspent: monthlyExpense > availableBudget,
|
||||
};
|
||||
16
src/features/home/strings.ts
Normal file
16
src/features/home/strings.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Home dashboard copy, ported verbatim from
|
||||
// ios/Mercury/Features/Home/DashboardView.swift.
|
||||
export const homeStrings = {
|
||||
wordmark: "MERCURY",
|
||||
safeToSpend: "Энэ сар зарцуулж болох",
|
||||
overspent: "Төсвөөс хэтэрсэн",
|
||||
spent: "Зарцуулсан",
|
||||
dailyLimit: "Өнөөдрийн лимит",
|
||||
thisMonth: "Энэ сард",
|
||||
expenseLabel: "зарлага",
|
||||
viewAll: "Бүгдийг харах",
|
||||
hideAmounts: "Мөнгөн дүн нуух",
|
||||
income: "Орлого",
|
||||
expense: "Зарлага",
|
||||
todaySpent: "Өнөөдөр зарцуулсан",
|
||||
} as const;
|
||||
Loading…
Add table
Reference in a new issue