fix: improve web auth refresh and mobile tutorials
This commit is contained in:
@@ -200,6 +200,7 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
const coinsAreaRef = useRef<HTMLDivElement>(null);
|
||||
const timePanelRef = useRef<HTMLElement>(null);
|
||||
const yaoPanelRef = useRef<HTMLElement>(null);
|
||||
const yaoRowsRef = useRef<HTMLDivElement>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [spotlightRect, setSpotlightRect] = useState<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
@@ -253,12 +254,17 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRef = [coinsAreaRef, timePanelRef, yaoPanelRef, submitBtnRef][guideStep];
|
||||
const mobileGuideTargets = [coinsAreaRef, timePanelRef, yaoRowsRef, submitBtnRef];
|
||||
const desktopGuideTargets = [coinsAreaRef, timePanelRef, yaoPanelRef, submitBtnRef];
|
||||
const targetRef = (isMobile ? mobileGuideTargets : desktopGuideTargets)[guideStep];
|
||||
if (!targetRef?.current) return;
|
||||
|
||||
const tooltipWidth = 320;
|
||||
const tooltipHeight = 180;
|
||||
const gap = 16;
|
||||
const tooltipHeight = isMobile ? 220 : 180;
|
||||
const gap = isMobile ? 24 : 16;
|
||||
|
||||
const overlayHost = scrollContainerRef.current;
|
||||
if (!overlayHost) return;
|
||||
|
||||
const scrollContainer = scrollContainerRef.current?.parentElement?.parentElement as HTMLElement | null;
|
||||
if (!scrollContainer) return;
|
||||
@@ -266,10 +272,9 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
const isInitialOpen = prevGuideStepRef.current === null;
|
||||
|
||||
if (isMobile) {
|
||||
const containerRect = scrollContainer.getBoundingClientRect();
|
||||
const elementRect = targetRef.current.getBoundingClientRect();
|
||||
|
||||
const elementLeft = elementRect.left - containerRect.left;
|
||||
const containerRect = scrollContainer.getBoundingClientRect();
|
||||
const elementTop = elementRect.top - containerRect.top + scrollContainer.scrollTop;
|
||||
const elementWidth = elementRect.width;
|
||||
const elementHeight = elementRect.height;
|
||||
@@ -278,22 +283,37 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
scrollContainer.scrollTop = 0;
|
||||
}
|
||||
|
||||
const scrollTopNeeded = Math.max(0, elementTop - 20);
|
||||
scrollContainer.scrollTo({ top: scrollTopNeeded, behavior: 'smooth' });
|
||||
const scrollTopNeeded = Math.max(
|
||||
0,
|
||||
guideStep === 3 ? elementTop - tooltipHeight - gap - 32 : elementTop - 20,
|
||||
);
|
||||
scrollContainer.scrollTo({ top: scrollTopNeeded, behavior: 'auto' });
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!targetRef.current) return;
|
||||
const newElementRect = targetRef.current.getBoundingClientRect();
|
||||
const newContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const hostRect = overlayHost.getBoundingClientRect();
|
||||
const visibleContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const visibleTop = visibleContainerRect.top - hostRect.top;
|
||||
const visibleBottom = visibleTop + visibleContainerRect.height;
|
||||
|
||||
const spotlightLeft = newElementRect.left - newContainerRect.left;
|
||||
const spotlightTop = newElementRect.top - newContainerRect.top;
|
||||
const spotlightLeft = newElementRect.left - hostRect.left;
|
||||
const spotlightTop = newElementRect.top - hostRect.top;
|
||||
|
||||
const tooltipLeft = Math.max(16, Math.min(
|
||||
(newElementRect.left + newElementRect.right - tooltipWidth) / 2 - newContainerRect.left,
|
||||
containerRect.width - tooltipWidth - 16
|
||||
(newElementRect.left + newElementRect.right - tooltipWidth) / 2 - hostRect.left,
|
||||
hostRect.width - tooltipWidth - 16
|
||||
));
|
||||
const tooltipTop = spotlightTop + elementHeight + gap;
|
||||
let tooltipTop = spotlightTop + elementHeight + gap;
|
||||
let side: 'bottom' | 'top' = 'bottom';
|
||||
if (guideStep === 3) {
|
||||
tooltipTop = Math.max(visibleTop + 16, spotlightTop - tooltipHeight - gap);
|
||||
side = 'top';
|
||||
}
|
||||
if (tooltipTop + tooltipHeight > visibleBottom) {
|
||||
tooltipTop = Math.max(visibleTop + 16, spotlightTop - tooltipHeight - gap);
|
||||
side = 'top';
|
||||
}
|
||||
|
||||
setSpotlightRect({
|
||||
left: spotlightLeft,
|
||||
@@ -302,7 +322,7 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
height: elementHeight
|
||||
});
|
||||
setTooltipPos({ left: tooltipLeft, top: tooltipTop });
|
||||
setTooltipSide('bottom');
|
||||
setTooltipSide(side);
|
||||
});
|
||||
|
||||
prevGuideStepRef.current = guideStep;
|
||||
@@ -495,7 +515,7 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Six yao rows */}
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div ref={yaoRowsRef} className="flex flex-col gap-2.5">
|
||||
{[5, 4, 3, 2, 1, 0].map((index) => {
|
||||
const result = yaoResults[index];
|
||||
const isBeingShaken = isShaking && currentShakingYao === index;
|
||||
@@ -597,11 +617,11 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
{guideOpen && guide && spotlightRect && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/70 md:block hidden"
|
||||
className="fixed inset-0 z-40 hidden bg-black/70 xl:block"
|
||||
onClick={() => closeGuide()}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 z-40 bg-black/70 md:hidden"
|
||||
className="absolute inset-0 z-40 xl:hidden"
|
||||
style={{ top: 0, height: '100vh' }}
|
||||
onClick={() => closeGuide()}
|
||||
/>
|
||||
@@ -631,7 +651,7 @@ export default function AutoDivinationPage({ locale, divination: d }: Props) {
|
||||
className={`absolute h-3 w-3 rotate-45 bg-slate-950 ${
|
||||
tooltipSide === 'right' ? '-left-1.5 top-6' :
|
||||
tooltipSide === 'left' ? '-right-1.5 top-6' :
|
||||
tooltipSide === 'top' ? '-bottom-1.5 left-6' :
|
||||
tooltipSide === 'top' ? '-bottom-1.5 left-1/2 -translate-x-1/2' :
|
||||
'-top-1.5 left-6'
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -209,6 +209,7 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
const coinsAreaRef = useRef<HTMLDivElement>(null);
|
||||
const timePanelRef = useRef<HTMLElement>(null);
|
||||
const yaoPanelRef = useRef<HTMLElement>(null);
|
||||
const yaoRowsRef = useRef<HTMLDivElement>(null);
|
||||
const submitBtnRef = useRef<HTMLButtonElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [spotlightRect, setSpotlightRect] = useState<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
@@ -269,12 +270,17 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRef = [coinsAreaRef, timePanelRef, yaoPanelRef, submitBtnRef][guideStep];
|
||||
const mobileGuideTargets = [coinsAreaRef, timePanelRef, yaoRowsRef, submitBtnRef];
|
||||
const desktopGuideTargets = [coinsAreaRef, timePanelRef, yaoPanelRef, submitBtnRef];
|
||||
const targetRef = (isMobile ? mobileGuideTargets : desktopGuideTargets)[guideStep];
|
||||
if (!targetRef?.current) return;
|
||||
|
||||
const tooltipWidth = 320;
|
||||
const tooltipHeight = 180;
|
||||
const gap = 16;
|
||||
const tooltipHeight = isMobile ? 220 : 180;
|
||||
const gap = isMobile ? 24 : 16;
|
||||
|
||||
const overlayHost = scrollContainerRef.current;
|
||||
if (!overlayHost) return;
|
||||
|
||||
// Get scroll container - it's the main element inside AppShell
|
||||
const scrollContainer = scrollContainerRef.current?.parentElement?.parentElement as HTMLElement | null;
|
||||
@@ -284,12 +290,10 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
|
||||
// ===== MOBILE: Absolute positioning relative to scroll container =====
|
||||
if (isMobile) {
|
||||
// Calculate element's offset relative to scroll container
|
||||
const containerRect = scrollContainer.getBoundingClientRect();
|
||||
const elementRect = targetRef.current.getBoundingClientRect();
|
||||
|
||||
// Element position relative to scroll container (accounts for current scroll)
|
||||
const elementLeft = elementRect.left - containerRect.left;
|
||||
const containerRect = scrollContainer.getBoundingClientRect();
|
||||
const elementTop = elementRect.top - containerRect.top + scrollContainer.scrollTop;
|
||||
const elementWidth = elementRect.width;
|
||||
const elementHeight = elementRect.height;
|
||||
@@ -299,30 +303,42 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
scrollContainer.scrollTop = 0;
|
||||
}
|
||||
|
||||
// Calculate where we need to scroll to make both spotlight and tooltip visible
|
||||
// Spotlight should be at top portion, tooltip below it
|
||||
const totalHeight = elementHeight + gap + tooltipHeight;
|
||||
const scrollTopNeeded = Math.max(0, elementTop - 20); // 20px margin above spotlight
|
||||
// Calculate where we need to scroll to make the spotlight visible.
|
||||
const scrollTopNeeded = Math.max(
|
||||
0,
|
||||
guideStep === 3 ? elementTop - tooltipHeight - gap - 32 : elementTop - 20,
|
||||
);
|
||||
|
||||
// Smooth scroll to position
|
||||
scrollContainer.scrollTo({ top: scrollTopNeeded, behavior: 'smooth' });
|
||||
scrollContainer.scrollTo({ top: scrollTopNeeded, behavior: 'auto' });
|
||||
|
||||
// Use requestAnimationFrame to ensure scroll has started before calculating final position
|
||||
// Use requestAnimationFrame to calculate after the scroll position has updated.
|
||||
requestAnimationFrame(() => {
|
||||
if (!targetRef.current) return;
|
||||
// Recalculate element position after scroll setup
|
||||
const newElementRect = targetRef.current!.getBoundingClientRect();
|
||||
const newContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const newElementRect = targetRef.current.getBoundingClientRect();
|
||||
const hostRect = overlayHost.getBoundingClientRect();
|
||||
const visibleContainerRect = scrollContainer.getBoundingClientRect();
|
||||
const visibleTop = visibleContainerRect.top - hostRect.top;
|
||||
const visibleBottom = visibleTop + visibleContainerRect.height;
|
||||
|
||||
// Position relative to container (for absolute positioning)
|
||||
const spotlightLeft = newElementRect.left - newContainerRect.left;
|
||||
const spotlightTop = newElementRect.top - newContainerRect.top;
|
||||
// Position relative to this component because the mobile overlay is absolute.
|
||||
const spotlightLeft = newElementRect.left - hostRect.left;
|
||||
const spotlightTop = newElementRect.top - hostRect.top;
|
||||
|
||||
// Tooltip goes below the element
|
||||
const tooltipLeft = Math.max(16, Math.min(
|
||||
(newElementRect.left + newElementRect.right - tooltipWidth) / 2 - newContainerRect.left,
|
||||
containerRect.width - tooltipWidth - 16
|
||||
(newElementRect.left + newElementRect.right - tooltipWidth) / 2 - hostRect.left,
|
||||
hostRect.width - tooltipWidth - 16
|
||||
));
|
||||
const tooltipTop = spotlightTop + elementHeight + gap;
|
||||
let tooltipTop = spotlightTop + elementHeight + gap;
|
||||
let side: 'bottom' | 'top' = 'bottom';
|
||||
if (guideStep === 3) {
|
||||
tooltipTop = Math.max(visibleTop + 16, spotlightTop - tooltipHeight - gap);
|
||||
side = 'top';
|
||||
}
|
||||
if (tooltipTop + tooltipHeight > visibleBottom) {
|
||||
tooltipTop = Math.max(visibleTop + 16, spotlightTop - tooltipHeight - gap);
|
||||
side = 'top';
|
||||
}
|
||||
|
||||
setSpotlightRect({
|
||||
left: spotlightLeft,
|
||||
@@ -331,7 +347,7 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
height: elementHeight
|
||||
});
|
||||
setTooltipPos({ left: tooltipLeft, top: tooltipTop });
|
||||
setTooltipSide('bottom');
|
||||
setTooltipSide(side);
|
||||
});
|
||||
|
||||
prevGuideStepRef.current = guideStep;
|
||||
@@ -513,7 +529,7 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
<span className="text-[13px] font-bold text-violet-700">{progress} / {TOTAL_YAO_COUNT}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div ref={yaoRowsRef} className="flex flex-col gap-2.5">
|
||||
{[5, 4, 3, 2, 1, 0].map((index) => {
|
||||
const result = yaoResults[index];
|
||||
// Only show "active" highlight when not in editing mode
|
||||
@@ -598,12 +614,12 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
<>
|
||||
{/* Dark overlay - fixed for desktop, covers viewport */}
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-black/70 md:block hidden"
|
||||
className="fixed inset-0 z-40 hidden bg-black/70 xl:block"
|
||||
onClick={() => closeGuide()}
|
||||
/>
|
||||
{/* Mobile dark overlay - positioned within scroll container */}
|
||||
<div
|
||||
className="absolute inset-0 z-40 bg-black/70 md:hidden"
|
||||
className="absolute inset-0 z-40 xl:hidden"
|
||||
style={{ top: 0, height: '100vh' }}
|
||||
onClick={() => closeGuide()}
|
||||
/>
|
||||
@@ -636,7 +652,7 @@ export default function ManualDivinationPage({ locale, divination: d }: Props) {
|
||||
className={`absolute h-3 w-3 rotate-45 bg-slate-950 ${
|
||||
tooltipSide === 'right' ? '-left-1.5 top-6' :
|
||||
tooltipSide === 'left' ? '-right-1.5 top-6' :
|
||||
tooltipSide === 'top' ? '-bottom-1.5 left-6' :
|
||||
tooltipSide === 'top' ? '-bottom-1.5 left-1/2 -translate-x-1/2' :
|
||||
'-top-1.5 left-6'
|
||||
}`}
|
||||
/>
|
||||
|
||||
+14
-1
@@ -89,6 +89,8 @@ export function isTokenExpired(): boolean {
|
||||
return auth.expires_at - 60_000 < Date.now();
|
||||
}
|
||||
|
||||
let refreshPromise: Promise<AuthData> | null = null;
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function toAuthData(response: SessionResponse): AuthData {
|
||||
@@ -144,7 +146,7 @@ export async function loginWithEmail(
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(): Promise<AuthData> {
|
||||
async function doRefreshAccessToken(): Promise<AuthData> {
|
||||
const auth = getAuth();
|
||||
if (!auth?.refresh_token) {
|
||||
clearAuth();
|
||||
@@ -165,6 +167,17 @@ export async function refreshAccessToken(): Promise<AuthData> {
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(): Promise<AuthData> {
|
||||
if (refreshPromise) return refreshPromise;
|
||||
|
||||
refreshPromise = doRefreshAccessToken();
|
||||
try {
|
||||
return await refreshPromise;
|
||||
} finally {
|
||||
refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
const auth = getAuth();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user