Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 23x 23x 23x 23x 23x 23x 23x 12x 12x 12x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 12x 23x 23x 23x 23x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 2x 2x 6x 6x 23x 23x 23x 23x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 23x 23x 23x 1x 1x 1x 1x 1x 1x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 1x 1x 1x 1x 1x 1x 27x 27x 4x 4x 23x 23x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x | 'use client'
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'
import { useRouter } from 'next/navigation'
// ============================================================================
// Types
// ============================================================================
export interface TransitionState {
type: 'quicklook-to-dashboard'
studentId: string
studentName: string
studentEmoji: string
studentColor: string
originBounds: {
left: number
top: number
width: number
height: number
}
timestamp: number
}
interface PageTransitionContextValue {
/** Current transition state (null if no transition in progress) */
transitionState: TransitionState | null
/** Whether the overlay is currently visible */
isTransitioning: boolean
/** Whether we're in the revealing phase (overlay fading out, content fading in) */
isRevealing: boolean
/** Start a transition to a student dashboard */
startTransition: (state: Omit<TransitionState, 'type' | 'timestamp'>) => void
/** Signal that the destination page is ready to be revealed */
signalReady: () => void
/** Clear any pending transition state */
clearTransition: () => void
}
const PageTransitionContext = createContext<PageTransitionContextValue | null>(null)
// ============================================================================
// Constants
// ============================================================================
const STORAGE_KEY = 'page-transition-state'
const TRANSITION_MAX_AGE_MS = 3000 // Transition state expires after 3 seconds
// ============================================================================
// Provider
// ============================================================================
export function PageTransitionProvider({ children }: { children: ReactNode }) {
const router = useRouter()
const [transitionState, setTransitionState] = useState<TransitionState | null>(null)
const [isTransitioning, setIsTransitioning] = useState(false)
const [phase, setPhase] = useState<'idle' | 'expanding' | 'navigating' | 'revealing'>('idle')
// Check for pending transition on mount (destination page)
useEffect(() => {
try {
const stored = sessionStorage.getItem(STORAGE_KEY)
if (stored) {
const state = JSON.parse(stored) as TransitionState
const age = Date.now() - state.timestamp
if (age < TRANSITION_MAX_AGE_MS) {
// Valid transition state - we're on the destination page
// Start in 'navigating' phase - the overlay is covering, waiting for signalReady()
console.log('[PageTransitionContext] Found valid transition state, age:', age)
setTransitionState(state)
setIsTransitioning(true)
setPhase('navigating') // Don't reveal until signalReady() is called
} else {
// Expired - clean up
sessionStorage.removeItem(STORAGE_KEY)
}
}
} catch {
// Ignore storage errors
}
}, [])
const startTransition = useCallback(
(stateData: Omit<TransitionState, 'type' | 'timestamp'>) => {
const state: TransitionState = {
...stateData,
type: 'quicklook-to-dashboard',
timestamp: Date.now(),
}
// Store in sessionStorage for the destination page
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state))
} catch {
// If storage fails, still do the navigation but without smooth transition
}
setTransitionState(state)
setIsTransitioning(true)
setPhase('expanding')
// Prefetch the destination
router.prefetch(`/practice/${state.studentId}/dashboard`)
// Start navigation after expansion animation begins (200ms into the animation)
setTimeout(() => {
setPhase('navigating')
router.push(`/practice/${state.studentId}/dashboard`)
}, 250)
},
[router]
)
const signalReady = useCallback(() => {
// Destination page signals it's ready - start reveal
console.log('[PageTransitionContext] signalReady called, setting phase to revealing')
setPhase('revealing')
// After reveal animation completes, clean up
setTimeout(() => {
console.log('[PageTransitionContext] Cleanup: setting isTransitioning to false')
setIsTransitioning(false)
setTransitionState(null)
setPhase('idle')
try {
sessionStorage.removeItem(STORAGE_KEY)
} catch {
// Ignore
}
}, 300)
}, [])
const clearTransition = useCallback(() => {
setIsTransitioning(false)
setTransitionState(null)
setPhase('idle')
try {
sessionStorage.removeItem(STORAGE_KEY)
} catch {
// Ignore
}
}, [])
const isRevealing = phase === 'revealing'
return (
<PageTransitionContext.Provider
value={{
transitionState,
isTransitioning,
isRevealing,
startTransition,
signalReady,
clearTransition,
}}
>
{children}
</PageTransitionContext.Provider>
)
}
// ============================================================================
// Hook
// ============================================================================
export function usePageTransition() {
const context = useContext(PageTransitionContext)
if (!context) {
throw new Error('usePageTransition must be used within a PageTransitionProvider')
}
return context
}
/**
* Hook for destination pages to check if they were navigated to via transition
* Returns the transition state if valid, and a signalReady callback
*/
export function useIncomingTransition() {
const { transitionState, isTransitioning, isRevealing, signalReady, clearTransition } =
usePageTransition()
return {
/** Whether we arrived via a page transition */
hasTransition: isTransitioning && transitionState !== null,
/** Whether the overlay is fading out (content should fade in) */
isRevealing,
/** The transition state (student info, origin bounds, etc.) */
transitionState,
/** Call this when the page is ready to be revealed */
signalReady,
/** Call this to abort/clear the transition */
clearTransition,
}
}
|