Skip to content

Commit 1087c40

Browse files
fix: autoplay tour on first visit
1 parent 2418d48 commit 1087c40

8 files changed

Lines changed: 166 additions & 113 deletions

File tree

src/components/Misc/GameInfo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const GameInfo: React.FC<Props> = ({
2828
onGameListClick,
2929
}: Props) => {
3030
const { setInstructionsModalProps } = useContext(ModalContext)
31-
const { startTour, hasCompletedTour } = useTour()
31+
const { startTour } = useTour()
3232

3333
return (
3434
<div className="flex w-full flex-col items-start justify-start gap-1 overflow-hidden bg-background-1 p-3 md:rounded">

src/components/Tour/TourManager.tsx

Lines changed: 126 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -54,36 +54,90 @@ export const TourManager: React.FC = () => {
5454

5555
// If position is null and we haven't found the target yet, retry after a short delay
5656
if (!position) {
57-
const retryCount = 5
58-
const retryDelay = 100
57+
const retryCount = 10
58+
const retryDelay = 200
5959

6060
const retry = (attempt: number) => {
61-
if (attempt >= retryCount) return
61+
if (attempt >= retryCount) {
62+
return
63+
}
6264

6365
setTimeout(() => {
6466
const retryPosition = calculateTargetPosition()
6567
if (retryPosition) {
6668
setTargetPosition(retryPosition)
69+
// Auto-scroll to ensure target element is visible
70+
ensureTargetIsVisible()
6771
} else {
6872
retry(attempt + 1)
6973
}
7074
}, retryDelay)
7175
}
7276

7377
retry(0)
78+
} else {
79+
// Auto-scroll to ensure target element is visible
80+
ensureTargetIsVisible()
7481
}
7582
}
7683

77-
updatePosition()
84+
const ensureTargetIsVisible = () => {
85+
const targetElement = document.getElementById(currentStep.targetId)
86+
if (targetElement) {
87+
const rect = targetElement.getBoundingClientRect()
88+
const viewportHeight = window.innerHeight
89+
const margin = 100 // Give some margin
90+
91+
// Only scroll if element is significantly off-screen
92+
const isSignificantlyOffScreen =
93+
rect.top < margin ||
94+
rect.bottom > viewportHeight - margin ||
95+
rect.left < 0 ||
96+
rect.right > window.innerWidth
97+
98+
if (isSignificantlyOffScreen) {
99+
// Use a gentler scroll that doesn't center aggressively
100+
targetElement.scrollIntoView({
101+
behavior: 'smooth',
102+
block: 'nearest',
103+
inline: 'nearest',
104+
})
105+
}
106+
}
107+
}
78108

79-
// Update position on scroll/resize
80-
const handleScroll = () => updatePosition()
81-
window.addEventListener('scroll', handleScroll)
82-
window.addEventListener('resize', handleScroll)
109+
// Add a small delay to ensure DOM is ready
110+
setTimeout(() => {
111+
updatePosition()
112+
}, 100)
113+
114+
// Update position on scroll/resize with throttling for better performance
115+
let throttleTimer: NodeJS.Timeout | null = null
116+
const handleScroll = () => {
117+
if (throttleTimer) return
118+
throttleTimer = setTimeout(() => {
119+
updatePosition()
120+
throttleTimer = null
121+
}, 16) // ~60fps
122+
}
123+
124+
const handleResize = () => {
125+
if (throttleTimer) return
126+
throttleTimer = setTimeout(() => {
127+
updatePosition()
128+
throttleTimer = null
129+
}, 16) // ~60fps
130+
}
131+
132+
window.addEventListener('scroll', handleScroll, { passive: true })
133+
window.addEventListener('resize', handleResize, { passive: true })
83134

84135
return () => {
85136
window.removeEventListener('scroll', handleScroll)
86-
window.removeEventListener('resize', handleScroll)
137+
window.removeEventListener('resize', handleResize)
138+
if (throttleTimer) {
139+
clearTimeout(throttleTimer)
140+
}
87141
}
88142
} else {
89143
// Reset target position when tour is not active
@@ -132,20 +186,30 @@ export const TourManager: React.FC = () => {
132186
skipTour,
133187
])
134188

135-
// Calculate tooltip position
189+
// Calculate tooltip position with responsive design
136190
const getTooltipPosition = () => {
137-
if (!targetPosition || !currentStep) return { top: 0, left: 0 }
191+
if (!targetPosition || !currentStep) return { top: 0, left: 0, width: 320 }
138192

139193
const placement = currentStep.placement || 'bottom'
140194
const offset = currentStep.offset || { x: 0, y: 0 }
141-
const tooltipWidth = 320
142-
const tooltipHeight = 200 // Approximate height
143-
const gap = 20
195+
const isMobile = window.innerWidth <= 768
196+
const gap = isMobile ? 12 : 20
197+
198+
// Responsive tooltip dimensions
199+
const tooltipWidth = isMobile ? Math.min(320, window.innerWidth - 40) : 320
200+
const tooltipHeight = isMobile ? 180 : 200 // Slightly smaller on mobile
201+
const padding = isMobile ? 12 : 20
144202

145203
let top = 0
146204
let left = 0
147205

148-
switch (placement) {
206+
// Only modify placement on very small screens (mobile)
207+
const effectivePlacement =
208+
isMobile && window.innerWidth <= 480 && placement === 'left'
209+
? 'bottom'
210+
: placement
211+
212+
switch (effectivePlacement) {
149213
case 'top':
150214
top = targetPosition.top - tooltipHeight - gap
151215
left = targetPosition.left + targetPosition.width / 2 - tooltipWidth / 2
@@ -164,18 +228,28 @@ export const TourManager: React.FC = () => {
164228
break
165229
}
166230

167-
// Ensure tooltip stays within viewport
168-
const padding = 20
169-
top = Math.max(
170-
padding,
171-
Math.min(top, window.innerHeight - tooltipHeight - padding),
172-
)
173-
left = Math.max(
174-
padding,
175-
Math.min(left, window.innerWidth - tooltipWidth - padding),
176-
)
177-
178-
return { top: top + offset.y, left: left + offset.x }
231+
// Ensure tooltip stays within viewport - be more conservative on desktop
232+
const viewportHeight = window.innerHeight
233+
const viewportWidth = window.innerWidth
234+
235+
// Adjust if tooltip goes off-screen
236+
if (top < padding) {
237+
top = padding
238+
} else if (top + tooltipHeight > viewportHeight - padding) {
239+
top = viewportHeight - tooltipHeight - padding
240+
}
241+
242+
if (left < padding) {
243+
left = padding
244+
} else if (left + tooltipWidth > viewportWidth - padding) {
245+
left = viewportWidth - tooltipWidth - padding
246+
}
247+
248+
return {
249+
top: top + offset.y,
250+
left: left + offset.x,
251+
width: tooltipWidth,
252+
}
179253
}
180254

181255
const handleNext = () => {
@@ -236,21 +310,22 @@ export const TourManager: React.FC = () => {
236310
animate={{ opacity: 1, y: 0 }}
237311
exit={{ opacity: 0, y: 20 }}
238312
transition={{ duration: 0.3, delay: 0.1 }}
239-
className="absolute z-[10000] w-80 rounded-lg bg-background-1 shadow-xl"
313+
className="absolute z-[10000] rounded-lg bg-background-1 shadow-xl"
240314
style={{
241315
top: tooltipPosition.top,
242316
left: tooltipPosition.left,
317+
width: tooltipPosition.width,
243318
}}
244319
>
245320
{/* Header */}
246-
<div className="flex items-center justify-between border-b border-white/10 p-4">
247-
<div className="flex items-center gap-3">
248-
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-human-4">
249-
<span className="text-sm font-bold text-white">
321+
<div className="flex items-center justify-between border-b border-white/10 p-3 sm:p-4">
322+
<div className="flex items-center gap-2 sm:gap-3">
323+
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-human-4 sm:h-8 sm:w-8">
324+
<span className="text-xs font-bold text-white sm:text-sm">
250325
{tourState.currentStep + 1}
251326
</span>
252327
</div>
253-
<h3 className="text-lg font-semibold text-primary">
328+
<h3 className="text-base font-semibold text-primary sm:text-lg">
254329
{currentStep.title}
255330
</h3>
256331
</div>
@@ -259,24 +334,26 @@ export const TourManager: React.FC = () => {
259334
className="text-secondary hover:text-primary"
260335
title="Skip tour"
261336
>
262-
<span className="material-symbols-outlined">close</span>
337+
<span className="material-symbols-outlined text-lg sm:text-xl">
338+
close
339+
</span>
263340
</button>
264341
</div>
265342

266343
{/* Content */}
267-
<div className="p-4">
268-
<p className="text-sm leading-relaxed text-secondary">
344+
<div className="p-3 sm:p-4">
345+
<p className="text-xs leading-relaxed text-secondary sm:text-sm">
269346
{currentStep.description}
270347
</p>
271348
</div>
272349

273350
{/* Footer */}
274-
<div className="flex items-center justify-between border-t border-white/10 p-4">
351+
<div className="flex items-center justify-between border-t border-white/10 p-3 sm:p-4">
275352
<div className="flex items-center gap-1">
276353
{tourState.steps.map((_, index) => (
277354
<div
278355
key={index}
279-
className={`h-2 w-2 rounded-full ${
356+
className={`h-1.5 w-1.5 rounded-full sm:h-2 sm:w-2 ${
280357
index === tourState.currentStep
281358
? 'bg-human-4'
282359
: index < tourState.currentStep
@@ -287,27 +364,27 @@ export const TourManager: React.FC = () => {
287364
))}
288365
</div>
289366

290-
<div className="flex items-center gap-2">
367+
<div className="flex items-center gap-1 sm:gap-2">
291368
<button
292369
onClick={handlePrev}
293370
disabled={tourState.currentStep === 0}
294-
className="flex items-center gap-1 rounded bg-background-2 px-3 py-1 text-sm text-secondary transition-colors hover:bg-background-3 disabled:cursor-not-allowed disabled:opacity-50"
371+
className="flex items-center gap-1 rounded bg-background-2 px-2 py-1 text-xs text-secondary transition-colors hover:bg-background-3 disabled:cursor-not-allowed disabled:opacity-50 sm:px-3 sm:text-sm"
295372
>
296-
<span className="material-symbols-outlined text-sm">
373+
<span className="material-symbols-outlined text-xs sm:text-sm">
297374
arrow_back
298375
</span>
299-
Previous
376+
<span className="hidden sm:inline">Previous</span>
300377
</button>
301378

302379
<button
303380
onClick={handleNext}
304-
className="flex items-center gap-1 rounded bg-human-4 px-3 py-1 text-sm text-white transition-colors hover:bg-human-4/80"
381+
className="flex items-center gap-1 rounded bg-human-4 px-2 py-1 text-xs text-white transition-colors hover:bg-human-4/80 sm:px-3 sm:text-sm"
305382
>
306383
{tourState.currentStep === tourState.steps.length - 1
307384
? 'Finish'
308385
: 'Next'}
309386
{tourState.currentStep < tourState.steps.length - 1 && (
310-
<span className="material-symbols-outlined text-sm">
387+
<span className="material-symbols-outlined text-xs sm:text-sm">
311388
arrow_forward
312389
</span>
313390
)}
@@ -316,9 +393,12 @@ export const TourManager: React.FC = () => {
316393
</div>
317394

318395
{/* Keyboard hints */}
319-
<div className="border-t border-white/10 px-4 py-2">
396+
<div className="border-t border-white/10 px-3 py-2 sm:px-4">
320397
<p className="text-xs text-secondary">
321-
Use arrow keys to navigate • Press Escape to skip
398+
<span className="hidden sm:inline">
399+
Use arrow keys to navigate • Press Escape to skip
400+
</span>
401+
<span className="sm:hidden">Tap to navigate • Tap X to skip</span>
322402
</p>
323403
</div>
324404
</motion.div>

src/contexts/TourContext/TourContext.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import React, {
66
useMemo,
77
memo,
88
} from 'react'
9-
import { tourConfigs } from 'src/config/tours'
109

1110
export interface TourStep {
1211
id: string
@@ -82,12 +81,20 @@ export const TourProvider: React.FC<TourProviderProps> = memo(
8281
}
8382

8483
setTourState((prevState) => {
85-
const completedTours = JSON.parse(
86-
localStorage.getItem('maia-completed-tours') || '[]',
87-
)
84+
// Use state first, then localStorage as fallback
85+
const stateCompletedTours = prevState.completedTours
86+
const localStorageCompletedTours =
87+
typeof window !== 'undefined'
88+
? JSON.parse(localStorage.getItem('maia-completed-tours') || '[]')
89+
: []
90+
91+
// Merge state and localStorage, removing duplicates
92+
const allCompletedTours = [
93+
...new Set([...stateCompletedTours, ...localStorageCompletedTours]),
94+
]
8895

8996
// If not forcing restart and tour is completed, don't start
90-
if (!forceRestart && completedTours.includes(tourId)) {
97+
if (!forceRestart && allCompletedTours.includes(tourId)) {
9198
return prevState
9299
}
93100

@@ -104,7 +111,6 @@ export const TourProvider: React.FC<TourProviderProps> = memo(
104111
) {
105112
return prevState
106113
}
107-
108114
isStartingTour.current = true
109115
// Reset the flag after a brief delay to allow the state to update
110116
setTimeout(() => {
@@ -115,7 +121,7 @@ export const TourProvider: React.FC<TourProviderProps> = memo(
115121
isActive: true,
116122
currentStep: 0,
117123
steps,
118-
completedTours: prevState.completedTours, // Use existing state, don't overwrite from localStorage
124+
completedTours: allCompletedTours, // Use merged completed tours
119125
currentTourId: tourId,
120126
}
121127
})
@@ -151,7 +157,12 @@ export const TourProvider: React.FC<TourProviderProps> = memo(
151157
setTourState((prev) => {
152158
// Mark tour as completed - use the properly tracked tour ID
153159
const currentTourId = prev.currentTourId || 'analysis' // fallback for safety
154-
const completedTours = [...prev.completedTours, currentTourId]
160+
161+
// Only add to completedTours if not already present
162+
const completedTours = prev.completedTours.includes(currentTourId)
163+
? prev.completedTours
164+
: [...prev.completedTours, currentTourId]
165+
155166
if (typeof window !== 'undefined') {
156167
localStorage.setItem(
157168
'maia-completed-tours',
@@ -174,7 +185,12 @@ export const TourProvider: React.FC<TourProviderProps> = memo(
174185
setTourState((prev) => {
175186
// Mark tour as completed when skipped - use the properly tracked tour ID
176187
const currentTourId = prev.currentTourId || 'analysis' // fallback for safety
177-
const completedTours = [...prev.completedTours, currentTourId]
188+
189+
// Only add to completedTours if not already present
190+
const completedTours = prev.completedTours.includes(currentTourId)
191+
? prev.completedTours
192+
: [...prev.completedTours, currentTourId]
193+
178194
if (typeof window !== 'undefined') {
179195
localStorage.setItem(
180196
'maia-completed-tours',

src/pages/analysis/[...id].tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,10 @@ const AnalysisPage: NextPage = () => {
8484
useEffect(() => {
8585
if (!openedModals.analysis && !initialTourCheck) {
8686
setInitialTourCheck(true)
87-
// Check if user has completed the tour on initial load only
88-
const completedTours =
89-
typeof window !== 'undefined'
90-
? JSON.parse(localStorage.getItem('maia-completed-tours') || '[]')
91-
: []
92-
93-
if (!completedTours.includes('analysis')) {
94-
startTour(tourConfigs.analysis.id, tourConfigs.analysis.steps, false)
95-
}
87+
// Always attempt to start the tour - the tour context will handle completion checking
88+
startTour(tourConfigs.analysis.id, tourConfigs.analysis.steps, false)
9689
}
97-
}, [openedModals.analysis, initialTourCheck])
90+
}, [openedModals.analysis, initialTourCheck, startTour])
9891
const [currentId, setCurrentId] = useState<string[]>(id as string[])
9992

10093
const getAndSetTournamentGame = useCallback(

0 commit comments

Comments
 (0)