diff --git a/src/ds/categoryStyle.ts b/src/ds/categoryStyle.ts index 5e5c933..95e90d8 100644 --- a/src/ds/categoryStyle.ts +++ b/src/ds/categoryStyle.ts @@ -1,58 +1,59 @@ +import type { IconName } from "./icons"; + // Mongolian display name + icon + color for a backend category. Categories are // stored under English names; this is the single place that maps each to how it // reads and looks in the UI. Ported from ios/Mercury/Features/Transactions/ -// CategoryStyle.swift — the web has no Seed multicolor-icon catalog, so each -// icon is an emoji + a soft color tint for the category chip. +// CategoryStyle.swift. export interface CategoryStyle { name: string; // Mongolian display name - emoji: string; + icon: IconName; tint: string; // chip background (soft) fg: string; // chip foreground / accent } -const s = (name: string, emoji: string, tint: string, fg: string): CategoryStyle => ({ name, emoji, tint, fg }); +const s = (name: string, icon: IconName, tint: string, fg: string): CategoryStyle => ({ name, icon, tint, fg }); const TABLE: Record = { // Food & drink - "food & drink": s("Хоол хүнс", "🍴", "#FDE8D7", "#B4530A"), - groceries: s("Хүнсний бараа", "🛒", "#E3F0D8", "#3F7A1E"), - dining: s("Хоолны газар", "🍱", "#FDE8D7", "#B4530A"), - dinner: s("Оройн хоол", "🍽️", "#FDE8D7", "#B4530A"), - coffee: s("Кофе", "☕", "#EEE3D7", "#7A5230"), - drink: s("Ундаа", "🥤", "#DDEAF6", "#215C9A"), + "food & drink": s("Хоол хүнс", "utensils", "#FDE8D7", "#B4530A"), + groceries: s("Хүнсний бараа", "cart", "#E3F0D8", "#3F7A1E"), + dining: s("Хоолны газар", "utensils", "#FDE8D7", "#B4530A"), + dinner: s("Оройн хоол", "utensils", "#FDE8D7", "#B4530A"), + coffee: s("Кофе", "coffee", "#EEE3D7", "#7A5230"), + drink: s("Ундаа", "coffee", "#DDEAF6", "#215C9A"), // Transport - transport: s("Тээвэр", "🚗", "#DDE6F6", "#2A4B9A"), + transport: s("Тээвэр", "car", "#DDE6F6", "#2A4B9A"), // Bills & services - "bills & services": s("Төлбөр & үйлчилгээ", "💳", "#E7E3F6", "#5B44B0"), - fees: s("Шимтгэл", "🧾", "#EDEBE7", "#6B6257"), - insurance: s("Даатгал", "🛡️", "#DFEFEA", "#1E7A63"), - phone: s("Утас", "📱", "#E1E5EA", "#42505F"), - subscriptions: s("Сабскрипшн", "✨", "#F5E7F0", "#9A2E77"), - "үйлчилгээ": s("Үйлчилгээ", "🔧", "#EDEBE7", "#6B6257"), + "bills & services": s("Төлбөр & үйлчилгээ", "card", "#E7E3F6", "#5B44B0"), + fees: s("Шимтгэл", "receipt", "#EDEBE7", "#6B6257"), + insurance: s("Даатгал", "shield", "#DFEFEA", "#1E7A63"), + phone: s("Утас", "phone", "#E1E5EA", "#42505F"), + subscriptions: s("Сабскрипшн", "sparkles", "#F5E7F0", "#9A2E77"), + "үйлчилгээ": s("Үйлчилгээ", "wrench", "#EDEBE7", "#6B6257"), // Debt & loans - "debt & loans": s("Зээл & өр", "🛡️", "#F6E3E3", "#A83232"), - debt: s("Өр", "🛡️", "#F6E3E3", "#A83232"), - loan: s("Зээл", "💳", "#E7E3F6", "#5B44B0"), + "debt & loans": s("Зээл & өр", "shield", "#F6E3E3", "#A83232"), + debt: s("Өр", "shield", "#F6E3E3", "#A83232"), + loan: s("Зээл", "card", "#E7E3F6", "#5B44B0"), // Shopping - shopping: s("Дэлгүүр", "🛍️", "#F5E7F0", "#9A2E77"), - electronics: s("Цахилгаан бараа", "🖥️", "#E1E5EA", "#42505F"), + shopping: s("Дэлгүүр", "bag", "#F5E7F0", "#9A2E77"), + electronics: s("Цахилгаан бараа", "monitor", "#E1E5EA", "#42505F"), // Entertainment - entertainment: s("Зугаа цэнгэл", "🎮", "#E7E3F6", "#5B44B0"), - anime: s("Аниме", "🎮", "#E7E3F6", "#5B44B0"), - spotify: s("Spotify", "🎵", "#E3F0D8", "#3F7A1E"), + entertainment: s("Зугаа цэнгэл", "gamepad", "#E7E3F6", "#5B44B0"), + anime: s("Аниме", "gamepad", "#E7E3F6", "#5B44B0"), + spotify: s("Spotify", "sparkles", "#E3F0D8", "#3F7A1E"), // Health & care - fitness: s("Фитнес", "🏋️", "#DDEAF6", "#215C9A"), - haircut: s("Үс засалт", "✂️", "#F5E7F0", "#9A2E77"), + fitness: s("Фитнес", "dumbbell", "#DDEAF6", "#215C9A"), + haircut: s("Үс засалт", "scissors", "#F5E7F0", "#9A2E77"), // Money movement / income - transfers: s("Шилжүүлэг", "🔁", "#E1E5EA", "#42505F"), - salary: s("Цалин", "💰", "#E3F0D8", "#3F7A1E"), + transfers: s("Шилжүүлэг", "repeat", "#E1E5EA", "#42505F"), + salary: s("Цалин", "wallet", "#E3F0D8", "#3F7A1E"), // Synthetic budget line for the loan repayment. - "зээлийн төлбөр": s("Зээлийн төлбөр", "🛡️", "#F6E3E3", "#A83232"), + "зээлийн төлбөр": s("Зээлийн төлбөр", "shield", "#F6E3E3", "#A83232"), }; -const INCOME_FALLBACK = s("Орлого", "💰", "#E3F0D8", "#3F7A1E"); -const OTHER_FALLBACK = s("Бусад", "🧾", "#EDEBE7", "#6B6257"); +const INCOME_FALLBACK = s("Орлого", "wallet", "#E3F0D8", "#3F7A1E"); +const OTHER_FALLBACK = s("Бусад", "receipt", "#EDEBE7", "#6B6257"); /** Look up the style for a backend category name. `income` decides the fallback * when the category is empty or unknown. Mirrors CategoryStyle.of in iOS. */ diff --git a/src/ds/icons.tsx b/src/ds/icons.tsx new file mode 100644 index 0000000..85b08f9 --- /dev/null +++ b/src/ds/icons.tsx @@ -0,0 +1,211 @@ +import type { SVGProps } from "react"; + +// Clean line-icon set (stroke, currentColor) — a dependency-free stand-in for +// the Seed multicolor catalog, which has no web build. 24x24, 1.75 stroke, +// round caps/joins. Color via `color` / currentColor; size via `size`. + +export type IconName = + | "utensils" + | "cart" + | "coffee" + | "car" + | "card" + | "shield" + | "phone" + | "sparkles" + | "bag" + | "monitor" + | "gamepad" + | "dumbbell" + | "scissors" + | "repeat" + | "wallet" + | "receipt" + | "wrench" + | "home" + | "list" + | "calendar-check" + | "layers" + | "user" + | "plus" + | "chevron-right" + | "chevron-left" + | "eye" + | "eye-off" + | "arrow-up-right" + | "arrow-down-left" + | "trending" + | "hand-coins" + | "bank"; + +const PATHS: Record = { + utensils: ( + <> + + + + ), + cart: ( + <> + + + + + ), + coffee: ( + <> + + + + + ), + car: ( + <> + + + + + + ), + card: ( + <> + + + + ), + shield: , + phone: ( + <> + + + + ), + sparkles: ( + <> + + + + ), + bag: ( + <> + + + + ), + monitor: ( + <> + + + + ), + gamepad: ( + <> + + + + ), + dumbbell: ( + <> + + + ), + scissors: ( + <> + + + + ), + repeat: ( + <> + + + + ), + wallet: ( + <> + + + + + ), + receipt: ( + <> + + + + ), + wrench: , + home: ( + <> + + + + ), + list: , + "calendar-check": ( + <> + + + + ), + layers: ( + <> + + + + ), + user: ( + <> + + + + ), + plus: , + "chevron-right": , + "chevron-left": , + eye: ( + <> + + + + ), + "eye-off": ( + <> + + + + ), + "arrow-up-right": , + "arrow-down-left": , + trending: , + "hand-coins": ( + <> + + + + ), + bank: ( + <> + + + ), +}; + +export function Icon({ name, size = 22, ...rest }: { name: IconName; size?: number } & Omit, "name">) { + return ( + + {PATHS[name]} + + ); +} diff --git a/src/ds/index.ts b/src/ds/index.ts index 3e95213..6b84b0d 100644 --- a/src/ds/index.ts +++ b/src/ds/index.ts @@ -16,3 +16,4 @@ export type { NameEditProps } from "./NameEdit"; export { Card } from "./Card"; export type { CardProps } from "./Card"; export * from "./categoryStyle"; +export * from "./icons"; diff --git a/src/features/accounting/TransactionDetail.tsx b/src/features/accounting/TransactionDetail.tsx index 48ee9ee..5b4d5ff 100644 --- a/src/features/accounting/TransactionDetail.tsx +++ b/src/features/accounting/TransactionDetail.tsx @@ -13,6 +13,7 @@ import { DialogDescription, DialogFooter, DialogAction, + Skeleton, TextFieldRoot, TextFieldTextarea, } from "@seed-design/react"; @@ -100,7 +101,7 @@ function DetailRow({ */ export function TransactionDetail({ id }: TransactionDetailProps) { const router = useRouter(); - const { data: transactions } = useTransactions(); + const { data: transactions, isLoading } = useTransactions(); const { data: categories = [] } = useCategories(); const categorize = useCategorize(); const renameTxn = useRenameTxn(); @@ -132,6 +133,15 @@ export function TransactionDetail({ id }: TransactionDetailProps) { setEditingNote(false); }, [id]); + if (isLoading && !txn) { + return ( +
+ router.push("/accounting")} /> + +
+ ); + } + if (!txn) { return (
diff --git a/src/features/accounting/TransactionList.tsx b/src/features/accounting/TransactionList.tsx index 6529567..e9369b8 100644 --- a/src/features/accounting/TransactionList.tsx +++ b/src/features/accounting/TransactionList.tsx @@ -7,6 +7,7 @@ import { useTransactions } from "@/api/hooks/reads"; import type { Txn } from "@/api/schemas"; import { Card, HideAmountsToggle } from "@/ds"; import { categoryStyle } from "@/ds/categoryStyle"; +import { Icon } from "@/ds/icons"; import { MASKED, tugrikRaw } from "@/ds/money"; import { accountingStrings as s } from "./strings"; import { txnRouteId } from "./txnRoute"; @@ -100,11 +101,11 @@ function TxnRow({ txn, hidden }: { txn: Txn; hidden: boolean }) { borderRadius: 13, display: "grid", placeItems: "center", - fontSize: 19, + color: isTransfer ? "var(--seed-color-fg-neutral-muted, #6b7280)" : cat.fg, background: isTransfer ? "var(--seed-color-bg-neutral-subtle, #eef0f2)" : cat.tint, }} > - {isTransfer ? "🔁" : cat.emoji} +
diff --git a/src/features/planner/PlannerView.tsx b/src/features/planner/PlannerView.tsx index e33842f..625eb5e 100644 --- a/src/features/planner/PlannerView.tsx +++ b/src/features/planner/PlannerView.tsx @@ -23,6 +23,7 @@ import { } from "@seed-design/react"; import { Card, MercuryButton, HideAmountsToggle, NameEdit } from "@/ds"; import { categoryStyle } from "@/ds/categoryStyle"; +import { Icon } from "@/ds/icons"; import { tugrik, tugrikShort } from "@/ds/money"; import { useBudget, useNetWorth } from "@/api/hooks/reads"; import { usePutBudget, useSavingsGoalMutations } from "@/api/hooks/mutations"; @@ -479,8 +480,21 @@ function CategoryList({ style={{ color: "inherit", textDecoration: "none" }} >
- - {categoryStyle(row.category).emoji} + + + + {categoryStyle(row.category).name}