ui(nav): simplify mobile bar to map, three tabs and more
This commit is contained in:
parent
344b41afc8
commit
23929653d3
3 changed files with 33 additions and 78 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue