Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/[locale]/[username]/communities/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import WalletPage from '../page';

export default function CommunitiesPage() {
return <WalletPage />;
}
72 changes: 46 additions & 26 deletions src/app/[locale]/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { usePathname, useRouter } from '@/i18n/routing';
import { RecentActivityLazy } from '@/components/wallet/client-wrappers';
import { BalanceRows } from '@/components/wallet/balance-rows';
import { ClaimRewardsBanner } from '@/components/wallet/claim-rewards-banner';
import { WalletSubMenu } from '@/components/layout/wallet-sub-menu';
import { useSteemWalletBalances } from '@/hooks/use-steem-wallet-balances';
import { UserProfileBanner, TopNav } from '@/components/layout/user-profile-banner';
import { UserProfileBanner } from '@/components/layout/user-profile-banner';
import { AccountWalletNav } from '@/components/layout/account-wallet-nav';
import { Skeleton } from '@/components/ui/skeleton';
import { WalletTransfersModals } from '@/components/wallet/wallet-transfers-modals';
import { WalletSectionPlaceholder } from '@/components/wallet/wallet-section-placeholder';
import { normalizeProfile } from '@/lib/steem/normalize-profile';
import { canManageBalanceForPageUrl } from '@/lib/auth/browser-storage';

type BannerProfileFields = {
displayName?: string;
Expand Down Expand Up @@ -52,6 +54,11 @@ export default function WalletPage() {
// Parse username from params (e.g. "@ety001" -> "ety001")
const urlUsername = rawUsername ? decodeURIComponent(rawUsername).replace(/^@/, '') : '';
const isMyAccount = !!isAuthenticated && !!loggedInUser && loggedInUser === urlUsername;
const showBalanceActions = canManageBalanceForPageUrl({
urlUsername,
loggedInUser,
isAuthenticated,
});

const [walletRefreshNonce, setWalletRefreshNonce] = useState(0);

Expand Down Expand Up @@ -125,6 +132,12 @@ export default function WalletPage() {
);
}

const isTransfersPath = pathname?.includes('/transfers') ?? false;
const isDelegationsPath = pathname?.includes('/delegations') ?? false;
const isPermissionsPath = pathname?.includes('/permissions') ?? false;
const isPasswordPath = pathname?.includes('/password') ?? false;
const isCommunitiesPath = pathname?.includes('/communities') ?? false;

return (
<div>
{/* User Profile Banner - matches legacy UserProfile.jsx */}
Expand All @@ -134,39 +147,46 @@ export default function WalletPage() {
{...(bannerProfile ?? {})}
/>

{/* Top Navigation - Blog | Wallet | Rewards */}
<TopNav
<AccountWalletNav
accountname={urlUsername}
activeSection="transfers"
isMyAccount={isMyAccount}
showOwnerWalletNav={showBalanceActions}
/>

{/* Wallet Content Area — claim banner above sub menu matches wallet-legacy UserWallet.jsx */}
<div className="max-w-6xl mx-auto px-4 py-6">
<ClaimRewardsBanner
balance={balance}
isMyAccount={isMyAccount}
loading={balanceLoading}
/>

<WalletSubMenu accountname={urlUsername} isMyAccount={isMyAccount} />

<div className="mt-4">
<BalanceRows
username={urlUsername}
{/* Wallet content: keep top padding tight under AccountWalletNav */}
<div className="mx-auto max-w-6xl space-y-3 px-4 pt-3 pb-6">
{isTransfersPath && (
<ClaimRewardsBanner
balance={balance}
globalProps={globalProps}
isMyAccount={isMyAccount}
loading={balanceLoading}
/>
</div>

{/* Recent Activity - lazy loaded */}
<RecentActivityLazy username={urlUsername} refreshNonce={walletRefreshNonce} />
)}

{isTransfersPath && (
<>
<BalanceRows
username={urlUsername}
balance={balance}
globalProps={globalProps}
loading={balanceLoading}
showBalanceActions={showBalanceActions}
/>
<RecentActivityLazy username={urlUsername} refreshNonce={walletRefreshNonce} />
</>
)}
{isDelegationsPath && <WalletSectionPlaceholder titleKey="delegations" />}
{isPermissionsPath && <WalletSectionPlaceholder titleKey="keysAndPermissions" />}
{isPasswordPath && <WalletSectionPlaceholder titleKey="changePassword" />}
{isCommunitiesPath && <WalletSectionPlaceholder titleKey="communities" />}
</div>

<Suspense fallback={null}>
<WalletTransfersModals
onWalletDataChanged={() => setWalletRefreshNonce((n) => n + 1)}
/>
{isTransfersPath && (
<WalletTransfersModals
onWalletDataChanged={() => setWalletRefreshNonce((n) => n + 1)}
/>
)}
</Suspense>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions src/app/[locale]/[username]/password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import WalletPage from '../page';

export default function ChangePasswordPage() {
return <WalletPage />;
}
5 changes: 5 additions & 0 deletions src/app/[locale]/[username]/permissions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import WalletPage from '../page';

export default function PermissionsPage() {
return <WalletPage />;
}
8 changes: 2 additions & 6 deletions src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { LoginForm } from '@/components/auth/login-form';
import { HomeRedirectOrLogin } from '@/components/home/home-redirect-or-login';

export default function HomePage() {
return (
<div className="flex min-h-screen items-center justify-center px-4 py-12">
<LoginForm />
</div>
);
return <HomeRedirectOrLogin />;
}
6 changes: 0 additions & 6 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,6 @@

/* UserWallet balance rows: use Tailwind py-4 px-4 on WalletBalanceRowShell only — avoid global .UserWallet__balance padding; unlayered CSS overrides @layer utilities. */

/* WalletSubMenu */
.WalletSubMenu a.active {
font-weight: 700;
color: var(--foreground);
}

/* Wallet dropdown - match legacy style */
.Wallet_dropdown {
cursor: pointer;
Expand Down
128 changes: 78 additions & 50 deletions src/components/auth/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AppDispatch } from '@/lib/store';
import { setCredentials } from '@/lib/store/slices/auth';
import { SteemSigner, apiClient } from '@/lib/steem/client';
import {
normalizeSteemUsername,
REMEMBERED_POSTING_KEY_KEY,
REMEMBERED_USERNAME_KEY,
} from '@/lib/auth/browser-storage';
Expand All @@ -27,33 +28,45 @@ interface LoginFormData {
password: string;
}

export function LoginForm() {
export interface LoginFormProps {
/** Wallet modal re-auth: skip navigation and call onLoginSuccess. */
embedded?: boolean;
/** Lock username to this account (normalized). */
fixedUsername?: string;
onLoginSuccess?: () => void;
}

export function LoginForm(props: LoginFormProps = {}) {
const { embedded = false, fixedUsername, onLoginSuccess } = props;
const t = useTranslations('auth');
const tCommon = useTranslations('common');
const dispatch = useDispatch<AppDispatch>();
const router = useRouter();
const [isPending, startTransition] = useTransition();

const [formData, setFormData] = useState<LoginFormData>({
username: '',
const [formData, setFormData] = useState<LoginFormData>(() => ({
username: fixedUsername ? normalizeSteemUsername(fixedUsername) : '',
password: '',
});
}));
const [error, setError] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [rememberUser, setRememberUser] = useState(false);

// Read remembered username only after mount so SSR and the first client render match.
useEffect(() => {
try {
const saved = localStorage.getItem(REMEMBERED_USERNAME_KEY) ?? '';
if (saved) {
setFormData((prev) => ({ ...prev, username: saved }));
setRememberUser(true);
if (embedded || fixedUsername) return;
const id = requestAnimationFrame(() => {
try {
const saved = localStorage.getItem(REMEMBERED_USERNAME_KEY) ?? '';
if (saved) {
setFormData((prev) => ({ ...prev, username: normalizeSteemUsername(saved) }));
setRememberUser(true);
}
} catch {
// ignore
}
} catch {
// ignore
}
}, []);
});
return () => cancelAnimationFrame(id);
}, [embedded, fixedUsername]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
Expand All @@ -67,7 +80,9 @@ export function LoginForm() {
setIsLoading(true);

try {
const username = formData.username.trim().toLowerCase().replace(/^@+/, '');
const username = fixedUsername
? normalizeSteemUsername(fixedUsername)
: normalizeSteemUsername(formData.username);
const rawSecret = formData.password.trim();

if (!username || !rawSecret) {
Expand Down Expand Up @@ -242,40 +257,52 @@ export function LoginForm() {
// ignore
}

// Navigate to user's wallet (transfers tab) with /@username in the path (not %40)
startTransition(() => {
router.push(transfersPathForUsername(username));
});
setIsLoading(false);
if (embedded) {
onLoginSuccess?.();
} else {
startTransition(() => {
router.push(transfersPathForUsername(username));
});
}
} catch (err) {
console.error('Login error:', err);
setError(tCommon('error'));
setIsLoading(false);
}
};

const usernameInputId = embedded ? 'wallet-reauth-username' : 'username';

return (
<div className="mx-auto w-full max-w-md">
{/* Login Form Card */}
<div className="rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm sm:p-8">
<div className={embedded ? 'w-full' : 'mx-auto w-full max-w-md'}>
<div
className={
embedded
? ''
: 'rounded-lg border border-border bg-card p-6 text-card-foreground shadow-sm sm:p-8'
}
>
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
{/* Username Input with @ prefix */}
<div className="flex flex-col gap-2">
<Label htmlFor="username" className="text-sm font-semibold text-foreground">
<Label htmlFor={usernameInputId} className="text-sm font-semibold text-foreground">
{t('username')}
</Label>
<div className="relative">
<span className="absolute inset-y-0 left-3 flex items-center text-muted-foreground pointer-events-none">
@
</span>
<Input
id="username"
id={usernameInputId}
name="username"
type="text"
value={formData.username}
onChange={handleChange}
required
placeholder={t('usernamePlaceholder')}
disabled={isLoading || isPending}
disabled={isLoading || isPending || !!fixedUsername}
readOnly={!!fixedUsername}
className="pl-8"
/>
</div>
Expand All @@ -301,31 +328,32 @@ export function LoginForm() {
</p>
</div>

{/* Save Login Option */}
<div className="flex items-start gap-3">
<Checkbox
id="keepLoggedIn"
checked={rememberUser}
onCheckedChange={(value) => setRememberUser(value === true)}
disabled={isLoading || isPending}
className="peer mt-0.5 border-muted-foreground/50 data-[state=unchecked]:bg-background"
/>
<Label
htmlFor="keepLoggedIn"
className="cursor-pointer text-sm font-normal leading-snug text-muted-foreground peer-disabled:cursor-not-allowed"
>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-help underline decoration-dotted decoration-muted-foreground/60 underline-offset-2">
{t('rememberUsername')}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-left">
{t('rememberUserTooltip')}
</TooltipContent>
</Tooltip>
</Label>
</div>
{!embedded && (
<div className="flex items-start gap-3">
<Checkbox
id="keepLoggedIn"
checked={rememberUser}
onCheckedChange={(value) => setRememberUser(value === true)}
disabled={isLoading || isPending}
className="peer mt-0.5 border-muted-foreground/50 data-[state=unchecked]:bg-background"
/>
<Label
htmlFor="keepLoggedIn"
className="cursor-pointer text-sm font-normal leading-snug text-muted-foreground peer-disabled:cursor-not-allowed"
>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-help underline decoration-dotted decoration-muted-foreground/60 underline-offset-2">
{t('rememberUsername')}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-left">
{t('rememberUserTooltip')}
</TooltipContent>
</Tooltip>
</Label>
</div>
)}

{/* Error Message */}
{error && (
Expand Down
41 changes: 41 additions & 0 deletions src/components/home/home-redirect-or-login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from '@/i18n/routing';
import { LoginForm } from '@/components/auth/login-form';
import { Skeleton } from '@/components/ui/skeleton';
import { getRememberedDeviceUsername } from '@/lib/auth/browser-storage';
import { transfersPathForUsername } from '@/lib/wallet/wallet-modal-search-params';

/**
* Root path: redirect to /@rememberedUser/transfers when a username is stored locally;
* otherwise show the login form.
*/
export function HomeRedirectOrLogin() {
const router = useRouter();
const [phase, setPhase] = useState<'checking' | 'login'>('checking');

useEffect(() => {
const remembered = getRememberedDeviceUsername();
if (remembered) {
router.replace(transfersPathForUsername(remembered));
return;
}
const id = requestAnimationFrame(() => setPhase('login'));
return () => cancelAnimationFrame(id);
}, [router]);

if (phase === 'checking') {
return (
<div className="flex min-h-screen items-center justify-center px-4 py-12">
<Skeleton className="h-10 w-64" />
</div>
);
}

return (
<div className="flex min-h-screen items-center justify-center px-4 py-12">
<LoginForm />
</div>
);
}
Loading
Loading