Compare commits
5 Commits
abacus-rea
...
abacus-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c46f3a7ba | ||
|
|
1fe12c4837 | ||
|
|
b7e7c4beff | ||
|
|
423ba55350 | ||
|
|
885fc725dc |
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>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +1,34 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import './globals.css'
|
||||
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
|
||||
import { UserProfileProvider } from '@/contexts/UserProfileContext'
|
||||
import { GameModeProvider } from '@/contexts/GameModeContext'
|
||||
import { FullscreenProvider } from '@/contexts/FullscreenContext'
|
||||
import { GameThemeProvider } from '@/contexts/GameThemeContext'
|
||||
import { AppNavBar } from '@/components/AppNavBar'
|
||||
import { ClientProviders } from '@/components/ClientProviders'
|
||||
import { AppNav } from '@/components/AppNav'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Soroban Flashcard Generator',
|
||||
description: 'Create beautiful, educational soroban flashcards with authentic Japanese abacus representations',
|
||||
viewport: {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
nav,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
nav: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AbacusDisplayProvider>
|
||||
<UserProfileProvider>
|
||||
<GameModeProvider>
|
||||
<FullscreenProvider>
|
||||
<GameThemeProvider>
|
||||
<AppNavBar />
|
||||
{children}
|
||||
</GameThemeProvider>
|
||||
</FullscreenProvider>
|
||||
</GameModeProvider>
|
||||
</UserProfileProvider>
|
||||
</AbacusDisplayProvider>
|
||||
<ClientProviders>
|
||||
<AppNav>{nav}</AppNav>
|
||||
{children}
|
||||
</ClientProviders>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
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)',
|
||||
|
||||
21
apps/web/src/middleware.ts
Normal file
21
apps/web/src/middleware.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
// Add pathname to headers so Server Components can access it
|
||||
const response = NextResponse.next()
|
||||
response.headers.set('x-pathname', request.nextUrl.pathname)
|
||||
return response
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
*/
|
||||
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||
],
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
# [1.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.3.0...abacus-react-v1.4.0) (2025-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export missing hooks and types from @soroban/abacus-react package ([423ba55](https://github.com/antialias/soroban-abacus-flashcards/commit/423ba5535023928f1e0198b2bd01c3c6cf7ee848))
|
||||
* migrate viewport from metadata to separate viewport export ([1fe12c4](https://github.com/antialias/soroban-abacus-flashcards/commit/1fe12c4837b1229d0f0ab93c55d0ffb504eb8721))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add middleware for pathname header support in [@nav](https://github.com/nav) fallback ([b7e7c4b](https://github.com/antialias/soroban-abacus-flashcards/commit/b7e7c4beff1e37e90e9e20a890c5af7a134a7fca))
|
||||
* implement [@nav](https://github.com/nav) parallel routes for game name display in mini navigation ([885fc72](https://github.com/antialias/soroban-abacus-flashcards/commit/885fc725dc0bb41bbb5e500c2c907c6182192854))
|
||||
|
||||
# [1.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.2.0...abacus-react-v1.3.0) (2025-09-29)
|
||||
|
||||
|
||||
|
||||
@@ -3,4 +3,18 @@ export type {
|
||||
AbacusConfig,
|
||||
BeadConfig,
|
||||
AbacusDimensions
|
||||
} from './AbacusReact';
|
||||
} from './AbacusReact';
|
||||
|
||||
export {
|
||||
useAbacusConfig,
|
||||
useAbacusDisplay,
|
||||
getDefaultAbacusConfig,
|
||||
AbacusDisplayProvider
|
||||
} from './AbacusContext';
|
||||
export type {
|
||||
ColorScheme,
|
||||
BeadShape,
|
||||
ColorPalette,
|
||||
AbacusDisplayConfig,
|
||||
AbacusDisplayContextType
|
||||
} from './AbacusContext';
|
||||
Reference in New Issue
Block a user