feat(web): real inline-SVG icon set (replace emoji); txn detail loading skeleton
This commit is contained in:
parent
06e74b1668
commit
66d82b8b68
6 changed files with 275 additions and 37 deletions
|
|
@ -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<string, CategoryStyle> = {
|
||||
// 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. */
|
||||
|
|
|
|||
211
src/ds/icons.tsx
Normal file
211
src/ds/icons.tsx
Normal file
|
|
@ -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<IconName, React.ReactNode> = {
|
||||
utensils: (
|
||||
<>
|
||||
<path d="M4 3v6a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V3M6 11v10" />
|
||||
<path d="M17 3c-1.5 0-3 1.5-3 4.5S15.5 12 17 12v9" />
|
||||
</>
|
||||
),
|
||||
cart: (
|
||||
<>
|
||||
<circle cx="9" cy="20" r="1.4" />
|
||||
<circle cx="18" cy="20" r="1.4" />
|
||||
<path d="M2 3h2.2l2 12.4a1.5 1.5 0 0 0 1.5 1.2h9.1a1.5 1.5 0 0 0 1.5-1.2L20.5 7H5.2" />
|
||||
</>
|
||||
),
|
||||
coffee: (
|
||||
<>
|
||||
<path d="M4 8h13v5a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5V8Z" />
|
||||
<path d="M17 9h2.5a2.5 2.5 0 0 1 0 5H17" />
|
||||
<path d="M8 2c-.6.8-.6 1.7 0 2.5M12 2c-.6.8-.6 1.7 0 2.5" />
|
||||
</>
|
||||
),
|
||||
car: (
|
||||
<>
|
||||
<path d="M5 12l1.5-4.5A2 2 0 0 1 8.4 6h7.2a2 2 0 0 1 1.9 1.5L19 12" />
|
||||
<path d="M3 12h18v4a1 1 0 0 1-1 1h-1.5M5.5 17H4a1 1 0 0 1-1-1v-4" />
|
||||
<path d="M6.5 17v1.5M17.5 17v1.5" />
|
||||
<circle cx="7" cy="14.5" r=".6" /><circle cx="17" cy="14.5" r=".6" />
|
||||
</>
|
||||
),
|
||||
card: (
|
||||
<>
|
||||
<rect x="2.5" y="5" width="19" height="14" rx="2.5" />
|
||||
<path d="M2.5 9.5h19" />
|
||||
</>
|
||||
),
|
||||
shield: <path d="M12 3l7 2.5v5.5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V5.5L12 3Z" />,
|
||||
phone: (
|
||||
<>
|
||||
<rect x="6.5" y="2.5" width="11" height="19" rx="2.5" />
|
||||
<path d="M11 18.5h2" />
|
||||
</>
|
||||
),
|
||||
sparkles: (
|
||||
<>
|
||||
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3Z" />
|
||||
<path d="M18 14l.8 2.2L21 17l-2.2.8L18 20l-.8-2.2L15 17l2.2-.8L18 14Z" />
|
||||
</>
|
||||
),
|
||||
bag: (
|
||||
<>
|
||||
<path d="M5.5 8h13l-.9 11a2 2 0 0 1-2 1.8H8.4a2 2 0 0 1-2-1.8L5.5 8Z" />
|
||||
<path d="M9 8V6.5a3 3 0 0 1 6 0V8" />
|
||||
</>
|
||||
),
|
||||
monitor: (
|
||||
<>
|
||||
<rect x="3" y="4" width="18" height="12" rx="2" />
|
||||
<path d="M9 20h6M12 16v4" />
|
||||
</>
|
||||
),
|
||||
gamepad: (
|
||||
<>
|
||||
<path d="M7 9h10a4 4 0 0 1 4 4v.5a3.5 3.5 0 0 1-6.3 2.1L14 15h-4l-.7.6A3.5 3.5 0 0 1 3 13.5V13a4 4 0 0 1 4-4Z" />
|
||||
<path d="M7.5 12v2M6.5 13h2M15.5 12.5h.01M17.5 14.5h.01" />
|
||||
</>
|
||||
),
|
||||
dumbbell: (
|
||||
<>
|
||||
<path d="M3 9v6M6 7v10M18 7v10M21 9v6M6 12h12" />
|
||||
</>
|
||||
),
|
||||
scissors: (
|
||||
<>
|
||||
<circle cx="6" cy="6" r="2.2" /><circle cx="6" cy="18" r="2.2" />
|
||||
<path d="M8 7.5 20 18M8 16.5 20 6" />
|
||||
</>
|
||||
),
|
||||
repeat: (
|
||||
<>
|
||||
<path d="M3 12V9a3 3 0 0 1 3-3h11M14 3l3 3-3 3" />
|
||||
<path d="M21 12v3a3 3 0 0 1-3 3H7M10 21l-3-3 3-3" />
|
||||
</>
|
||||
),
|
||||
wallet: (
|
||||
<>
|
||||
<path d="M3.5 7.5A2 2 0 0 1 5.5 5.5h11a1.5 1.5 0 0 1 0 3H4" />
|
||||
<rect x="3" y="7.5" width="18" height="11.5" rx="2.5" />
|
||||
<circle cx="16.5" cy="13.5" r="1.1" />
|
||||
</>
|
||||
),
|
||||
receipt: (
|
||||
<>
|
||||
<path d="M6 3h12v18l-2-1.3-2 1.3-2-1.3-2 1.3-2-1.3L6 21V3Z" />
|
||||
<path d="M9 8h6M9 12h6" />
|
||||
</>
|
||||
),
|
||||
wrench: <path d="M14.5 6.5a3.5 3.5 0 0 0 4.4 4.4l-8.9 8.9a2 2 0 0 1-2.8-2.8l8.9-8.9a3.5 3.5 0 0 0-1.6-1.6Z" />,
|
||||
home: (
|
||||
<>
|
||||
<path d="M4 11 12 4l8 7" />
|
||||
<path d="M6 10v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-9" />
|
||||
</>
|
||||
),
|
||||
list: <path d="M4 7h16M4 12h16M4 17h16" />,
|
||||
"calendar-check": (
|
||||
<>
|
||||
<rect x="3.5" y="5" width="17" height="16" rx="2.5" />
|
||||
<path d="M3.5 9.5h17M8 3v4M16 3v4M8.5 15l2.2 2.2L15.5 12" />
|
||||
</>
|
||||
),
|
||||
layers: (
|
||||
<>
|
||||
<path d="M12 3 3 8l9 5 9-5-9-5Z" />
|
||||
<path d="M3 13l9 5 9-5M3 18l9 5 9-5" opacity=".5" />
|
||||
</>
|
||||
),
|
||||
user: (
|
||||
<>
|
||||
<circle cx="12" cy="8" r="3.5" />
|
||||
<path d="M5 20a7 7 0 0 1 14 0" />
|
||||
</>
|
||||
),
|
||||
plus: <path d="M12 5v14M5 12h14" />,
|
||||
"chevron-right": <path d="m9 5 7 7-7 7" />,
|
||||
"chevron-left": <path d="m15 5-7 7 7 7" />,
|
||||
eye: (
|
||||
<>
|
||||
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
|
||||
<circle cx="12" cy="12" r="2.6" />
|
||||
</>
|
||||
),
|
||||
"eye-off": (
|
||||
<>
|
||||
<path d="M4 4l16 16" />
|
||||
<path d="M9.5 9.6A2.6 2.6 0 0 0 12 14.5a2.6 2.6 0 0 0 2.4-1.6M6.5 6.8C4 8.4 2.5 12 2.5 12s3.5 6.5 9.5 6.5c1.5 0 2.8-.4 4-1M16.5 15.2C19 13.6 21.5 12 21.5 12s-3-5.5-8-6.4" />
|
||||
</>
|
||||
),
|
||||
"arrow-up-right": <path d="M7 17 17 7M9 7h8v8" />,
|
||||
"arrow-down-left": <path d="M17 7 7 17M15 17H7V9" />,
|
||||
trending: <path d="m3 16 5-5 4 4 8-8M15 7h6v6" />,
|
||||
"hand-coins": (
|
||||
<>
|
||||
<circle cx="8" cy="6" r="3" />
|
||||
<path d="M13 9h4.5a2 2 0 0 1 0 4H14M3 20l3-3h6a2 2 0 0 0 2-2" />
|
||||
</>
|
||||
),
|
||||
bank: (
|
||||
<>
|
||||
<path d="M3 9.5 12 4l9 5.5M4.5 9.5V18M19.5 9.5V18M8 10v6M12 10v6M16 10v6M3 20h18" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
export function Icon({ name, size = 22, ...rest }: { name: IconName; size?: number } & Omit<SVGProps<SVGSVGElement>, "name">) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.75}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
{...rest}
|
||||
>
|
||||
{PATHS[name]}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,3 +16,4 @@ export type { NameEditProps } from "./NameEdit";
|
|||
export { Card } from "./Card";
|
||||
export type { CardProps } from "./Card";
|
||||
export * from "./categoryStyle";
|
||||
export * from "./icons";
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<BackButton onClick={() => router.push("/accounting")} />
|
||||
<Skeleton style={{ height: 300, width: "100%", borderRadius: "var(--seed-radius-r3)" }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!txn) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<Icon name={isTransfer ? "repeat" : cat.icon} size={20} />
|
||||
</span>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
|
|
|
|||
|
|
@ -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" }}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 8, fontWeight: 700 }}>
|
||||
<span aria-hidden>{categoryStyle(row.category).emoji}</span>
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 10, fontWeight: 700 }}>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 11,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: categoryStyle(row.category).fg,
|
||||
background: categoryStyle(row.category).tint,
|
||||
}}
|
||||
>
|
||||
<Icon name={categoryStyle(row.category).icon} size={18} />
|
||||
</span>
|
||||
{categoryStyle(row.category).name}
|
||||
</span>
|
||||
<span style={{ fontWeight: 700, color: over ? "var(--seed-color-fg-critical)" : "var(--seed-color-fg-placeholder)" }}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue