Files
web_sport/apps/storefront/src/features/account/account-shell.tsx
T
2026-08-13 23:20:23 +07:00

99 lines
3.2 KiB
TypeScript

'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useEffect, type ReactNode } from 'react';
import { Button, Skeleton, cn } from '@sport/ui';
import { Link } from '@/i18n/navigation';
import { routes } from '@/lib/routes';
import { useCustomer } from './customer-provider';
const NAV = [
{ href: routes.account(), key: 'overview' },
{ href: routes.accountOrders(), key: 'orders' },
{ href: routes.accountAddresses(), key: 'addresses' },
{ href: routes.accountWishlist(), key: 'wishlist' },
{ href: routes.accountProfile(), key: 'profile' },
] as const;
/**
* The signed-in shell around every /account page.
*
* Guards on the client rather than in middleware, because the session lives in
* an httpOnly cookie that only the API can validate — middleware would have to
* call the API on every navigation to learn anything the provider already
* knows. The API re-checks every request regardless, so this is presentation:
* it decides what to *show*, never what is *allowed*.
*/
export function AccountShell({ children }: { children: ReactNode }) {
const t = useTranslations('account');
const { customer, status, signOut } = useCustomer();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
if (status !== 'anonymous') return;
// `next` so signing in returns them to the page they asked for, rather
// than dumping everyone on the account overview.
router.replace(`${routes.signIn()}?next=${encodeURIComponent(pathname)}`);
}, [status, router, pathname]);
if (status === 'loading') {
return (
<div className="space-y-4" aria-busy="true">
<Skeleton className="h-8 w-56" />
<Skeleton className="h-64 w-full" />
</div>
);
}
// The redirect above is already in flight; rendering nothing avoids a flash
// of the empty account page on the way out.
if (!customer) return null;
return (
<div className="grid gap-10 lg:grid-cols-[13rem_1fr]">
<aside>
<p className="text-xs font-semibold uppercase tracking-widest">{t('greeting')}</p>
<p className="mt-1 text-lg font-bold">{customer.displayName}</p>
<nav className="mt-6">
<ul className="space-y-0.5">
{NAV.map((item) => {
const active =
item.href === routes.account()
? pathname.endsWith('/account')
: pathname.includes(item.href);
return (
<li key={item.href}>
<Link
href={item.href}
aria-current={active ? 'page' : undefined}
className={cn(
'block px-3 py-2 text-sm transition-colors',
active ? 'bg-ink-950 text-white' : 'text-ink-600 hover:bg-ink-100',
)}
>
{t(`nav.${item.key}`)}
</Link>
</li>
);
})}
</ul>
</nav>
<Button variant="ghost" size="sm" className="mt-6" onClick={() => void signOut()}>
{t('signOut')}
</Button>
</aside>
<div className="min-w-0">{children}</div>
</div>
);
}