fix: improve web auth refresh and mobile tutorials

This commit is contained in:
ZL-Q
2026-05-10 15:22:08 +08:00
parent 3f0942329d
commit 627454971c
8 changed files with 183 additions and 46 deletions
+38 -18
View File
@@ -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'
}`}
/>
+43 -27
View File
@@ -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
View File
@@ -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 {