170 lines
5.7 KiB
TypeScript
170 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { Card, HideAmountsToggle, Icon, IconChip, MercuryButton, ThemeToggle } from "@/ds";
|
|
import type { IconName } from "@/ds";
|
|
import { useMe, useSettings } from "@/api/hooks/reads";
|
|
import { profileStrings } from "./strings";
|
|
import { SettingsForm } from "./SettingsForm";
|
|
import { ConnectedBanks } from "./ConnectedBanks";
|
|
|
|
/** The Миний (profile) tab hub: account summary, hide-amounts toggle,
|
|
* entries into Categories/Subscriptions (their own routes) and Settings
|
|
* (rendered inline, no dedicated route), the read-only connected-banks list,
|
|
* and logout. Ports `ProfileView.swift`'s layout minus bank connect/disconnect
|
|
* (mobile-only per the Task 13 brief) and the not-yet-wired static rows
|
|
* (Шинэ мэдээ / Түгээмэл асуултууд / Санал хүсэлт / Үйлчилгээний нөхцөл). */
|
|
export function ProfileView() {
|
|
const router = useRouter();
|
|
const { data: me } = useMe();
|
|
const { data: settings } = useSettings();
|
|
const [showSettings, setShowSettings] = React.useState(false);
|
|
const [loggingOut, setLoggingOut] = React.useState(false);
|
|
|
|
const emailLocal = me?.email ? me.email.split("@")[0] : "";
|
|
const displayName = settings?.holderName || emailLocal || me?.email || "";
|
|
const initial = (displayName || me?.email || "?").trim().charAt(0).toUpperCase() || "?";
|
|
|
|
async function handleLogout() {
|
|
setLoggingOut(true);
|
|
try {
|
|
await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin" });
|
|
} finally {
|
|
router.push("/login");
|
|
}
|
|
}
|
|
|
|
if (showSettings) {
|
|
return <SettingsForm onBack={() => setShowSettings(false)} />;
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
<h1 style={{ fontSize: 18, fontWeight: 600, margin: 0 }}>{profileStrings.header.title}</h1>
|
|
<HideAmountsToggle label={profileStrings.display.hideAmounts} />
|
|
</div>
|
|
|
|
<Card style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
|
<span
|
|
aria-hidden
|
|
style={{
|
|
width: 56,
|
|
height: 56,
|
|
flexShrink: 0,
|
|
borderRadius: 28,
|
|
display: "grid",
|
|
placeItems: "center",
|
|
fontSize: 22,
|
|
fontWeight: 800,
|
|
color: "var(--mercury-on-brand)",
|
|
background: "var(--mercury-brand-yellow)",
|
|
}}
|
|
>
|
|
{initial}
|
|
</span>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
|
<span
|
|
style={{
|
|
fontSize: 16,
|
|
fontWeight: 700,
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{displayName}
|
|
</span>
|
|
{me?.email && (
|
|
<span
|
|
style={{
|
|
fontSize: 13,
|
|
color: "var(--seed-color-fg-muted, #6b7280)",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{me.email}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card style={{ display: "flex", flexDirection: "column" }}>
|
|
<nav style={{ display: "flex", flexDirection: "column" }}>
|
|
<MenuRow icon="wrench" label={profileStrings.menu.settings} onClick={() => setShowSettings(true)} />
|
|
<MenuLink icon="list" label={profileStrings.menu.categories} href="/profile/categories" />
|
|
<MenuLink icon="sparkles" label={profileStrings.menu.subscriptions} href="/profile/subscriptions" />
|
|
</nav>
|
|
</Card>
|
|
|
|
<Card style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
<span style={{ fontSize: 13, fontWeight: 600, color: "var(--seed-color-fg-muted, #6b7280)" }}>
|
|
{profileStrings.display.themeMode}
|
|
</span>
|
|
<ThemeToggle
|
|
labels={{
|
|
system: profileStrings.display.themeSystem,
|
|
light: profileStrings.display.themeLight,
|
|
dark: profileStrings.display.themeDark,
|
|
}}
|
|
/>
|
|
</Card>
|
|
|
|
<Card>
|
|
<ConnectedBanks />
|
|
</Card>
|
|
|
|
<MercuryButton variant="secondary" onClick={handleLogout} loading={loggingOut}>
|
|
{profileStrings.logout}
|
|
</MercuryButton>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const menuRowStyle: React.CSSProperties = {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: 12,
|
|
width: "100%",
|
|
padding: "10px 0",
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
font: "inherit",
|
|
color: "inherit",
|
|
textAlign: "left",
|
|
textDecoration: "none",
|
|
};
|
|
|
|
function MenuRowContent({ icon, label }: { icon: IconName; label: string }) {
|
|
return (
|
|
<>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
|
<IconChip icon={icon} tint="var(--seed-color-bg-neutral-subtle, #eef0f2)" fg="var(--seed-color-fg-neutral)" />
|
|
<span style={{ fontSize: 15, fontWeight: 600 }}>{label}</span>
|
|
</div>
|
|
<Icon name="chevron-right" size={18} style={{ color: "var(--seed-color-fg-placeholder, #9ca3af)" }} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
function MenuRow({ icon, label, onClick }: { icon: IconName; label: string; onClick: () => void }) {
|
|
return (
|
|
<button type="button" onClick={onClick} style={menuRowStyle}>
|
|
<MenuRowContent icon={icon} label={label} />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function MenuLink({ icon, label, href }: { icon: IconName; label: string; href: string }) {
|
|
return (
|
|
<Link href={href} style={menuRowStyle}>
|
|
<MenuRowContent icon={icon} label={label} />
|
|
</Link>
|
|
);
|
|
}
|