ui(nav): simplify mobile bar to map, three tabs and more

This commit is contained in:
SkateSpots 2026-10-30 20:25:52 +07:00
parent 344b41afc8
commit 23929653d3
3 changed files with 33 additions and 78 deletions

View file

@ -7,10 +7,10 @@ import {
DEFAULT_TAB_IDS,
type NavTab,
} from '@/hooks/useNavPreferences';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTranslation } from '@/contexts/language';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { cn } from '@/lib/utils';
import { MapPin, User, Lock, ArrowUp, ArrowDown, RotateCcw, Check, Sparkles } from 'lucide-react';
import { MapPin, MoreHorizontal, Lock, ArrowUp, ArrowDown, RotateCcw, Check, Sparkles } from 'lucide-react';
interface CustomizeNavSheetProps {
open: boolean;
@ -20,14 +20,14 @@ interface CustomizeNavSheetProps {
/**
* Bottom-sheet editor for the mobile nav bar.
*
* Layout model: Map (fixed left) · 3 user-picked · Profile (fixed right).
* Layout model: Map (fixed left) · 3 user-picked · More (fixed right).
* The user toggles up to 3 destinations from the catalog and can reorder them.
* Draft state is local until "Done", so changes feel deliberate and cancelable.
*/
export function CustomizeNavSheet({ open, onOpenChange }: CustomizeNavSheetProps) {
const { middleIds, setMiddleIds, reset, catalog } = useNavPreferences();
const { user } = useCurrentUser();
const { t } = useTranslation();
const { user } = useCurrentUser();
// Local draft so we can cancel without persisting.
const [draft, setDraft] = useState<string[]>(middleIds);
@ -115,8 +115,8 @@ export function CustomizeNavSheet({ open, onOpenChange }: CustomizeNavSheetProps
}
return <PreviewSlot key={tab.id} icon={tab.icon} label={label(tab)} accent="#ff0040" />;
})}
{/* Fixed Profile */}
<PreviewSlot icon={User} label={isLoggedIn ? t('navSheet.profile') : t('navSheet.login')} locked accent="#ff0040" />
{/* Fixed More */}
<PreviewSlot icon={MoreHorizontal} label={t('nav.more')} locked accent="#ff0040" />
</div>
</div>

View file

@ -367,21 +367,17 @@ function MobileHeaderUserMenu({
function MobileNav({
isActive,
isLoggedIn,
userPubkey,
userAvatar,
userName,
t,
onCustomize,
onMore,
fixedMapTab,
middleTabs,
}: {
isActive: (path: string) => boolean;
isLoggedIn: boolean;
userPubkey?: string;
userAvatar?: string;
userName?: string;
t: (key: string) => string;
onCustomize: () => void;
onMore: () => void;
fixedMapTab: NavTab;
middleTabs: NavTab[];
}) {
@ -483,60 +479,21 @@ function MobileNav({
{/* CUSTOM: 3 middle tabs */}
{resolvedMiddle.map((tab, i) => renderTab(tab, `mid-${tab.id}-${i}`))}
{/* FIXED: Profile / Login tab.
Logged in → your public profile (View Profile).
Logged out → Settings (login lives there).
Long-press / right-click still opens nav customization. */}
{(() => {
const profileHref = isLoggedIn && userPubkey ? `/${nip19.npubEncode(userPubkey)}` : '/settings';
const tabActive = isActive(profileHref);
return (
<Link
to={profileHref}
onTouchStart={startPress}
onTouchEnd={cancelPress}
onTouchMove={cancelPress}
onContextMenu={(e) => {
e.preventDefault();
onCustomize();
}}
className={cn(
'group relative flex flex-col items-center justify-center gap-1 py-1.5 px-1 flex-1 touch-manipulation active:scale-90 transition-transform duration-150',
tabActive ? 'text-[#ff0040]' : 'text-gray-500'
)}
{/* FIXED: More — the burger menu owns profile, login, wallet,
settings and every secondary section. */}
<button
type="button"
onClick={onMore}
className="group relative flex flex-1 touch-manipulation flex-col items-center justify-center gap-1 px-1 py-1.5 active:scale-90 transition-transform duration-150 text-gray-500"
aria-label={t('nav.more')}
>
<span
className={cn(
'relative flex items-center justify-center h-9 w-12 rounded-2xl transition-all duration-300',
tabActive ? 'bg-[#ff0040]/15 shadow-[0_0_16px_rgba(255,0,64,0.35)]' : 'bg-transparent',
)}
>
{isLoggedIn && userPubkey ? (
<div className="relative">
<Avatar className={cn(
'h-[24px] w-[24px] border-2 transition-transform duration-300',
tabActive ? 'border-[#ff0040] scale-110' : 'border-gray-600 group-active:scale-95'
)}>
<AvatarImage src={userAvatar} alt={userName} />
<AvatarFallback className="bg-[#ff0040]/20 text-[#ff0040] text-[8px] font-bold">
{(userName || '??').slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 bg-green-500 border border-black rounded-full" />
</div>
) : (
<User className={cn('h-[22px] w-[22px] transition-transform duration-300', tabActive ? 'scale-110' : 'group-active:scale-95')} />
)}
<span className="relative flex h-9 w-12 items-center justify-center rounded-2xl bg-transparent transition-all duration-300">
<MoreHorizontal className="h-[22px] w-[22px] transition-transform duration-300 group-active:scale-95" />
</span>
<span className={cn(
'block w-full truncate text-center text-[9px] xs:text-[10px] leading-tight tracking-wide uppercase transition-all',
tabActive ? 'font-extrabold' : 'font-semibold',
)}>
{isLoggedIn ? t('chromeUi.profile') : t('chromeUi.logIn')}
<span className="block w-full truncate text-center text-[9px] xs:text-[10px] font-semibold uppercase leading-tight tracking-wide">
{t('nav.more')}
</span>
</Link>
);
})()}
</button>
</div>
</div>
</nav>
@ -618,7 +575,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
const author = useAuthor(user?.pubkey || '');
const displayName = author.data?.metadata?.name ?? genUserName(user?.pubkey);
const userAvatar = author.data?.metadata?.picture;
const { fixedMapTab, middleTabs, catalog: navCatalog } = useNavPreferences();
const {
primaryTabs: desktopPrimaryTabs,
@ -1283,10 +1239,8 @@ export function Layout({ children, noPadding }: LayoutProps) {
{/* Mobile Bottom Navigation */}
<MobileNav
isActive={isActive}
onMore={() => setMobileMenuOpen(true)}
isLoggedIn={!!user}
userPubkey={user?.pubkey}
userAvatar={userAvatar}
userName={displayName}
t={t}
onCustomize={() => setShowCustomizeNav(true)}
fixedMapTab={fixedMapTab}

View file

@ -23,8 +23,8 @@ import { usePreferences } from '@/hooks/usePreferences';
* Bottom-nav customization (mobile).
*
* Model: the bar always shows a FIXED Map (Shops & Spots) on the left and a
* FIXED Profile/Login slot on the right. The user customizes the **3 middle
* slots** from a curated catalog. Anything they don't pin stays reachable in the
* FIXED More button on the right. The user customizes the **3 middle slots**
* from a curated catalog. Profile/login lives in the header avatar menu. Anything they don't pin stays reachable in the
* burger menu, so nothing is ever lost — just re-prioritized.
*
* Stored per-device in localStorage (v1). Could later sync across devices via a
@ -55,7 +55,7 @@ export const FIXED_MAP_TAB: NavTab = {
/**
* Everything the user can pin to the 3 middle slots.
* (Map is excluded — it's the fixed anchor. Profile is the fixed right slot.)
* (Map is excluded — it's the fixed anchor. More is the fixed right slot.)
*/
export const NAV_CATALOG: NavTab[] = [
{ id: 'lab', href: '/lab', labelKey: 'nav.lab', label: 'The Lab', icon: GraduationCap },
@ -75,11 +75,12 @@ export const NAV_CATALOG: NavTab[] = [
];
/** Number of user-customizable middle slots. */
export const MIDDLE_SLOTS = 4;
export const MIDDLE_SLOTS = 3;
/** Default middle tabs. Includes Mints (Cashu mint directory) so it's reachable
* from the phone nav out of the box; users can swap any of them via long-press. */
export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews', 'mints'];
/** Default middle tabs. Keep the mobile bar to five intents total:
* fixed Map + these three + a fixed More button. Everything else lives in the
* burger menu, and users can still swap any middle tab via long-press. */
export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews'];
const STORAGE_KEY = 'skatespots:nav-tabs:v1';
@ -90,7 +91,7 @@ const STORAGE_KEY = 'skatespots:nav-tabs:v1';
// ever lost. Map is always the first primary tab and cannot be hidden/moved.
/** Default desktop primary tabs. */
export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews', 'mints'];
export const DEFAULT_DESKTOP_IDS = ['lab', 'events', 'crews', 'community'];
const DESKTOP_STORAGE_KEY = 'skatespots:desktop-nav:v1';