refactor: consolidate abacus display context management
- Remove duplicate AbacusDisplayContext in favor of centralized abacus-react provider - Update all components to use useAbacusDisplay and useAbacusConfig hooks from @soroban/abacus-react - Create ClientProviders component to centralize provider setup - Simplify context management across the application 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationForm
|
||||
import { LivePreview } from '@/components/LivePreview'
|
||||
import { GenerationProgress } from '@/components/GenerationProgress'
|
||||
import { StyleControls } from '@/components/StyleControls'
|
||||
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
|
||||
import { useAbacusConfig } from '@soroban/abacus-react'
|
||||
|
||||
// Complete, validated configuration ready for generation
|
||||
export interface FlashcardConfig {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { container, stack, hstack, grid } from '../../../styled-system/patterns'
|
||||
import { TypstSoroban } from '@/components/TypstSoroban'
|
||||
import { InteractiveAbacus } from '@/components/InteractiveAbacus'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
|
||||
import { useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
|
||||
import { getTutorialForEditor } from '@/utils/tutorialConverter'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { stack, hstack } from '../../styled-system/patterns'
|
||||
import { useAbacusDisplay, ColorScheme, BeadShape } from '@/contexts/AbacusDisplayContext'
|
||||
import { useAbacusDisplay, ColorScheme, BeadShape } from '@soroban/abacus-react'
|
||||
|
||||
interface AbacusDisplayDropdownProps {
|
||||
isFullscreen?: boolean
|
||||
|
||||
25
apps/web/src/components/ClientProviders.tsx
Normal file
25
apps/web/src/components/ClientProviders.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode } from 'react'
|
||||
import { AbacusDisplayProvider } from '@soroban/abacus-react'
|
||||
import { UserProfileProvider } from '@/contexts/UserProfileContext'
|
||||
import { GameModeProvider } from '@/contexts/GameModeContext'
|
||||
import { FullscreenProvider } from '@/contexts/FullscreenContext'
|
||||
|
||||
interface ClientProvidersProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ClientProviders({ children }: ClientProvidersProps) {
|
||||
return (
|
||||
<AbacusDisplayProvider>
|
||||
<UserProfileProvider>
|
||||
<GameModeProvider>
|
||||
<FullscreenProvider>
|
||||
{children}
|
||||
</FullscreenProvider>
|
||||
</GameModeProvider>
|
||||
</UserProfileProvider>
|
||||
</AbacusDisplayProvider>
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as Switch from '@radix-ui/react-switch'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { stack, hstack, grid } from '../../styled-system/patterns'
|
||||
import { FlashcardFormState } from '@/app/create/page'
|
||||
import { useAbacusDisplay } from '@/contexts/AbacusDisplayContext'
|
||||
import { useAbacusDisplay } from '@soroban/abacus-react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface StyleControlsProps {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||
import { generateSorobanSVG, getWasmStatus, triggerWasmPreload, type SorobanConfig } from '@/lib/typst-soroban'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
|
||||
import { useAbacusConfig } from '@soroban/abacus-react'
|
||||
|
||||
interface TypstSorobanProps {
|
||||
number: number
|
||||
|
||||
@@ -15,7 +15,7 @@ import { TutorialUIProvider } from './TutorialUIContext'
|
||||
import { CoachBar } from './CoachBar/CoachBar'
|
||||
import { PedagogicalDecompositionDisplay } from './PedagogicalDecompositionDisplay'
|
||||
import { DecompositionWithReasons } from './DecompositionWithReasons'
|
||||
import { useAbacusDisplay } from '@/contexts/AbacusDisplayContext'
|
||||
import { useAbacusDisplay } from '@soroban/abacus-react'
|
||||
import './CoachBar/coachbar.css'
|
||||
|
||||
// Helper function to find the topmost bead with arrows
|
||||
@@ -1308,6 +1308,8 @@ function TutorialPlayerContent({
|
||||
colorScheme={abacusConfig.colorScheme}
|
||||
beadShape={abacusConfig.beadShape}
|
||||
hideInactiveBeads={abacusConfig.hideInactiveBeads}
|
||||
soundEnabled={abacusConfig.soundEnabled}
|
||||
soundVolume={abacusConfig.soundVolume}
|
||||
highlightBeads={currentStep.highlightBeads}
|
||||
stepBeadHighlights={currentStepBeads}
|
||||
currentStep={currentMultiStep}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'
|
||||
|
||||
// Abacus display configuration types
|
||||
export type ColorScheme = 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
|
||||
export type BeadShape = 'diamond' | 'circle' | 'square'
|
||||
|
||||
export interface AbacusDisplayConfig {
|
||||
colorScheme: ColorScheme
|
||||
beadShape: BeadShape
|
||||
hideInactiveBeads: boolean
|
||||
coloredNumerals: boolean
|
||||
scaleFactor: number
|
||||
soundEnabled: boolean
|
||||
soundVolume: number
|
||||
}
|
||||
|
||||
export interface AbacusDisplayContextType {
|
||||
config: AbacusDisplayConfig
|
||||
updateConfig: (updates: Partial<AbacusDisplayConfig>) => void
|
||||
resetToDefaults: () => void
|
||||
}
|
||||
|
||||
// Default configuration - matches current create page defaults
|
||||
const DEFAULT_CONFIG: AbacusDisplayConfig = {
|
||||
colorScheme: 'place-value',
|
||||
beadShape: 'diamond',
|
||||
hideInactiveBeads: false,
|
||||
coloredNumerals: false,
|
||||
scaleFactor: 1.0, // Normalized for display, can be scaled per component
|
||||
soundEnabled: true,
|
||||
soundVolume: 0.8
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'soroban-abacus-display-config'
|
||||
|
||||
// Load config from localStorage with fallback to defaults
|
||||
function loadConfigFromStorage(): AbacusDisplayConfig {
|
||||
if (typeof window === 'undefined') return DEFAULT_CONFIG
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
// Validate that all required fields are present and have valid values
|
||||
return {
|
||||
colorScheme: ['monochrome', 'place-value', 'heaven-earth', 'alternating'].includes(parsed.colorScheme)
|
||||
? parsed.colorScheme : DEFAULT_CONFIG.colorScheme,
|
||||
beadShape: ['diamond', 'circle', 'square'].includes(parsed.beadShape)
|
||||
? parsed.beadShape : DEFAULT_CONFIG.beadShape,
|
||||
hideInactiveBeads: typeof parsed.hideInactiveBeads === 'boolean'
|
||||
? parsed.hideInactiveBeads : DEFAULT_CONFIG.hideInactiveBeads,
|
||||
coloredNumerals: typeof parsed.coloredNumerals === 'boolean'
|
||||
? parsed.coloredNumerals : DEFAULT_CONFIG.coloredNumerals,
|
||||
scaleFactor: typeof parsed.scaleFactor === 'number' && parsed.scaleFactor > 0
|
||||
? parsed.scaleFactor : DEFAULT_CONFIG.scaleFactor,
|
||||
soundEnabled: typeof parsed.soundEnabled === 'boolean'
|
||||
? parsed.soundEnabled : DEFAULT_CONFIG.soundEnabled,
|
||||
soundVolume: typeof parsed.soundVolume === 'number' && parsed.soundVolume >= 0 && parsed.soundVolume <= 1
|
||||
? parsed.soundVolume : DEFAULT_CONFIG.soundVolume
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load abacus config from localStorage:', error)
|
||||
}
|
||||
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
|
||||
// Save config to localStorage
|
||||
function saveConfigToStorage(config: AbacusDisplayConfig): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
||||
} catch (error) {
|
||||
console.warn('Failed to save abacus config to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const AbacusDisplayContext = createContext<AbacusDisplayContextType | null>(null)
|
||||
|
||||
export function useAbacusDisplay() {
|
||||
const context = useContext(AbacusDisplayContext)
|
||||
if (!context) {
|
||||
throw new Error('useAbacusDisplay must be used within an AbacusDisplayProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface AbacusDisplayProviderProps {
|
||||
children: ReactNode
|
||||
initialConfig?: Partial<AbacusDisplayConfig>
|
||||
}
|
||||
|
||||
export function AbacusDisplayProvider({
|
||||
children,
|
||||
initialConfig = {}
|
||||
}: AbacusDisplayProviderProps) {
|
||||
const [config, setConfig] = useState<AbacusDisplayConfig>(() => {
|
||||
// Always start with defaults to ensure server/client consistency
|
||||
return { ...DEFAULT_CONFIG, ...initialConfig }
|
||||
})
|
||||
|
||||
// Load from localStorage only after hydration
|
||||
useEffect(() => {
|
||||
const stored = loadConfigFromStorage()
|
||||
setConfig(stored)
|
||||
}, [])
|
||||
|
||||
// Save to localStorage whenever config changes
|
||||
useEffect(() => {
|
||||
saveConfigToStorage(config)
|
||||
}, [config])
|
||||
|
||||
const updateConfig = useCallback((updates: Partial<AbacusDisplayConfig>) => {
|
||||
setConfig(prev => {
|
||||
const newConfig = { ...prev, ...updates }
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const resetToDefaults = useCallback(() => {
|
||||
setConfig(DEFAULT_CONFIG)
|
||||
}, [])
|
||||
|
||||
const value: AbacusDisplayContextType = {
|
||||
config,
|
||||
updateConfig,
|
||||
resetToDefaults
|
||||
}
|
||||
|
||||
return (
|
||||
<AbacusDisplayContext.Provider value={value}>
|
||||
{children}
|
||||
</AbacusDisplayContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience hook for components that need specific config values
|
||||
export function useAbacusConfig() {
|
||||
const { config } = useAbacusDisplay()
|
||||
return config
|
||||
}
|
||||
Reference in New Issue
Block a user