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,
|
DEFAULT_TAB_IDS,
|
||||||
type NavTab,
|
type NavTab,
|
||||||
} from '@/hooks/useNavPreferences';
|
} from '@/hooks/useNavPreferences';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
||||||
import { useTranslation } from '@/contexts/language';
|
import { useTranslation } from '@/contexts/language';
|
||||||
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { cn } from '@/lib/utils';
|
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 {
|
interface CustomizeNavSheetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -20,14 +20,14 @@ interface CustomizeNavSheetProps {
|
||||||
/**
|
/**
|
||||||
* Bottom-sheet editor for the mobile nav bar.
|
* 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.
|
* 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.
|
* Draft state is local until "Done", so changes feel deliberate and cancelable.
|
||||||
*/
|
*/
|
||||||
export function CustomizeNavSheet({ open, onOpenChange }: CustomizeNavSheetProps) {
|
export function CustomizeNavSheet({ open, onOpenChange }: CustomizeNavSheetProps) {
|
||||||
const { middleIds, setMiddleIds, reset, catalog } = useNavPreferences();
|
const { middleIds, setMiddleIds, reset, catalog } = useNavPreferences();
|
||||||
const { user } = useCurrentUser();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { user } = useCurrentUser();
|
||||||
|
|
||||||
// Local draft so we can cancel without persisting.
|
// Local draft so we can cancel without persisting.
|
||||||
const [draft, setDraft] = useState<string[]>(middleIds);
|
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" />;
|
return <PreviewSlot key={tab.id} icon={tab.icon} label={label(tab)} accent="#ff0040" />;
|
||||||
})}
|
})}
|
||||||
{/* Fixed Profile */}
|
{/* Fixed More */}
|
||||||
<PreviewSlot icon={User} label={isLoggedIn ? t('navSheet.profile') : t('navSheet.login')} locked accent="#ff0040" />
|
<PreviewSlot icon={MoreHorizontal} label={t('nav.more')} locked accent="#ff0040" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -367,21 +367,17 @@ function MobileHeaderUserMenu({
|
||||||
function MobileNav({
|
function MobileNav({
|
||||||
isActive,
|
isActive,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
userPubkey,
|
|
||||||
userAvatar,
|
|
||||||
userName,
|
|
||||||
t,
|
t,
|
||||||
onCustomize,
|
onCustomize,
|
||||||
|
onMore,
|
||||||
fixedMapTab,
|
fixedMapTab,
|
||||||
middleTabs,
|
middleTabs,
|
||||||
}: {
|
}: {
|
||||||
isActive: (path: string) => boolean;
|
isActive: (path: string) => boolean;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
userPubkey?: string;
|
|
||||||
userAvatar?: string;
|
|
||||||
userName?: string;
|
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
onCustomize: () => void;
|
onCustomize: () => void;
|
||||||
|
onMore: () => void;
|
||||||
fixedMapTab: NavTab;
|
fixedMapTab: NavTab;
|
||||||
middleTabs: NavTab[];
|
middleTabs: NavTab[];
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -483,60 +479,21 @@ function MobileNav({
|
||||||
{/* CUSTOM: 3 middle tabs */}
|
{/* CUSTOM: 3 middle tabs */}
|
||||||
{resolvedMiddle.map((tab, i) => renderTab(tab, `mid-${tab.id}-${i}`))}
|
{resolvedMiddle.map((tab, i) => renderTab(tab, `mid-${tab.id}-${i}`))}
|
||||||
|
|
||||||
{/* FIXED: Profile / Login tab.
|
{/* FIXED: More — the burger menu owns profile, login, wallet,
|
||||||
Logged in → your public profile (View Profile).
|
settings and every secondary section. */}
|
||||||
Logged out → Settings (login lives there).
|
<button
|
||||||
Long-press / right-click still opens nav customization. */}
|
type="button"
|
||||||
{(() => {
|
onClick={onMore}
|
||||||
const profileHref = isLoggedIn && userPubkey ? `/${nip19.npubEncode(userPubkey)}` : '/settings';
|
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"
|
||||||
const tabActive = isActive(profileHref);
|
aria-label={t('nav.more')}
|
||||||
return (
|
>
|
||||||
<Link
|
<span className="relative flex h-9 w-12 items-center justify-center rounded-2xl bg-transparent transition-all duration-300">
|
||||||
to={profileHref}
|
<MoreHorizontal className="h-[22px] w-[22px] transition-transform duration-300 group-active:scale-95" />
|
||||||
onTouchStart={startPress}
|
</span>
|
||||||
onTouchEnd={cancelPress}
|
<span className="block w-full truncate text-center text-[9px] xs:text-[10px] font-semibold uppercase leading-tight tracking-wide">
|
||||||
onTouchMove={cancelPress}
|
{t('nav.more')}
|
||||||
onContextMenu={(e) => {
|
</span>
|
||||||
e.preventDefault();
|
</button>
|
||||||
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'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -618,7 +575,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
|
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
const displayName = author.data?.metadata?.name ?? genUserName(user?.pubkey);
|
const displayName = author.data?.metadata?.name ?? genUserName(user?.pubkey);
|
||||||
const userAvatar = author.data?.metadata?.picture;
|
|
||||||
const { fixedMapTab, middleTabs, catalog: navCatalog } = useNavPreferences();
|
const { fixedMapTab, middleTabs, catalog: navCatalog } = useNavPreferences();
|
||||||
const {
|
const {
|
||||||
primaryTabs: desktopPrimaryTabs,
|
primaryTabs: desktopPrimaryTabs,
|
||||||
|
|
@ -1283,10 +1239,8 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
{/* Mobile Bottom Navigation */}
|
{/* Mobile Bottom Navigation */}
|
||||||
<MobileNav
|
<MobileNav
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
onMore={() => setMobileMenuOpen(true)}
|
||||||
isLoggedIn={!!user}
|
isLoggedIn={!!user}
|
||||||
userPubkey={user?.pubkey}
|
|
||||||
userAvatar={userAvatar}
|
|
||||||
userName={displayName}
|
|
||||||
t={t}
|
t={t}
|
||||||
onCustomize={() => setShowCustomizeNav(true)}
|
onCustomize={() => setShowCustomizeNav(true)}
|
||||||
fixedMapTab={fixedMapTab}
|
fixedMapTab={fixedMapTab}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ import { usePreferences } from '@/hooks/usePreferences';
|
||||||
* Bottom-nav customization (mobile).
|
* Bottom-nav customization (mobile).
|
||||||
*
|
*
|
||||||
* Model: the bar always shows a FIXED Map (Shops & Spots) on the left and a
|
* 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
|
* FIXED More button on the right. The user customizes the **3 middle slots**
|
||||||
* slots** from a curated catalog. Anything they don't pin stays reachable in the
|
* 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.
|
* burger menu, so nothing is ever lost — just re-prioritized.
|
||||||
*
|
*
|
||||||
* Stored per-device in localStorage (v1). Could later sync across devices via a
|
* 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.
|
* 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[] = [
|
export const NAV_CATALOG: NavTab[] = [
|
||||||
{ id: 'lab', href: '/lab', labelKey: 'nav.lab', label: 'The Lab', icon: GraduationCap },
|
{ 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. */
|
/** 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
|
/** Default middle tabs. Keep the mobile bar to five intents total:
|
||||||
* from the phone nav out of the box; users can swap any of them via long-press. */
|
* fixed Map + these three + a fixed More button. Everything else lives in the
|
||||||
export const DEFAULT_TAB_IDS = ['lab', 'events', 'crews', 'mints'];
|
* 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';
|
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.
|
// ever lost. Map is always the first primary tab and cannot be hidden/moved.
|
||||||
|
|
||||||
/** Default desktop primary tabs. */
|
/** 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';
|
const DESKTOP_STORAGE_KEY = 'skatespots:desktop-nav:v1';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue