Inspired by a dashboard design the user shared. - App: new palette and fonts, borderless rounded panels with soft shadows, a pink-to-blue gradient card for your own song (text and controls switch to white on it), flat cards for friends, a 'Hi Chris, welcome back' header, an outline bell, and a pink pill on the active tab. - Website: the same look, plus a wide-screen layout with a sidebar, the page in a rounded panel, and a right column with your profile, recent activity and recent plays. Phones keep the top bar and bottom tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { usePathname } from 'next/navigation';
|
|
import { useState } from 'react';
|
|
import { client, useMe } from '@/lib/api';
|
|
import { ActivityBell } from './ActivityBell';
|
|
|
|
/** Top bar: logo and the four main places. On phones those move to a bottom tab bar; wide screens use Shell's sidebar. */
|
|
export function Nav() {
|
|
const { me } = useMe();
|
|
const path = usePathname();
|
|
const tabs = me
|
|
? [
|
|
{ href: '/', label: 'Feed', icon: <FeedIcon />, active: path === '/' },
|
|
{ href: '/friends', label: 'Friends', icon: <FriendsIcon />, active: path.startsWith('/friends') },
|
|
{ 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>
|
|
{me && <ActivityBell />}
|
|
<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>
|
|
))}
|
|
</nav>
|
|
)}
|
|
{me && !me.emailVerified && <VerifyBanner email={me.pendingEmail ?? me.email} />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/** Until they confirm, a forgotten password can't be reset; nudge them. */
|
|
function VerifyBanner({ email }: { email: string }) {
|
|
const [sent, setSent] = useState(false);
|
|
const resend = async () => {
|
|
try {
|
|
await client.resendVerification();
|
|
setSent(true);
|
|
} catch (err) {
|
|
alert((err as Error).message);
|
|
}
|
|
};
|
|
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>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const icon = { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' } as const;
|
|
export 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>
|
|
);
|
|
export const FriendsIcon = () => (
|
|
<svg {...icon} aria-hidden>
|
|
<circle cx="9" cy="8" r="3.5" />
|
|
<path d="M2 20c0-3.6 3.1-6 7-6s7 2.4 7 6" />
|
|
<path d="M16 4.5a3.5 3.5 0 0 1 0 7" />
|
|
<path d="M18 14c2.4.6 4 2.6 4 6" />
|
|
</svg>
|
|
);
|
|
export 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>
|
|
);
|
|
export 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>
|
|
);
|