Tidy the header and Settings; remove a featured song
CI / check (push) Canceled after 0s

- 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:
2026-10-06 14:15:14 -04:00
co-authored by Claude Opus 5.5
parent 75062d3644
commit 601398acd6
6 changed files with 499 additions and 244 deletions
+73 -38
View File
@@ -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>
);