feat: implement @nav parallel routes for game name display in mini navigation
- Add @nav/default.tsx for fallback nav content - Add @nav/games/matching/page.tsx for Memory Pairs game nav - Add @nav/games/memory-quiz/page.tsx for Memory Lightning game nav - Update AppNav to use @nav slot content with header-based fallback - Remove debug logging from navigation components The @nav parallel routes pattern allows each game route to declare its own navigation content server-side, keeping nav content colocated with routes while avoiding client-side state management or lazy loading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
3
apps/web/src/app/@nav/default.tsx
Normal file
3
apps/web/src/app/@nav/default.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function DefaultNav() {
|
||||
return null // No navigation content for routes without specific @nav slots
|
||||
}
|
||||
14
apps/web/src/app/@nav/games/matching/page.tsx
Normal file
14
apps/web/src/app/@nav/games/matching/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function MatchingNav() {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧩 Memory Pairs
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
14
apps/web/src/app/@nav/games/memory-quiz/page.tsx
Normal file
14
apps/web/src/app/@nav/games/memory-quiz/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function MemoryQuizNav() {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧠 Memory Lightning
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
52
apps/web/src/components/AppNav.tsx
Normal file
52
apps/web/src/components/AppNav.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { headers } from 'next/headers'
|
||||
import { AppNavBar } from './AppNavBar'
|
||||
|
||||
interface AppNavProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function getNavContentForPath(pathname: string): React.ReactNode {
|
||||
// Route-based nav content - no lazy loading needed
|
||||
if (pathname === '/games/matching' || pathname.startsWith('/arcade') && pathname.includes('matching')) {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧩 Memory Pairs
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
|
||||
if (pathname === '/games/memory-quiz' || pathname.startsWith('/arcade') && pathname.includes('memory-quiz')) {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧠 Memory Lightning
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function AppNav({ children }: AppNavProps) {
|
||||
const headersList = headers()
|
||||
const pathname = headersList.get('x-pathname') || ''
|
||||
|
||||
// Use @nav slot content if available, otherwise fall back to route-based detection
|
||||
const navContent = children || getNavContentForPath(pathname)
|
||||
|
||||
return <AppNavBar navSlot={navContent} />
|
||||
}
|
||||
@@ -1,91 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, hstack } from '../../styled-system/patterns'
|
||||
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
|
||||
import { useFullscreen } from '../contexts/FullscreenContext'
|
||||
import { useGameTheme } from '../contexts/GameThemeContext'
|
||||
|
||||
interface AppNavBarProps {
|
||||
variant?: 'full' | 'minimal'
|
||||
navSlot?: React.ReactNode
|
||||
}
|
||||
|
||||
export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const isGamePage = pathname?.startsWith('/games')
|
||||
const isArcadePage = pathname?.startsWith('/arcade')
|
||||
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
|
||||
const { theme: gameTheme, isHydrated } = useGameTheme()
|
||||
|
||||
// Route-based theme detection as fallback for page reloads
|
||||
const getRouteBasedTheme = () => {
|
||||
if (pathname === '/games/memory-quiz') {
|
||||
return {
|
||||
gameName: "Memory Lightning",
|
||||
backgroundColor: "linear-gradient(to bottom right, #f0fdf4, #eff6ff)"
|
||||
}
|
||||
}
|
||||
if (pathname === '/games/matching') {
|
||||
return {
|
||||
gameName: "Memory Pairs",
|
||||
backgroundColor: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Use context theme if available, otherwise fall back to route-based detection
|
||||
const currentTheme = gameTheme || getRouteBasedTheme()
|
||||
|
||||
// Helper function to get themed background colors
|
||||
const getThemedBackground = (opacity: number = 0.85) => {
|
||||
// Apply theming for route-based themes immediately, or after hydration for context themes
|
||||
if (currentTheme?.backgroundColor && (getRouteBasedTheme() || isHydrated)) {
|
||||
const color = currentTheme.backgroundColor
|
||||
if (color.startsWith('#')) {
|
||||
// Convert hex to rgba
|
||||
const hex = color.slice(1)
|
||||
const r = parseInt(hex.slice(0, 2), 16)
|
||||
const g = parseInt(hex.slice(2, 4), 16)
|
||||
const b = parseInt(hex.slice(4, 6), 16)
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`
|
||||
} else if (color.startsWith('rgb')) {
|
||||
// Handle rgb/rgba formats
|
||||
const match = color.match(/rgba?\(([^)]+)\)/)
|
||||
if (match) {
|
||||
const values = match[1].split(',').map(v => v.trim())
|
||||
if (values.length >= 3) {
|
||||
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`
|
||||
}
|
||||
}
|
||||
} else if (color.startsWith('linear-gradient')) {
|
||||
// Extract colors from gradient and use dominant color
|
||||
const hexMatch = color.match(/#[0-9a-fA-F]{6}/g)
|
||||
if (hexMatch && hexMatch.length > 0) {
|
||||
// Use the first color from the gradient
|
||||
const hex = hexMatch[0].slice(1)
|
||||
const r = parseInt(hex.slice(0, 2), 16)
|
||||
const g = parseInt(hex.slice(2, 4), 16)
|
||||
const b = parseInt(hex.slice(4, 6), 16)
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`
|
||||
}
|
||||
// Fallback: try to extract rgb values
|
||||
const rgbMatch = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g)
|
||||
if (rgbMatch && rgbMatch.length > 0) {
|
||||
const values = rgbMatch[0].match(/\d+/g)
|
||||
if (values && values.length >= 3) {
|
||||
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`
|
||||
}
|
||||
}
|
||||
// Final fallback for gradients
|
||||
return isFullscreen ? `rgba(0, 0, 0, ${opacity})` : `rgba(255, 255, 255, ${opacity})`
|
||||
}
|
||||
}
|
||||
return isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'
|
||||
}
|
||||
|
||||
// Auto-detect variant based on context
|
||||
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
|
||||
@@ -103,42 +38,36 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
transition: 'all 0.3s ease'
|
||||
})}>
|
||||
<div className={hstack({ gap: '2' })}>
|
||||
{/* Game branding (fullscreen only) */}
|
||||
{isFullscreen && (isArcadePage || isGamePage) && (
|
||||
{/* Game branding from slot */}
|
||||
{navSlot && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '8px 16px',
|
||||
background: currentTheme ? getThemedBackground(0.85) : 'rgba(0, 0, 0, 0.85)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
||||
backdropFilter: 'blur(15px)'
|
||||
backdropFilter: isFullscreen ? 'blur(15px)' : 'none'
|
||||
}}
|
||||
>
|
||||
<h1 className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
background: gameTheme ? 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)' : 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
})}>
|
||||
🕹️ {currentTheme?.gameName || (isArcadePage ? 'Arcade' : 'Game')}
|
||||
</h1>
|
||||
<div className={css({
|
||||
px: '2',
|
||||
py: '1',
|
||||
background: 'rgba(34, 197, 94, 0.2)',
|
||||
border: '1px solid rgba(34, 197, 94, 0.3)',
|
||||
rounded: 'full',
|
||||
fontSize: 'xs',
|
||||
color: 'green.300',
|
||||
fontWeight: 'semibold'
|
||||
})}>
|
||||
✨ FULLSCREEN
|
||||
</div>
|
||||
{navSlot}
|
||||
{isFullscreen && (
|
||||
<div className={css({
|
||||
px: '2',
|
||||
py: '1',
|
||||
background: 'rgba(34, 197, 94, 0.2)',
|
||||
border: '1px solid rgba(34, 197, 94, 0.3)',
|
||||
rounded: 'full',
|
||||
fontSize: 'xs',
|
||||
color: 'green.300',
|
||||
fontWeight: 'semibold'
|
||||
})}>
|
||||
✨ FULLSCREEN
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -149,7 +78,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
background: currentTheme ? getThemedBackground(isFullscreen ? 0.85 : 1) : (isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'),
|
||||
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
||||
@@ -198,7 +127,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
background: currentTheme ? getThemedBackground(isFullscreen ? 0.85 : 1) : (isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'),
|
||||
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
||||
|
||||
Reference in New Issue
Block a user