344 lines
15 KiB
TypeScript
344 lines
15 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
Flag,
|
|
Table,
|
|
BarChart3,
|
|
Settings,
|
|
Plus,
|
|
RotateCcw,
|
|
Trophy,
|
|
Users,
|
|
User as UserIcon,
|
|
LogOut,
|
|
Shield,
|
|
Eye,
|
|
EyeOff,
|
|
ChevronDown,
|
|
Lock,
|
|
} from 'lucide-react';
|
|
import { Round } from '../types/golf';
|
|
import { formatToPar } from '../utils/golfStats';
|
|
import { useAuth } from '../context/AuthContext';
|
|
|
|
export type ActiveTab = 'hole' | 'scorecard' | 'analytics' | 'settings';
|
|
|
|
interface NavbarProps {
|
|
activeTab: ActiveTab;
|
|
setActiveTab: (tab: ActiveTab) => void;
|
|
currentRound?: Round | null;
|
|
onNewRound: () => void;
|
|
onOpenRounds: () => void;
|
|
onOpenAchievements: () => void;
|
|
onOpenCommunity: () => void;
|
|
onOpenAuth: () => void;
|
|
achievementsUnlockedCount: number;
|
|
scoreSummary: {
|
|
totalGross: number;
|
|
toPar: number;
|
|
holesPlayedCount: number;
|
|
};
|
|
}
|
|
|
|
export const Navbar: React.FC<NavbarProps> = ({
|
|
activeTab,
|
|
setActiveTab,
|
|
currentRound,
|
|
onNewRound,
|
|
onOpenRounds,
|
|
onOpenAchievements,
|
|
onOpenCommunity,
|
|
onOpenAuth,
|
|
achievementsUnlockedCount,
|
|
scoreSummary,
|
|
}) => {
|
|
const { user, logout, updateProfile } = useAuth();
|
|
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
{/* Top Header - Desktop & Tablet */}
|
|
<header className="sticky top-0 z-30 bg-stone-900/95 backdrop-blur-md border-b border-stone-800">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-4 sm:gap-6">
|
|
{/* Zone 1: Brand Wordmark */}
|
|
<div className="flex items-center gap-3 shrink-0">
|
|
<button
|
|
onClick={() => setActiveTab('hole')}
|
|
className="text-left flex items-center gap-2.5 focus:outline-none group"
|
|
>
|
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center text-white shadow-md shadow-emerald-950/40">
|
|
<Flag className="w-4 h-4 text-emerald-100" />
|
|
</div>
|
|
<div>
|
|
<span className="text-lg font-extrabold tracking-tight text-white group-hover:text-emerald-400 transition-colors">
|
|
PinPoint Golf
|
|
</span>
|
|
<span className="hidden lg:inline-flex items-center ml-2 text-xs font-medium text-stone-400">
|
|
{currentRound ? currentRound.courseName : 'No Active Round'}
|
|
{currentRound?.isCompleted && (
|
|
<span className="ml-1.5 text-[10px] font-bold text-emerald-400 bg-stone-800 border border-emerald-500/30 px-1.5 py-0.5 rounded flex items-center gap-1">
|
|
<Lock className="w-2.5 h-2.5" />
|
|
Saved
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Zone 2: Navigation Links */}
|
|
<nav className="hidden md:flex items-center gap-1 sm:gap-1.5 text-sm font-medium">
|
|
<button
|
|
onClick={() => setActiveTab('hole')}
|
|
className={`px-3 py-1.5 rounded-lg transition-colors whitespace-nowrap ${
|
|
activeTab === 'hole'
|
|
? 'bg-stone-800 text-emerald-400 font-semibold shadow-sm'
|
|
: 'text-stone-300 hover:text-white hover:bg-stone-800/60'
|
|
}`}
|
|
>
|
|
Hole Entry
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('scorecard')}
|
|
className={`px-3 py-1.5 rounded-lg transition-colors whitespace-nowrap ${
|
|
activeTab === 'scorecard'
|
|
? 'bg-stone-800 text-emerald-400 font-semibold shadow-sm'
|
|
: 'text-stone-300 hover:text-white hover:bg-stone-800/60'
|
|
}`}
|
|
>
|
|
Scorecard
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('analytics')}
|
|
className={`px-3 py-1.5 rounded-lg transition-colors whitespace-nowrap ${
|
|
activeTab === 'analytics'
|
|
? 'bg-stone-800 text-emerald-400 font-semibold shadow-sm'
|
|
: 'text-stone-300 hover:text-white hover:bg-stone-800/60'
|
|
}`}
|
|
>
|
|
Analytics
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('settings')}
|
|
className={`px-3 py-1.5 rounded-lg transition-colors whitespace-nowrap ${
|
|
activeTab === 'settings'
|
|
? 'bg-stone-800 text-emerald-400 font-semibold shadow-sm'
|
|
: 'text-stone-300 hover:text-white hover:bg-stone-800/60'
|
|
}`}
|
|
>
|
|
Settings
|
|
</button>
|
|
|
|
{/* Community / Leaderboard Link */}
|
|
<button
|
|
onClick={onOpenCommunity}
|
|
className="px-3 py-1.5 rounded-lg text-amber-300/90 hover:text-amber-200 hover:bg-amber-500/10 transition-colors flex items-center gap-1.5"
|
|
title={user ? 'Community Leaderboards' : 'Community (Sign-in required)'}
|
|
>
|
|
<Users className="w-3.5 h-3.5 text-amber-400" />
|
|
<span>Community</span>
|
|
{!user && <Lock className="w-2.5 h-2.5 text-amber-400/80" />}
|
|
</button>
|
|
</nav>
|
|
|
|
{/* Zone 3: Score Quick Pill + Actions + Auth */}
|
|
<div className="flex items-center gap-1.5 sm:gap-2.5 shrink-0">
|
|
{/* Live Score Tally */}
|
|
<div className="flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-1 bg-stone-800/90 border border-stone-700/80 rounded-lg text-xs font-mono tabular-nums">
|
|
<span className="text-stone-400 hidden xs:inline">Score</span>
|
|
<span className="font-bold text-white text-xs sm:text-sm">
|
|
{scoreSummary.totalGross > 0 ? scoreSummary.totalGross : '—'}
|
|
</span>
|
|
<span
|
|
className={`font-semibold px-1.5 py-0.5 rounded text-[11px] sm:text-xs ${
|
|
scoreSummary.toPar < 0
|
|
? 'bg-emerald-500/20 text-emerald-400'
|
|
: scoreSummary.toPar > 0
|
|
? 'bg-amber-500/20 text-amber-400'
|
|
: 'bg-stone-700 text-stone-300'
|
|
}`}
|
|
>
|
|
{formatToPar(scoreSummary.toPar)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Achievements Trophy Button */}
|
|
<button
|
|
onClick={onOpenAchievements}
|
|
className="flex items-center gap-1 px-2 sm:px-2.5 py-1.5 bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 border border-amber-500/30 rounded-lg text-xs font-bold transition-all shadow-sm"
|
|
title={user ? 'Career Trophies & Achievements' : 'Trophies (Sign-in required)'}
|
|
>
|
|
<Trophy className="w-3.5 h-3.5 text-amber-400" />
|
|
{user ? (
|
|
<span className="font-mono text-xs">{achievementsUnlockedCount}</span>
|
|
) : (
|
|
<Lock className="w-3 h-3 text-amber-400" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Mobile Community Button */}
|
|
<button
|
|
onClick={onOpenCommunity}
|
|
className="md:hidden p-1.5 text-amber-300 hover:bg-amber-500/10 rounded-lg transition relative"
|
|
title={user ? 'Community Leaderboard' : 'Community (Sign-in required)'}
|
|
>
|
|
<Users className="w-4 h-4 text-amber-400" />
|
|
{!user && (
|
|
<Lock className="w-2.5 h-2.5 text-amber-400 absolute top-1 right-1" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Saved Rounds */}
|
|
<button
|
|
onClick={onOpenRounds}
|
|
title="Saved Rounds"
|
|
className="p-1.5 sm:p-2 text-stone-300 hover:text-white hover:bg-stone-800 rounded-lg transition-colors"
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
</button>
|
|
|
|
{/* New Round */}
|
|
<button
|
|
onClick={onNewRound}
|
|
className="hidden sm:flex items-center gap-1 sm:gap-1.5 px-2.5 sm:px-3 py-1.5 text-xs font-semibold text-white bg-emerald-600 hover:bg-emerald-500 rounded-lg shadow-sm transition-colors whitespace-nowrap"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>New Round</span>
|
|
</button>
|
|
|
|
{/* User Auth Profile Button */}
|
|
{user ? (
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setProfileDropdownOpen(!profileDropdownOpen)}
|
|
className="flex items-center gap-2 px-2.5 py-1 bg-stone-800/90 hover:bg-stone-750 border border-stone-700 rounded-xl text-xs text-stone-200 transition"
|
|
>
|
|
{user.avatarUrl ? (
|
|
<img src={user.avatarUrl} alt={user.name} className="w-5 h-5 rounded-full object-cover" />
|
|
) : (
|
|
<div className="w-5 h-5 rounded-full bg-emerald-600 text-[10px] font-bold text-white flex items-center justify-center">
|
|
{user.name.charAt(0).toUpperCase()}
|
|
</div>
|
|
)}
|
|
<div className="hidden sm:flex flex-col text-left leading-tight">
|
|
<span className="font-semibold text-[11px] truncate max-w-[90px]">{user.name}</span>
|
|
<span className="text-[10px] text-emerald-400 font-mono">Hcp {user.handicapIndex}</span>
|
|
</div>
|
|
<ChevronDown className="w-3 h-3 text-stone-400" />
|
|
</button>
|
|
|
|
{/* Dropdown Menu */}
|
|
{profileDropdownOpen && (
|
|
<div className="absolute right-0 mt-2 w-64 bg-stone-900 border border-stone-800 rounded-2xl shadow-2xl p-3 z-50 animate-fadeIn space-y-2">
|
|
<div className="px-2 py-1.5 border-b border-stone-800/80">
|
|
<p className="font-bold text-xs text-stone-100">{user.name}</p>
|
|
<p className="text-[10px] text-stone-400 font-mono">@{user.username}</p>
|
|
<p className="text-[10px] text-stone-500 mt-0.5">{user.homeClub}</p>
|
|
</div>
|
|
|
|
<div className="px-2 py-1 flex items-center justify-between text-xs">
|
|
<span className="text-stone-400">Handicap Index:</span>
|
|
<span className="font-mono font-bold text-emerald-400">{user.handicapIndex}</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={async () => {
|
|
await updateProfile({ isPublic: !user.isPublic });
|
|
}}
|
|
className="w-full px-2 py-1.5 rounded-lg text-left text-xs text-stone-300 hover:bg-stone-800 flex items-center justify-between transition"
|
|
>
|
|
<span className="flex items-center gap-1.5">
|
|
{user.isPublic ? <Eye className="w-3.5 h-3.5 text-emerald-400" /> : <EyeOff className="w-3.5 h-3.5 text-stone-400" />}
|
|
Public Stats:
|
|
</span>
|
|
<span className={`font-semibold text-[11px] ${user.isPublic ? 'text-emerald-400' : 'text-stone-500'}`}>
|
|
{user.isPublic ? 'Enabled' : 'Private'}
|
|
</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
setProfileDropdownOpen(false);
|
|
onOpenCommunity();
|
|
}}
|
|
className="w-full px-2 py-1.5 rounded-lg text-left text-xs text-stone-300 hover:bg-stone-800 flex items-center gap-2 transition"
|
|
>
|
|
<Users className="w-3.5 h-3.5 text-amber-400" />
|
|
View Community Leaderboard
|
|
</button>
|
|
|
|
<div className="pt-1 border-t border-stone-800">
|
|
<button
|
|
onClick={() => {
|
|
setProfileDropdownOpen(false);
|
|
logout();
|
|
}}
|
|
className="w-full px-2 py-1.5 rounded-lg text-left text-xs text-rose-400 hover:bg-rose-500/10 flex items-center gap-2 transition"
|
|
>
|
|
<LogOut className="w-3.5 h-3.5" />
|
|
Sign Out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={onOpenAuth}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-stone-800 hover:bg-stone-700 text-stone-200 border border-stone-700 rounded-xl text-xs font-semibold shadow-sm transition"
|
|
>
|
|
<UserIcon className="w-3.5 h-3.5 text-emerald-400" />
|
|
<span>Sign In</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Mobile Bottom Tab Bar (Thumb Zone Ergonomics) */}
|
|
<div className="md:hidden fixed bottom-0 left-0 right-0 z-40 bg-stone-900/95 backdrop-blur-lg border-t border-stone-800">
|
|
<div className="grid grid-cols-4 items-center h-16 max-w-lg mx-auto px-2">
|
|
<button
|
|
onClick={() => setActiveTab('hole')}
|
|
className={`flex flex-col items-center justify-center py-1 transition-colors ${
|
|
activeTab === 'hole' ? 'text-emerald-400 font-semibold' : 'text-stone-400 hover:text-stone-200'
|
|
}`}
|
|
>
|
|
<Flag className="w-5 h-5" />
|
|
<span className="text-[10px] mt-1">Hole Entry</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('scorecard')}
|
|
className={`flex flex-col items-center justify-center py-1 transition-colors ${
|
|
activeTab === 'scorecard' ? 'text-emerald-400 font-semibold' : 'text-stone-400 hover:text-stone-200'
|
|
}`}
|
|
>
|
|
<Table className="w-5 h-5" />
|
|
<span className="text-[10px] mt-1">Scorecard</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('analytics')}
|
|
className={`flex flex-col items-center justify-center py-1 transition-colors ${
|
|
activeTab === 'analytics' ? 'text-emerald-400 font-semibold' : 'text-stone-400 hover:text-stone-200'
|
|
}`}
|
|
>
|
|
<BarChart3 className="w-5 h-5" />
|
|
<span className="text-[10px] mt-1">Analytics</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('settings')}
|
|
className={`flex flex-col items-center justify-center py-1 transition-colors ${
|
|
activeTab === 'settings' ? 'text-emerald-400 font-semibold' : 'text-stone-400 hover:text-stone-200'
|
|
}`}
|
|
>
|
|
<Settings className="w-5 h-5" />
|
|
<span className="text-[10px] mt-1">Settings</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|