feat(web): rework profile + auth visuals to reference bar
Elevate the Profile (Миний) tab and login/register/forgot screens to match TransactionList's IconChip + Card row pattern: - Profile: avatar medallion (initial + brand tint) + name/email header, IconChip menu rows (Тохиргоо/Ангилал/Захиалга) with chevrons, IconChip + EmptyState connected-banks list. - Categories/Subscriptions: IconChip rows via categoryStyle, EmptyState for empty lists, chevron-left back buttons. - Auth: shared AuthHeader with a tinted monogram + wordmark lockup, more comfortable field/CTA spacing in AuthForm. Visual only — no changes to hooks, mutations, or auth actions.
This commit is contained in:
parent
e0c07be232
commit
059b7e13d5
10 changed files with 336 additions and 277 deletions
|
|
@ -1,45 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AuthForm } from "@/features/auth/AuthForm";
|
||||
import { authStrings } from "@/features/auth/strings";
|
||||
import { AuthHeader } from "@/features/auth/AuthHeader";
|
||||
|
||||
// Ports ios/Mercury/Features/Auth/ForgotPasswordView.swift: back chevron,
|
||||
// MERCURY wordmark, explanatory subtitle (rendered by AuthForm), the email
|
||||
// field, and the "Сэргээх холбоос авах" CTA.
|
||||
export default function ForgotPasswordPage() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
aria-label="Буцах"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
alignSelf: "flex-start",
|
||||
marginTop: 24,
|
||||
fontSize: 20,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
<div style={{ height: 72 }} />
|
||||
|
||||
<h1
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{authStrings.wordmark}
|
||||
</h1>
|
||||
<AuthHeader />
|
||||
|
||||
<div style={{ flex: 1, minHeight: 40 }} />
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AuthForm } from "@/features/auth/AuthForm";
|
||||
import { authStrings } from "@/features/auth/strings";
|
||||
import { AuthHeader } from "@/features/auth/AuthHeader";
|
||||
|
||||
// Ports ios/Mercury/Features/Auth/LoginView.swift: back chevron, MERCURY
|
||||
// wordmark, then the login form (email + password + forgot link + CTAs).
|
||||
|
|
@ -11,34 +11,7 @@ export default function LoginPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
aria-label="Буцах"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
alignSelf: "flex-start",
|
||||
marginTop: 24,
|
||||
fontSize: 20,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
<div style={{ height: 72 }} />
|
||||
|
||||
<h1
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{authStrings.wordmark}
|
||||
</h1>
|
||||
<AuthHeader />
|
||||
|
||||
<div style={{ flex: 1, minHeight: 40 }} />
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AuthForm } from "@/features/auth/AuthForm";
|
||||
import { authStrings } from "@/features/auth/strings";
|
||||
import { AuthHeader } from "@/features/auth/AuthHeader";
|
||||
|
||||
// Ports ios/Mercury/Features/Auth/RegisterView.swift: back chevron, MERCURY
|
||||
// wordmark, then the registration form (email + password + confirm + CTAs).
|
||||
|
|
@ -11,34 +11,7 @@ export default function RegisterPage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
aria-label="Буцах"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
alignSelf: "flex-start",
|
||||
marginTop: 24,
|
||||
fontSize: 20,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
<div style={{ height: 72 }} />
|
||||
|
||||
<h1
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
{authStrings.wordmark}
|
||||
</h1>
|
||||
<AuthHeader />
|
||||
|
||||
<div style={{ flex: 1, minHeight: 40 }} />
|
||||
|
||||
|
|
|
|||
|
|
@ -137,8 +137,8 @@ export function AuthForm({
|
|||
|
||||
if (step === "twoFactor") {
|
||||
return (
|
||||
<form onSubmit={handleCodeSubmit} className="flex flex-col gap-3">
|
||||
<p className="text-center text-sm" style={{ color: "var(--mercury-subtle, #6b7280)" }}>
|
||||
<form onSubmit={handleCodeSubmit} className="flex flex-col gap-5">
|
||||
<p className="text-center text-sm" style={{ color: "var(--mercury-subtle, #6b7280)", margin: 0 }}>
|
||||
{authStrings.twoFactor.subtitle}
|
||||
</p>
|
||||
|
||||
|
|
@ -162,13 +162,14 @@ export function AuthForm({
|
|||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="flex flex-col gap-3">
|
||||
<form onSubmit={handleFormSubmit} className="flex flex-col gap-5">
|
||||
{mode === "forgot" && (
|
||||
<p className="text-center text-sm" style={{ color: "var(--mercury-subtle, #6b7280)" }}>
|
||||
<p className="text-center text-sm" style={{ color: "var(--mercury-subtle, #6b7280)", margin: 0 }}>
|
||||
{authStrings.forgot.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<TextFieldRoot value={email} onValueChange={setEmail} name="email">
|
||||
<TextFieldInput
|
||||
type="email"
|
||||
|
|
@ -209,19 +210,21 @@ export function AuthForm({
|
|||
type="button"
|
||||
onClick={onNavigateForgot}
|
||||
className="self-end text-sm"
|
||||
style={{ color: "var(--mercury-subtle, #6b7280)", background: "none", border: "none" }}
|
||||
style={{ color: "var(--mercury-subtle, #6b7280)", background: "none", border: "none", padding: 0 }}
|
||||
>
|
||||
{authStrings.login.forgot}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormError message={errorMessage} />
|
||||
{infoMessage && (
|
||||
<p className="text-sm" style={{ color: "var(--mercury-subtle, #6b7280)" }}>
|
||||
<p className="text-sm" style={{ color: "var(--mercury-subtle, #6b7280)", margin: 0 }}>
|
||||
{infoMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3" style={{ marginTop: 4 }}>
|
||||
<MercuryButton type="submit" variant="primary" loading={isSubmitting}>
|
||||
{mode === "login"
|
||||
? authStrings.login.submit
|
||||
|
|
@ -240,6 +243,7 @@ export function AuthForm({
|
|||
{authStrings.register.loginLink}
|
||||
</MercuryButton>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -247,7 +251,7 @@ export function AuthForm({
|
|||
function FormError({ message }: { message?: string }) {
|
||||
if (!message) return null;
|
||||
return (
|
||||
<p role="alert" className="text-sm" style={{ color: "var(--seed-color-fg-critical)" }}>
|
||||
<p role="alert" className="text-sm" style={{ color: "var(--seed-color-fg-critical)", margin: 0 }}>
|
||||
{message}
|
||||
</p>
|
||||
);
|
||||
|
|
|
|||
71
src/features/auth/AuthHeader.tsx
Normal file
71
src/features/auth/AuthHeader.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Icon } from "@/ds";
|
||||
import { authStrings } from "./strings";
|
||||
|
||||
/**
|
||||
* Shared header for the three auth screens (login / register / forgot): a
|
||||
* back chevron pinned top-left, then a MERCURY wordmark lockup — a tinted
|
||||
* monogram badge stacked above the bold, letter-spaced wordmark — centered
|
||||
* below it. Purely presentational; navigation still goes through
|
||||
* `router.back()`, unchanged from the markup this replaces in each page.
|
||||
*/
|
||||
export function AuthHeader() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
aria-label="Буцах"
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
marginTop: 24,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 12,
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "var(--seed-color-fg-neutral)",
|
||||
}}
|
||||
>
|
||||
<Icon name="chevron-left" size={22} />
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: 48, display: "flex", flexDirection: "column", alignItems: "center", gap: 14 }}>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 16,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontSize: 22,
|
||||
fontWeight: 800,
|
||||
color: "var(--mercury-on-brand)",
|
||||
background: "var(--mercury-brand-yellow)",
|
||||
}}
|
||||
>
|
||||
M
|
||||
</span>
|
||||
<h1
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontWeight: 800,
|
||||
letterSpacing: 3,
|
||||
fontSize: 24,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{authStrings.wordmark}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ import {
|
|||
Skeleton,
|
||||
} from "@seed-design/react";
|
||||
import { MercuryButton } from "@/ds/MercuryButton";
|
||||
import { EmptyState, Icon, IconChip } from "@/ds";
|
||||
import { categoryStyle } from "@/ds/categoryStyle";
|
||||
import { useCategories } from "@/api/hooks/reads";
|
||||
import { useCategoryMutations } from "@/api/hooks/mutations";
|
||||
import type { Category } from "@/api/schemas";
|
||||
|
|
@ -105,9 +107,9 @@ export function CategoriesManager({ onBack }: CategoriesManagerProps) {
|
|||
type="button"
|
||||
onClick={onBack}
|
||||
aria-label={profileStrings.categories.back}
|
||||
style={{ background: "none", border: "none", fontSize: 20, cursor: "pointer" }}
|
||||
style={{ display: "grid", placeItems: "center", background: "none", border: "none", cursor: "pointer", color: "var(--seed-color-fg-neutral)" }}
|
||||
>
|
||||
←
|
||||
<Icon name="chevron-left" size={22} />
|
||||
</button>
|
||||
)}
|
||||
<h1 style={{ fontSize: 16, fontWeight: 600, margin: 0, flex: 1 }}>{profileStrings.categories.title}</h1>
|
||||
|
|
@ -124,7 +126,7 @@ export function CategoriesManager({ onBack }: CategoriesManagerProps) {
|
|||
)}
|
||||
|
||||
{!isLoading && groups.length === 0 && (
|
||||
<p style={{ fontSize: 14, color: "var(--seed-color-fg-muted, #6b7280)" }}>{profileStrings.categories.empty}</p>
|
||||
<EmptyState icon="list" title={profileStrings.categories.empty} />
|
||||
)}
|
||||
|
||||
{groups.map((group) => (
|
||||
|
|
@ -132,6 +134,7 @@ export function CategoriesManager({ onBack }: CategoriesManagerProps) {
|
|||
<CategoryRow
|
||||
icon={group.icon}
|
||||
name={group.name}
|
||||
kind="expense"
|
||||
heading
|
||||
onEdit={() => setSheet({ mode: "edit", category: { name: group.name, kind: "expense", depth: 0, icon: group.icon } })}
|
||||
onDelete={() => setDeleteTarget(group.name)}
|
||||
|
|
@ -204,20 +207,23 @@ export function CategoriesManager({ onBack }: CategoriesManagerProps) {
|
|||
function CategoryRow({
|
||||
icon,
|
||||
name,
|
||||
kind,
|
||||
heading,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
icon?: string | null;
|
||||
name: string;
|
||||
kind?: string;
|
||||
heading?: boolean;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const style = categoryStyle(name, kind === "income");
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span aria-hidden style={{ fontSize: heading ? 20 : 16 }}>{icon || "🏷️"}</span>
|
||||
<span style={{ fontSize: heading ? 16 : 14, fontWeight: heading ? 600 : 400, flex: 1 }}>{name}</span>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<IconChip icon={style.icon} tint={style.tint} fg={style.fg} size={heading ? 40 : 32} />
|
||||
<span style={{ fontSize: heading ? 16 : 14, fontWeight: heading ? 700 : 500, flex: 1 }}>{style.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
|
|
@ -247,24 +253,26 @@ function CategoryChip({
|
|||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const style = categoryStyle(category.name, category.kind === "income");
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
padding: "6px 10px",
|
||||
padding: "6px 10px 6px 6px",
|
||||
borderRadius: 999,
|
||||
background: "var(--seed-color-bg-layer-default)",
|
||||
background: style.tint,
|
||||
color: style.fg,
|
||||
}}
|
||||
>
|
||||
<span aria-hidden>{category.icon || "🏷️"}</span>
|
||||
<span style={{ fontSize: 13 }}>{category.name}</span>
|
||||
<IconChip icon={style.icon} tint="transparent" fg={style.fg} size={22} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600 }}>{style.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
aria-label={`${profileStrings.categories.editAction}: ${category.name}`}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 12, color: "var(--seed-color-fg-muted, #6b7280)" }}
|
||||
style={{ background: "none", border: "none", cursor: "pointer", fontSize: 12, color: "inherit", opacity: 0.7 }}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
|
|
@ -315,9 +323,9 @@ function CategoryEditor({
|
|||
type="button"
|
||||
onClick={onCancel}
|
||||
aria-label={profileStrings.categories.cancel}
|
||||
style={{ background: "none", border: "none", fontSize: 20, cursor: "pointer" }}
|
||||
style={{ display: "grid", placeItems: "center", background: "none", border: "none", cursor: "pointer", color: "var(--seed-color-fg-neutral)" }}
|
||||
>
|
||||
←
|
||||
<Icon name="chevron-left" size={22} />
|
||||
</button>
|
||||
<h1 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>
|
||||
{editing ? profileStrings.categories.edit : profileStrings.categories.add}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useConnections } from "@/api/hooks/reads";
|
||||
import { Skeleton } from "@seed-design/react";
|
||||
import { EmptyState, IconChip, SectionHeader } from "@/ds";
|
||||
import { profileStrings } from "./strings";
|
||||
|
||||
/**
|
||||
|
|
@ -11,31 +12,28 @@ import { profileStrings } from "./strings";
|
|||
*/
|
||||
export function ConnectedBanks() {
|
||||
const { data: connections, isLoading } = useConnections();
|
||||
const empty = !isLoading && (!connections || connections.length === 0);
|
||||
|
||||
return (
|
||||
<section aria-labelledby="connected-banks-heading">
|
||||
<h2
|
||||
id="connected-banks-heading"
|
||||
style={{ fontSize: 12, color: "var(--seed-color-fg-subtle, #6b7280)", margin: "0 0 12px" }}
|
||||
>
|
||||
{profileStrings.banks.title}
|
||||
</h2>
|
||||
<div id="connected-banks-heading">
|
||||
<SectionHeader title={profileStrings.banks.title} />
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 12 }}>
|
||||
<Skeleton height="44px" />
|
||||
<Skeleton height="44px" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && (!connections || connections.length === 0) && (
|
||||
<p style={{ fontSize: 14, color: "var(--seed-color-fg-muted, #6b7280)" }}>
|
||||
{profileStrings.banks.empty}
|
||||
</p>
|
||||
{empty && (
|
||||
<EmptyState icon="bank" title={profileStrings.banks.empty} hint={profileStrings.banks.manageNote} compact />
|
||||
)}
|
||||
|
||||
{!isLoading && connections && connections.length > 0 && (
|
||||
<ul style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<>
|
||||
<ul style={{ listStyle: "none", margin: "8px 0 0", padding: 0, display: "flex", flexDirection: "column" }}>
|
||||
{connections.map((c) => (
|
||||
<li
|
||||
key={c.bank}
|
||||
|
|
@ -43,14 +41,26 @@ export function ConnectedBanks() {
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
padding: "10px 0",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>{c.bank}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--seed-color-fg-muted, #6b7280)" }}>
|
||||
{c.username}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
||||
<IconChip icon="bank" tint="var(--seed-color-bg-neutral-subtle, #eef0f2)" fg="var(--seed-color-fg-neutral)" />
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 0 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{c.bank}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: "var(--seed-color-fg-muted, #6b7280)" }}>{c.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
{c.courierManaged && (
|
||||
<span
|
||||
|
|
@ -58,6 +68,7 @@ export function ConnectedBanks() {
|
|||
fontSize: 11,
|
||||
padding: "4px 8px",
|
||||
borderRadius: 999,
|
||||
flexShrink: 0,
|
||||
background: "var(--seed-color-bg-layer-default)",
|
||||
color: "var(--seed-color-fg-muted, #6b7280)",
|
||||
}}
|
||||
|
|
@ -68,11 +79,12 @@ export function ConnectedBanks() {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: 12, color: "var(--seed-color-fg-muted, #6b7280)", marginTop: 8 }}>
|
||||
{profileStrings.banks.manageNote}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, HideAmountsToggle, MercuryButton } from "@/ds";
|
||||
import { Card, HideAmountsToggle, Icon, IconChip, MercuryButton } from "@/ds";
|
||||
import type { IconName } from "@/ds";
|
||||
import { useMe, useSettings } from "@/api/hooks/reads";
|
||||
import { profileStrings } from "./strings";
|
||||
import { SettingsForm } from "./SettingsForm";
|
||||
|
|
@ -24,6 +25,7 @@ export function ProfileView() {
|
|||
|
||||
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);
|
||||
|
|
@ -45,23 +47,57 @@ export function ProfileView() {
|
|||
<HideAmountsToggle label={profileStrings.display.hideAmounts} />
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>{displayName}</span>
|
||||
<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: 12, color: "var(--seed-color-fg-muted, #6b7280)" }}>
|
||||
{profileStrings.account.handlePrefix}
|
||||
{emailLocal}
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--seed-color-fg-muted, #6b7280)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{me.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<nav style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<MenuRow label={profileStrings.menu.settings} onClick={() => setShowSettings(true)} />
|
||||
<MenuLink label={profileStrings.menu.categories} href="/profile/categories" />
|
||||
<MenuLink label={profileStrings.menu.subscriptions} href="/profile/subscriptions" />
|
||||
<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>
|
||||
|
||||
|
|
@ -76,49 +112,46 @@ export function ProfileView() {
|
|||
);
|
||||
}
|
||||
|
||||
function MenuRow({ label, onClick }: { label: string; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
const menuRowStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 0",
|
||||
gap: 12,
|
||||
width: "100%",
|
||||
padding: "10px 0",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
font: "inherit",
|
||||
color: "inherit",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14 }}>{label}</span>
|
||||
<span aria-hidden style={{ color: "var(--seed-color-fg-placeholder, #9ca3af)" }}>
|
||||
›
|
||||
</span>
|
||||
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({ label, href }: { label: string; href: string }) {
|
||||
function MenuLink({ icon, label, href }: { icon: IconName; label: string; href: string }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 0",
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 14 }}>{label}</span>
|
||||
<span aria-hidden style={{ color: "var(--seed-color-fg-placeholder, #9ca3af)" }}>
|
||||
›
|
||||
</span>
|
||||
<Link href={href} style={menuRowStyle}>
|
||||
<MenuRowContent icon={icon} label={label} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Skeleton,
|
||||
} from "@seed-design/react";
|
||||
import { MercuryButton } from "@/ds/MercuryButton";
|
||||
import { Icon } from "@/ds";
|
||||
import { useSettings } from "@/api/hooks/reads";
|
||||
import { useSaveSettings } from "@/api/hooks/mutations";
|
||||
import type { Settings } from "@/api/schemas";
|
||||
|
|
@ -126,9 +127,9 @@ export function SettingsForm({ onBack }: SettingsFormProps) {
|
|||
type="button"
|
||||
onClick={onBack}
|
||||
aria-label={profileStrings.settings.back}
|
||||
style={{ background: "none", border: "none", fontSize: 20, cursor: "pointer" }}
|
||||
style={{ display: "grid", placeItems: "center", background: "none", border: "none", cursor: "pointer", color: "var(--seed-color-fg-neutral)" }}
|
||||
>
|
||||
←
|
||||
<Icon name="chevron-left" size={22} />
|
||||
</button>
|
||||
)}
|
||||
<h1 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>{profileStrings.settings.title}</h1>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import * as React from "react";
|
||||
import { TextFieldRoot, TextFieldInput, Skeleton } from "@seed-design/react";
|
||||
import { MercuryButton } from "@/ds/MercuryButton";
|
||||
import { EmptyState, Icon, IconChip } from "@/ds";
|
||||
import { tugrik } from "@/ds/money";
|
||||
import { useSubscriptions } from "@/api/hooks/reads";
|
||||
import { useSubscriptionMutations } from "@/api/hooks/mutations";
|
||||
|
|
@ -58,9 +59,9 @@ export function SubscriptionsView({ onBack }: SubscriptionsViewProps) {
|
|||
type="button"
|
||||
onClick={onBack}
|
||||
aria-label={profileStrings.subscriptions.back}
|
||||
style={{ background: "none", border: "none", fontSize: 20, cursor: "pointer" }}
|
||||
style={{ display: "grid", placeItems: "center", background: "none", border: "none", cursor: "pointer", color: "var(--seed-color-fg-neutral)" }}
|
||||
>
|
||||
←
|
||||
<Icon name="chevron-left" size={22} />
|
||||
</button>
|
||||
)}
|
||||
<h1 style={{ fontSize: 16, fontWeight: 600, margin: 0, flex: 1 }}>{profileStrings.subscriptions.title}</h1>
|
||||
|
|
@ -77,7 +78,7 @@ export function SubscriptionsView({ onBack }: SubscriptionsViewProps) {
|
|||
)}
|
||||
|
||||
{!isLoading && all.length === 0 && (
|
||||
<p style={{ fontSize: 14, color: "var(--seed-color-fg-muted, #6b7280)" }}>{profileStrings.subscriptions.empty}</p>
|
||||
<EmptyState icon="sparkles" title={profileStrings.subscriptions.empty} />
|
||||
)}
|
||||
|
||||
{!isLoading && detected.length > 0 && (
|
||||
|
|
@ -136,14 +137,27 @@ function SubscriptionRow({
|
|||
destructive?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 0" }}>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>{sub.label}</span>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "8px 0" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12, minWidth: 0 }}>
|
||||
<IconChip icon="sparkles" tint="var(--seed-color-bg-neutral-subtle, #eef0f2)" fg="var(--seed-color-fg-neutral)" size={36} />
|
||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{sub.label}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: "var(--seed-color-fg-muted, #6b7280)" }}>
|
||||
{tugrik(sub.monthly)} / {sub.cadence}
|
||||
{sub.nextDue ? ` · ${sub.nextDue}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAction}
|
||||
|
|
@ -204,9 +218,9 @@ function ManualSubscriptionForm({
|
|||
type="button"
|
||||
onClick={onCancel}
|
||||
aria-label={profileStrings.subscriptions.cancel}
|
||||
style={{ background: "none", border: "none", fontSize: 20, cursor: "pointer" }}
|
||||
style={{ display: "grid", placeItems: "center", background: "none", border: "none", cursor: "pointer", color: "var(--seed-color-fg-neutral)" }}
|
||||
>
|
||||
←
|
||||
<Icon name="chevron-left" size={22} />
|
||||
</button>
|
||||
<h1 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>{profileStrings.subscriptions.addManual}</h1>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue