- Web header is just the logo and Feed / Profile / Settings (current page highlighted). On phones those move to a bottom tab bar. Log out, Admin and Support moved into Settings. - The bottom of Settings (web and app) is now one Account list: email, password, blocked accounts, admin, support, privacy, terms, log out and delete, each a row that opens in place (web) or a screen (app). - Featured song shows what's featured now, with Change and Remove. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -1,49 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { api, client, useMe } from '@/lib/api';
|
||||
import { SupportLink } from './SupportLink';
|
||||
import { client, useMe } from '@/lib/api';
|
||||
|
||||
/** Top bar: logo and the three main places. On phones those move to a bottom tab bar. */
|
||||
export function Nav() {
|
||||
const { me, mutate } = useMe();
|
||||
const router = useRouter();
|
||||
const logout = async () => {
|
||||
await api('/auth/logout', { method: 'POST', body: {} });
|
||||
await mutate(undefined, { revalidate: true });
|
||||
router.push('/');
|
||||
};
|
||||
const { me } = useMe();
|
||||
const path = usePathname();
|
||||
const tabs = me
|
||||
? [
|
||||
{ href: '/', label: 'Feed', icon: <FeedIcon />, active: path === '/' },
|
||||
{ href: `/u/${me.username}`, label: 'Profile', icon: <ProfileIcon />, active: path.startsWith(`/u/${me.username}`) },
|
||||
{ href: '/settings', label: 'Settings', icon: <SettingsIcon />, active: path.startsWith('/settings') || path === '/admin' },
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="nav">
|
||||
<Link href="/" className="brand">
|
||||
<img className="brand-logo" src="/logo.png" alt="" width={30} height={30} />
|
||||
musicshare
|
||||
</Link>
|
||||
<div className="nav-links">
|
||||
<SupportLink />
|
||||
{me ? (
|
||||
<>
|
||||
<Link href="/">Feed</Link>
|
||||
<Link href={`/u/${me.username}`}>Profile</Link>
|
||||
<Link href="/settings">Settings</Link>
|
||||
{me.role === 'admin' && <Link href="/admin">Admin</Link>}
|
||||
<button className="link-button" onClick={logout}>
|
||||
Log out
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login">Log in</Link>
|
||||
<Link href="/register" className="btn btn-primary">
|
||||
Sign up
|
||||
<nav className="nav">
|
||||
<Link href="/" className="brand">
|
||||
<img className="brand-logo" src="/logo.png" alt="" width={30} height={30} />
|
||||
musicshare
|
||||
</Link>
|
||||
<div className="nav-links">
|
||||
{me ? (
|
||||
tabs.map((t) => (
|
||||
<Link key={t.href} href={t.href} className={t.active ? 'active' : ''} aria-current={t.active ? 'page' : undefined}>
|
||||
{t.label}
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login">Log in</Link>
|
||||
<Link href="/register" className="btn btn-primary">
|
||||
Sign up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
{me && (
|
||||
<nav className="tabbar" aria-label="Main">
|
||||
{tabs.map((t) => (
|
||||
<Link key={t.href} href={t.href} className={t.active ? 'active' : ''} aria-current={t.active ? 'page' : undefined}>
|
||||
{t.icon}
|
||||
<span>{t.label}</span>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
{me && !me.emailVerified && <VerifyBanner email={me.pendingEmail ?? me.email} />}
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
{me && !me.emailVerified && <VerifyBanner email={me.pendingEmail ?? me.email} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +70,34 @@ function VerifyBanner({ email }: { email: string }) {
|
||||
return (
|
||||
<div className="banner">
|
||||
Confirm your email: we sent a link to <strong>{email}</strong>.{' '}
|
||||
{sent ? <span className="muted">Sent again.</span> : <button className="link-button" onClick={resend}>Send it again</button>}
|
||||
{sent ? (
|
||||
<span className="muted">Sent again.</span>
|
||||
) : (
|
||||
<button className="link-button" onClick={resend}>
|
||||
Send it again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const icon = { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' } as const;
|
||||
const FeedIcon = () => (
|
||||
<svg {...icon} aria-hidden>
|
||||
<path d="M9 18V5l12-2v13" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
);
|
||||
const ProfileIcon = () => (
|
||||
<svg {...icon} aria-hidden>
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 21c0-4 3.6-7 8-7s8 3 8 7" />
|
||||
</svg>
|
||||
);
|
||||
const SettingsIcon = () => (
|
||||
<svg {...icon} aria-hidden>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user