Compare commits
6 Commits
7b95998b05
...
a54a74d7de
| Author | SHA1 | Date | |
|---|---|---|---|
| a54a74d7de | |||
|
|
236fcda671 | ||
|
|
9ed054e5d0 | ||
|
|
63853f22de | ||
|
|
5dd41d8022 | ||
|
|
937758bd49 |
3
bun.lock
3
bun.lock
@@ -30,6 +30,7 @@
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dotenv": "^17.2.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"facehash": "^0.0.7",
|
||||
"framer-motion": "^12.23.12",
|
||||
"ioredis": "^5.7.0",
|
||||
"pg": "^8.16.3",
|
||||
@@ -710,6 +711,8 @@
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"facehash": ["facehash@0.0.7", "", { "peerDependencies": { "@types/react": "", "next": ">=15", "react": ">=18 <20", "react-dom": ">=18 <20" }, "optionalPeers": ["@types/react", "next"] }, "sha512-P4fw6z5DIGMbjtqEaOw7fYvYpQetSOSJOfqy3xuET7cDUI6f9CKlSX0UZIYNrtsPpCoz3LoPP5E8bNbpZBP30A=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="],
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dotenv": "^17.2.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"facehash": "^0.0.7",
|
||||
"framer-motion": "^12.23.12",
|
||||
"ioredis": "^5.7.0",
|
||||
"pg": "^8.16.3",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { type QueryClient } from "@tanstack/react-query";
|
||||
import { ensureSuperTokensFrontend } from "@/lib/supertokens/client";
|
||||
import { AuthContextType } from "@/contexts/auth-context";
|
||||
import Providers from "@/features/core/components/providers";
|
||||
import { SessionMonitor } from "@/components/session-monitor";
|
||||
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
|
||||
import { HeaderConfig } from "@/features/core/types/header-config";
|
||||
import { playerQueries } from "@/features/players/queries";
|
||||
@@ -126,6 +127,7 @@ function RootComponent() {
|
||||
return (
|
||||
<RootDocument>
|
||||
<Providers>
|
||||
<SessionMonitor />
|
||||
<Outlet />
|
||||
</Providers>
|
||||
</RootDocument>
|
||||
|
||||
@@ -86,6 +86,7 @@ function RouteComponent() {
|
||||
<SeedTournament
|
||||
tournamentId={tournament.id}
|
||||
teams={tournament.teams || []}
|
||||
isRegional={tournament.regional}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import FullScreenLoader from '@/components/full-screen-loader'
|
||||
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
|
||||
import { resetRefreshFlag } from '@/lib/supertokens/client'
|
||||
import { resetRefreshFlag, getOrCreateRefreshPromise } from '@/lib/supertokens/client'
|
||||
import { logger } from '@/lib/supertokens'
|
||||
|
||||
export const Route = createFileRoute('/refresh-session')({
|
||||
component: RouteComponent,
|
||||
@@ -17,23 +18,31 @@ function RouteComponent() {
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
resetRefreshFlag();
|
||||
const refreshed = await attemptRefreshingSession()
|
||||
logger.info("Refresh session route: starting refresh");
|
||||
|
||||
const refreshed = await getOrCreateRefreshPromise(async () => {
|
||||
return await attemptRefreshingSession();
|
||||
});
|
||||
|
||||
if (refreshed) {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const redirect = urlParams.get('redirect')
|
||||
logger.info("Refresh session route: refresh successful");
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const redirect = urlParams.get('redirect');
|
||||
|
||||
if (redirect && !redirect.includes('_serverFn') && !redirect.includes('/api/')) {
|
||||
window.location.href = decodeURIComponent(redirect)
|
||||
logger.info("Refresh session route: redirecting to", redirect);
|
||||
window.location.href = decodeURIComponent(redirect);
|
||||
} else {
|
||||
logger.info("Refresh session route: redirecting to home");
|
||||
window.location.href = '/';
|
||||
}
|
||||
} else {
|
||||
window.location.href = '/login'
|
||||
logger.warn("Refresh session route: refresh failed, redirecting to login");
|
||||
window.location.href = '/login';
|
||||
}
|
||||
} catch (error) {
|
||||
window.location.href = '/login'
|
||||
logger.error("Refresh session route: error during refresh", error);
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
85
src/components/player-avatar.tsx
Normal file
85
src/components/player-avatar.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Paper, useMantineTheme } from "@mantine/core";
|
||||
import { Facehash } from "facehash";
|
||||
|
||||
interface PlayerAvatarProps {
|
||||
name?: string;
|
||||
size?: number;
|
||||
disableFullscreen?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const PlayerAvatar = ({
|
||||
name = "",
|
||||
size = 35,
|
||||
disableFullscreen = false,
|
||||
style,
|
||||
}: PlayerAvatarProps) => {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const getFacehashSize = (size: number): 32 | 48 | 64 | 80 => {
|
||||
if (size <= 40) return 32;
|
||||
if (size <= 56) return 48;
|
||||
if (size <= 72) return 64;
|
||||
return 80;
|
||||
};
|
||||
|
||||
const facehashSize = getFacehashSize(size);
|
||||
|
||||
const colors = [
|
||||
"hsla(314, 100%, 80%, 1)",
|
||||
"hsla(58, 93%, 72%, 1)",
|
||||
"hsla(218, 92%, 72%, 1)",
|
||||
"hsla(19, 99%, 44%, 1)",
|
||||
"hsla(156, 86%, 40%, 1)",
|
||||
"hsla(314, 100%, 85%, 1)",
|
||||
"hsla(58, 92%, 79%, 1)",
|
||||
"hsla(218, 91%, 78%, 1)",
|
||||
"hsla(19, 99%, 50%, 1)",
|
||||
"hsla(156, 86%, 64%, 1)",
|
||||
];
|
||||
|
||||
return (
|
||||
<Paper
|
||||
p={size / 20}
|
||||
radius="100%"
|
||||
withBorder
|
||||
style={{
|
||||
cursor: !disableFullscreen ? 'pointer' : 'default',
|
||||
transition: 'transform 0.15s ease',
|
||||
...style,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!disableFullscreen) {
|
||||
e.currentTarget.style.transform = 'scale(1.02)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
>
|
||||
<Facehash
|
||||
name={name}
|
||||
size={size}
|
||||
variant="solid"
|
||||
colors={colors}
|
||||
intensity3d="dramatic"
|
||||
enableBlink
|
||||
style={{ borderRadius: '100%', overflow: 'hidden', color: 'black' }}
|
||||
/>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerAvatar;
|
||||
60
src/components/session-monitor.tsx
Normal file
60
src/components/session-monitor.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { doesSessionExist } from 'supertokens-web-js/recipe/session';
|
||||
import { getOrCreateRefreshPromise } from '@/lib/supertokens/client';
|
||||
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session';
|
||||
import { logger } from '@/lib/supertokens';
|
||||
|
||||
export function SessionMonitor() {
|
||||
const lastRefreshTimeRef = useRef<number>(0);
|
||||
const REFRESH_COOLDOWN = 30 * 1000;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const handleVisibilityChange = async () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
|
||||
const publicRoutes = ['/login', '/logout', '/refresh-session'];
|
||||
if (publicRoutes.some(route => window.location.pathname === route)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastRefreshTimeRef.current < REFRESH_COOLDOWN) {
|
||||
logger.info('Session monitor: skipping refresh (cooldown)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionExists = await doesSessionExist();
|
||||
if (!sessionExists) {
|
||||
logger.info('Session monitor: no session exists, skipping refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Session monitor: tab became visible, refreshing session');
|
||||
|
||||
const refreshed = await getOrCreateRefreshPromise(async () => {
|
||||
return await attemptRefreshingSession();
|
||||
});
|
||||
|
||||
if (refreshed) {
|
||||
lastRefreshTimeRef.current = Date.now();
|
||||
logger.info('Session monitor: session refreshed successfully');
|
||||
} else {
|
||||
logger.warn('Session monitor: refresh returned false');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Session monitor: error refreshing session', error);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
139
src/components/team-avatar.tsx
Normal file
139
src/components/team-avatar.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Box, AvatarGroup } from "@mantine/core";
|
||||
import { CrownIcon } from "@phosphor-icons/react";
|
||||
import { TeamInfo } from "@/features/teams/types";
|
||||
import Avatar from "./avatar";
|
||||
import PlayerAvatar from "./player-avatar";
|
||||
|
||||
interface TeamAvatarProps {
|
||||
team: TeamInfo;
|
||||
size?: number;
|
||||
radius?: string | number;
|
||||
withBorder?: boolean;
|
||||
disableFullscreen?: boolean;
|
||||
contain?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
winner?: boolean;
|
||||
isRegional?: boolean;
|
||||
}
|
||||
|
||||
const TeamAvatar = ({
|
||||
team,
|
||||
size = 35,
|
||||
radius = "sm",
|
||||
withBorder = true,
|
||||
disableFullscreen = false,
|
||||
contain = false,
|
||||
style,
|
||||
winner = false,
|
||||
isRegional,
|
||||
}: TeamAvatarProps) => {
|
||||
const hasNoLogo = !team.logo;
|
||||
const hasTwoPlayers = team.players?.length === 2;
|
||||
|
||||
let shouldShowPlayerAvatars = false;
|
||||
|
||||
if (isRegional !== undefined) {
|
||||
shouldShowPlayerAvatars = isRegional && hasTwoPlayers && hasNoLogo;
|
||||
} else {
|
||||
const tournaments = (team as any).tournaments;
|
||||
const hasTournaments = tournaments && tournaments.length > 0;
|
||||
const allTournamentsAreRegional = hasTournaments && tournaments.every((t: any) => t.regional === true);
|
||||
|
||||
shouldShowPlayerAvatars = hasTwoPlayers && hasNoLogo && (allTournamentsAreRegional || !hasTournaments);
|
||||
}
|
||||
|
||||
if (shouldShowPlayerAvatars && team.players?.length === 2) {
|
||||
const playerSize = size * 0.6;
|
||||
const crownSize = Math.max(12, size * 0.35);
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
position: "relative",
|
||||
width: size,
|
||||
height: size,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<AvatarGroup spacing={size * -0.25}>
|
||||
<Box style={{ position: "relative" }}>
|
||||
<PlayerAvatar
|
||||
name={`${team.players[0].first_name} ${team.players[0].last_name}`}
|
||||
size={playerSize}
|
||||
disableFullscreen={disableFullscreen}
|
||||
/>
|
||||
{winner && (
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -crownSize * 1.1,
|
||||
left: "45%",
|
||||
transform: "translateX(-50%)",
|
||||
color: "gold",
|
||||
rotate: "-5deg",
|
||||
}}
|
||||
>
|
||||
<CrownIcon size={crownSize} weight="fill" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box style={{ position: "relative" }}>
|
||||
<PlayerAvatar
|
||||
name={`${team.players[1].first_name} ${team.players[1].last_name}`}
|
||||
size={playerSize}
|
||||
disableFullscreen={disableFullscreen}
|
||||
/>
|
||||
{winner && (
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -crownSize * 0.95,
|
||||
left: "65%",
|
||||
transform: "translateX(-50%)",
|
||||
color: "gold",
|
||||
rotate: "10deg",
|
||||
}}
|
||||
>
|
||||
<CrownIcon size={crownSize} weight="fill" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</AvatarGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const crownSize = Math.max(14, size * 0.4);
|
||||
|
||||
return (
|
||||
<Box style={{ position: "relative", ...style }}>
|
||||
<Avatar
|
||||
name={team.name}
|
||||
size={size}
|
||||
radius={radius}
|
||||
withBorder={withBorder}
|
||||
disableFullscreen={disableFullscreen}
|
||||
contain={contain}
|
||||
src={team.logo ? `/api/files/teams/${team.id}/${team.logo}` : undefined}
|
||||
/>
|
||||
{winner && (
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -crownSize * 0.6,
|
||||
left: -crownSize * 0.25,
|
||||
transform: "rotate(-25deg)",
|
||||
color: "gold",
|
||||
}}
|
||||
>
|
||||
<CrownIcon size={crownSize} weight="fill" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamAvatar;
|
||||
@@ -85,6 +85,55 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
|
||||
return () => void ac.abort();
|
||||
}, []);
|
||||
|
||||
// Fix wheel scrolling over child elements
|
||||
useEffect(() => {
|
||||
const scrollWrapper = document.getElementById('scroll-wrapper');
|
||||
if (!scrollWrapper) return;
|
||||
|
||||
const viewport = scrollWrapper.querySelector('.mantine-ScrollArea-viewport') as HTMLElement;
|
||||
if (!viewport) return;
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if the target is inside a nested scrollable container
|
||||
let element = target;
|
||||
while (element && element !== viewport) {
|
||||
const overflow = window.getComputedStyle(element).overflow;
|
||||
const overflowY = window.getComputedStyle(element).overflowY;
|
||||
const overflowX = window.getComputedStyle(element).overflowX;
|
||||
|
||||
// If we found a scrollable ancestor (not the main viewport), don't interfere
|
||||
if (
|
||||
(overflow === 'auto' || overflow === 'scroll' ||
|
||||
overflowY === 'auto' || overflowY === 'scroll' ||
|
||||
overflowX === 'auto' || overflowX === 'scroll') &&
|
||||
element !== viewport
|
||||
) {
|
||||
// Check if this element can actually scroll in the wheel direction
|
||||
const canScrollY = element.scrollHeight > element.clientHeight;
|
||||
const canScrollX = element.scrollWidth > element.clientWidth;
|
||||
|
||||
if ((e.deltaY !== 0 && canScrollY) || (e.deltaX !== 0 && canScrollX)) {
|
||||
return; // Let the nested scroller handle it
|
||||
}
|
||||
}
|
||||
|
||||
element = element.parentElement as HTMLElement;
|
||||
}
|
||||
|
||||
// No nested scroller found, scroll the main viewport
|
||||
viewport.scrollTop += e.deltaY;
|
||||
viewport.scrollLeft += e.deltaX;
|
||||
};
|
||||
|
||||
scrollWrapper.addEventListener('wheel', handleWheel, { passive: true });
|
||||
|
||||
return () => {
|
||||
scrollWrapper.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (scrollAreaRef.current) {
|
||||
@@ -129,8 +178,15 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
|
||||
onScrollPositionChange={onScrollPositionChange}
|
||||
type='never' mah='100%' h='100%'
|
||||
pt={(scrolling || scrollY > 40) || !isRefreshing ? 0 : 40 - scrollY}
|
||||
styles={{
|
||||
viewport: {
|
||||
'& > *': {
|
||||
pointerEvents: 'auto'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box pt='1rem'pb='0.285rem' mih={height} style={{ boxSizing: 'content-box' }}>
|
||||
<Box pt='1rem'pb='0.285rem' mih={height} style={{ boxSizing: 'content-box', pointerEvents: 'auto' }}>
|
||||
{children}
|
||||
</Box>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core";
|
||||
import { CrownIcon, FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Match } from "../types";
|
||||
import Avatar from "@/components/avatar";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
import EmojiBar from "@/features/reactions/components/emoji-bar";
|
||||
import { Suspense } from "react";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
@@ -113,32 +113,16 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="sm" style={{ flex: 1 }}>
|
||||
<Box
|
||||
style={{ position: "relative", cursor: "pointer" }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={handleHomeTeamClick}
|
||||
>
|
||||
<Avatar
|
||||
<TeamAvatar
|
||||
team={match.home!}
|
||||
size={40}
|
||||
name={match.home?.name!}
|
||||
radius="sm"
|
||||
src={
|
||||
match.home?.logo
|
||||
? `/api/files/teams/${match.home?.id}/${match.home?.logo}`
|
||||
: undefined
|
||||
}
|
||||
winner={isHomeWin}
|
||||
isRegional={match.tournament.regional}
|
||||
/>
|
||||
{isHomeWin && (
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -10,
|
||||
left: -4,
|
||||
transform: "rotate(-25deg)",
|
||||
color: "gold",
|
||||
}}
|
||||
>
|
||||
<CrownIcon size={16} weight="fill" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip
|
||||
label={match.home?.name!}
|
||||
@@ -175,32 +159,16 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
||||
<Group justify="space-between" align="center">
|
||||
<Group gap="sm" style={{ flex: 1 }}>
|
||||
<Box
|
||||
style={{ position: "relative", cursor: "pointer" }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={handleAwayTeamClick}
|
||||
>
|
||||
<Avatar
|
||||
<TeamAvatar
|
||||
team={match.away!}
|
||||
size={40}
|
||||
name={match.away?.name!}
|
||||
radius="sm"
|
||||
src={
|
||||
match.away?.logo
|
||||
? `/api/files/teams/${match.away?.id}/${match.away?.logo}`
|
||||
: undefined
|
||||
}
|
||||
winner={isAwayWin}
|
||||
isRegional={match.tournament.regional}
|
||||
/>
|
||||
{isAwayWin && (
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -10,
|
||||
left: -4,
|
||||
transform: "rotate(-25deg)",
|
||||
color: "gold",
|
||||
}}
|
||||
>
|
||||
<CrownIcon size={16} weight="fill" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip
|
||||
label={match.away?.name}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAllPlayerStats } from "../queries";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import PlayerHeadToHeadSheet from "./player-head-to-head-sheet";
|
||||
import Avatar from "@/components/avatar";
|
||||
import PlayerAvatar from "@/components/player-avatar";
|
||||
|
||||
const LeagueHeadToHead = () => {
|
||||
const [player1Id, setPlayer1Id] = useState<string | null>(null);
|
||||
@@ -89,7 +89,7 @@ const LeagueHeadToHead = () => {
|
||||
{player1Id ? (
|
||||
<>
|
||||
<Stack gap={4} align="center" style={{ flex: 1 }}>
|
||||
<Avatar name={player1Name} size={36} />
|
||||
<PlayerAvatar name={player1Name} size={36} disableFullscreen />
|
||||
<Text size="xs" fw={600} ta="center" lineClamp={1}>
|
||||
{player1Name}
|
||||
</Text>
|
||||
@@ -110,7 +110,7 @@ const LeagueHeadToHead = () => {
|
||||
</>
|
||||
) : (
|
||||
<Stack gap={4} align="center">
|
||||
<Avatar size={36} />
|
||||
<PlayerAvatar size={36} disableFullscreen />
|
||||
<Text size="xs" c="dimmed" fw={500}>
|
||||
Player 1
|
||||
</Text>
|
||||
@@ -145,7 +145,7 @@ const LeagueHeadToHead = () => {
|
||||
{player2Id ? (
|
||||
<>
|
||||
<Stack gap={4} align="center" style={{ flex: 1 }}>
|
||||
<Avatar name={player2Name} size={36} />
|
||||
<PlayerAvatar name={player2Name} size={36} disableFullscreen />
|
||||
<Text size="xs" fw={600} ta="center" lineClamp={1}>
|
||||
{player2Name}
|
||||
</Text>
|
||||
@@ -166,7 +166,7 @@ const LeagueHeadToHead = () => {
|
||||
</>
|
||||
) : (
|
||||
<Stack gap={4} align="center">
|
||||
<Avatar size={36} />
|
||||
<PlayerAvatar size={36} disableFullscreen />
|
||||
<Text size="xs" c="dimmed" fw={500}>
|
||||
Player 2
|
||||
</Text>
|
||||
@@ -241,7 +241,7 @@ const LeagueHeadToHead = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Avatar name={player.player_name} size={44} />
|
||||
<PlayerAvatar name={player.player_name} size={44} disableFullscreen />
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={600} truncate>
|
||||
{player.player_name}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { List, ListItem, Skeleton, Text } from "@mantine/core";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import Avatar from "@/components/avatar";
|
||||
import PlayerAvatar from "@/components/player-avatar";
|
||||
import { Player } from "@/features/players/types";
|
||||
import { useCallback } from "react";
|
||||
|
||||
@@ -29,7 +29,7 @@ const PlayerList = ({ players, loading = false }: PlayerListProps) => {
|
||||
{players?.map((player) => (
|
||||
<ListItem key={player.id}
|
||||
py='xs'
|
||||
icon={<Avatar size={40} name={`${player.first_name} ${player.last_name}`} />}
|
||||
icon={<PlayerAvatar size={40} name={`${player.first_name} ${player.last_name}`} disableFullscreen />}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handleClick(player.id)}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
InfoIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { PlayerStats } from "../types";
|
||||
import Avatar from "@/components/avatar";
|
||||
import PlayerAvatar from "@/components/player-avatar";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useAllPlayerStats } from "../queries";
|
||||
|
||||
@@ -93,7 +93,7 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onU
|
||||
}}
|
||||
>
|
||||
<Group p={0} gap="sm" align="center" w="100%" wrap="nowrap" style={{ overflow: 'hidden' }}>
|
||||
<Avatar name={stat.player_name} size={40} style={{ flexShrink: 0 }} />
|
||||
<PlayerAvatar name={stat.player_name} size={40} style={{ flexShrink: 0 }} disableFullscreen />
|
||||
<Stack gap={2} style={{ flexGrow: 1, overflow: 'hidden', minWidth: 0 }}>
|
||||
<Group gap='xs'>
|
||||
<Text size="sm" fw={600}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Flex, Title, ActionIcon, Stack, Button, Box } from "@mantine/core";
|
||||
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
import NameUpdateForm from "./name-form";
|
||||
import Avatar from "@/components/avatar";
|
||||
import PlayerAvatar from "@/components/player-avatar";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { Player } from "../../types";
|
||||
import PlayerHeadToHeadSheet from "../player-head-to-head-sheet";
|
||||
@@ -41,7 +41,7 @@ const Header = ({ player }: HeaderProps) => {
|
||||
<>
|
||||
<Stack gap="sm" align="center" pt="md">
|
||||
<Flex h="15dvh" px='xl' w='100%' align='self-end' gap='md'>
|
||||
<Avatar name={name} size={100} />
|
||||
<PlayerAvatar name={name} size={100} />
|
||||
<Flex align='center' justify='center' gap={4} pb={20} w='100%'>
|
||||
<Title ta='center' style={{ fontSize, lineHeight: 1.2 }}>{name}</Title>
|
||||
<ActionIcon display={owner ? 'block' : 'none'} radius='xl' variant='subtle' onClick={nameSheet.open}>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
||||
import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches, getUnenrolledPlayers, getPlayersActivity } from "./server";
|
||||
import { logger } from '@/lib/supertokens';
|
||||
|
||||
let queryRefreshRedirect: Promise<void> | null = null;
|
||||
|
||||
export const playerKeys = {
|
||||
auth: ['auth'],
|
||||
@@ -54,24 +57,45 @@ export const playerQueries = {
|
||||
|
||||
export const useMe = () => {
|
||||
const { queryKey, queryFn } = playerQueries.auth();
|
||||
return useServerSuspenseQuery({
|
||||
queryKey,
|
||||
return useServerSuspenseQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
options: {
|
||||
staleTime: 0,
|
||||
refetchOnMount: true,
|
||||
staleTime: 30 * 1000,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: true,
|
||||
retry: (failureCount, error: any) => {
|
||||
if (error?.response?.status === 401) {
|
||||
const errorData = error?.response?.data;
|
||||
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
||||
const currentUrl = window.location.pathname + window.location.search;
|
||||
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
||||
if (errorData?.error === 'SESSION_REFRESH_REQUIRED') {
|
||||
logger.warn("Query detected SESSION_REFRESH_REQUIRED");
|
||||
|
||||
if (!queryRefreshRedirect) {
|
||||
const currentUrl = window.location.pathname + window.location.search;
|
||||
logger.info("Query initiating refresh redirect to:", currentUrl);
|
||||
|
||||
queryRefreshRedirect = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
queryRefreshRedirect.finally(() => {
|
||||
setTimeout(() => {
|
||||
queryRefreshRedirect = null;
|
||||
}, 1000);
|
||||
});
|
||||
} else {
|
||||
logger.info("Query: refresh redirect already in progress");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return failureCount < 3;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { useTeam } from "../queries";
|
||||
import Avatar from "@/components/avatar";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
import SongSummary from "./team-form/song-summary";
|
||||
|
||||
interface TeamCardProps {
|
||||
@@ -46,11 +46,10 @@ const TeamCard = ({ teamId }: TeamCardProps) => {
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Group gap="md" align="center" p="xs">
|
||||
<Avatar
|
||||
name={team.name}
|
||||
<TeamAvatar
|
||||
team={team}
|
||||
size={40}
|
||||
radius="md"
|
||||
src={team.logo ? `/api/files/teams/${team.id}/${team.logo}` : undefined}
|
||||
style={{
|
||||
backgroundColor: team.primary_color || undefined,
|
||||
color: team.accent_color || undefined,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import Avatar from "@/components/avatar";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
import { TeamInfo } from "@/features/teams/types";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useCallback, useMemo } from "react";
|
||||
@@ -88,15 +88,10 @@ const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => {
|
||||
key={`team-list-${team.id}`}
|
||||
p="xs"
|
||||
icon={
|
||||
<Avatar
|
||||
<TeamAvatar
|
||||
team={team}
|
||||
radius="sm"
|
||||
size={40}
|
||||
name={`${team.name}`}
|
||||
src={
|
||||
team.logo
|
||||
? `/api/files/teams/${team.id}/${team.logo}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
style={{ cursor: "pointer" }}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useState, useCallback, useMemo, memo } from "react";
|
||||
import { useTournament, useUnenrolledTeams } from "../queries";
|
||||
import useEnrollTeam from "../hooks/use-enroll-team";
|
||||
import useUnenrollTeam from "../hooks/use-unenroll-team";
|
||||
import Avatar from "@/components/avatar";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
import { Team, TeamInfo } from "@/features/teams/types";
|
||||
|
||||
interface EditEnrolledTeamsProps {
|
||||
@@ -22,9 +22,10 @@ interface TeamItemProps {
|
||||
team: TeamInfo;
|
||||
onUnenroll: (teamId: string) => void;
|
||||
disabled: boolean;
|
||||
isRegional?: boolean;
|
||||
}
|
||||
|
||||
const TeamItem = memo(({ team, onUnenroll, disabled }: TeamItemProps) => {
|
||||
const TeamItem = memo(({ team, onUnenroll, disabled, isRegional }: TeamItemProps) => {
|
||||
const playerNames = useMemo(
|
||||
() =>
|
||||
team.players?.map((p) => `${p.first_name} ${p.last_name}`).join(", ") ||
|
||||
@@ -34,15 +35,11 @@ const TeamItem = memo(({ team, onUnenroll, disabled }: TeamItemProps) => {
|
||||
|
||||
return (
|
||||
<Group py="xs" px="sm" w="100%" gap="sm" align="center">
|
||||
<Avatar
|
||||
<TeamAvatar
|
||||
team={team}
|
||||
size={32}
|
||||
radius="sm"
|
||||
name={team.name}
|
||||
src={
|
||||
team.logo
|
||||
? `/api/files/teams/${team.id}/${team.logo}`
|
||||
: undefined
|
||||
}
|
||||
isRegional={isRegional}
|
||||
/>
|
||||
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={500} truncate>
|
||||
@@ -73,6 +70,8 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
|
||||
const { data: unenrolledTeams = [], isLoading: unenrolledLoading } =
|
||||
useUnenrolledTeams(tournamentId);
|
||||
|
||||
const isRegional = tournament?.regional;
|
||||
|
||||
const { mutate: enrollTeam, isPending: isEnrolling } = useEnrollTeam();
|
||||
const { mutate: unenrollTeam, isPending: isUnenrolling } = useUnenrollTeam();
|
||||
|
||||
@@ -107,15 +106,11 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
|
||||
const team = option.data;
|
||||
return (
|
||||
<Group py="xs" px="sm" gap="sm" align="center">
|
||||
<Avatar
|
||||
<TeamAvatar
|
||||
team={team as any}
|
||||
size={32}
|
||||
radius="sm"
|
||||
name={team.name}
|
||||
src={
|
||||
team.logo
|
||||
? `/api/files/teams/${team.id}/${team.logo}`
|
||||
: undefined
|
||||
}
|
||||
isRegional={isRegional}
|
||||
/>
|
||||
<Text fw={500} truncate>
|
||||
{team.name}
|
||||
@@ -174,6 +169,7 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
|
||||
team={team}
|
||||
onUnenroll={handleUnenrollTeam}
|
||||
disabled={isUnenrolling}
|
||||
isRegional={isRegional}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -87,6 +87,7 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
start_time: tournament.start_time,
|
||||
enroll_time: tournament.enroll_time,
|
||||
end_time: tournament.end_time,
|
||||
regional: tournament.regional,
|
||||
}}
|
||||
close={closeEditTournament}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DotsNineIcon } from "@phosphor-icons/react";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
||||
import { generateTournamentBracket } from "../../matches/server";
|
||||
import { TeamInfo } from "@/features/teams/types";
|
||||
import Avatar from "@/components/avatar";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
import { useBracketPreview } from "@/features/bracket/queries";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
@@ -23,11 +23,13 @@ import { tournamentKeys } from "../queries";
|
||||
interface SeedTournamentProps {
|
||||
tournamentId: string;
|
||||
teams: TeamInfo[];
|
||||
isRegional?: boolean;
|
||||
}
|
||||
|
||||
const SeedTournament: React.FC<SeedTournamentProps> = ({
|
||||
tournamentId,
|
||||
teams,
|
||||
isRegional,
|
||||
}) => {
|
||||
const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams);
|
||||
const { data: bracketPreview } = useBracketPreview(teams.length);
|
||||
@@ -171,15 +173,11 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<Avatar
|
||||
<TeamAvatar
|
||||
team={team}
|
||||
size={24}
|
||||
radius="sm"
|
||||
name={team.name}
|
||||
src={
|
||||
team.logo
|
||||
? `/api/files/teams/${team.id}/${team.logo}`
|
||||
: undefined
|
||||
}
|
||||
isRegional={isRegional}
|
||||
/>
|
||||
|
||||
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FileInput, Stack, TextInput, Textarea } from "@mantine/core";
|
||||
import { FileInput, Stack, TextInput, Textarea, Checkbox } from "@mantine/core";
|
||||
import { useForm, UseFormInput } from "@mantine/form";
|
||||
import { LinkIcon } from "@phosphor-icons/react";
|
||||
import SlidePanel, { SlidePanelField } from "@/components/sheet/slide-panel";
|
||||
@@ -35,6 +35,7 @@ const TournamentForm = ({
|
||||
enroll_time: initialValues?.enroll_time || "",
|
||||
end_time: initialValues?.end_time || "",
|
||||
logo: undefined,
|
||||
regional: initialValues?.regional || false,
|
||||
},
|
||||
onSubmitPreventDefault: "always",
|
||||
validate: {
|
||||
@@ -150,6 +151,12 @@ const TournamentForm = ({
|
||||
minRows={3}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label="Regional Tournament"
|
||||
key={form.key("regional")}
|
||||
{...form.getInputProps("regional", { type: "checkbox" })}
|
||||
/>
|
||||
|
||||
<FileInput
|
||||
key={form.key("logo")}
|
||||
accept="image/png,image/jpeg,image/gif,image/jpg"
|
||||
|
||||
@@ -63,6 +63,7 @@ export const tournamentInputSchema = z.object({
|
||||
enroll_time: z.string(),
|
||||
start_time: z.string(),
|
||||
end_time: z.string().optional(),
|
||||
regional: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type TournamentInput = z.infer<typeof tournamentInputSchema>;
|
||||
|
||||
@@ -7,7 +7,7 @@ export function createMatchesService(pb: PocketBase) {
|
||||
return {
|
||||
async getMatch(id: string): Promise<Match | null> {
|
||||
const result = await pb.collection("matches").getOne(id, {
|
||||
expand: "tournament, home, away",
|
||||
expand: "tournament, home, away, home.players, away.players",
|
||||
});
|
||||
return transformMatch(result);
|
||||
},
|
||||
@@ -19,7 +19,7 @@ export function createMatchesService(pb: PocketBase) {
|
||||
|
||||
const result = await pb.collection("matches").getFullList({
|
||||
filter: `tournament="${match.tournament.id}" && (home_from_lid = ${match.lid} || away_from_lid = ${match.lid}) && bye = false`,
|
||||
expand: "tournament, home, away",
|
||||
expand: "tournament, home, away, home.players, away.players",
|
||||
});
|
||||
|
||||
const winnerMatch = result.find(m => (m.home_from_lid === match.lid && !m.home_from_loser) || (m.away_from_lid === match.lid && !m.away_from_loser));
|
||||
@@ -50,7 +50,7 @@ export function createMatchesService(pb: PocketBase) {
|
||||
async updateMatch(id: string, data: Partial<MatchInput>): Promise<Match> {
|
||||
logger.info("PocketBase | Updating match", { id, data });
|
||||
const result = await pb.collection("matches").update<Match>(id, data, {
|
||||
expand: 'home, away, tournament'
|
||||
expand: 'home, away, tournament, home.players, away.players'
|
||||
});
|
||||
return transformMatch(result);
|
||||
},
|
||||
|
||||
@@ -126,7 +126,7 @@ export function createPlayersService(pb: PocketBase) {
|
||||
const result = await pb.collection("matches").getFullList({
|
||||
filter: `(${teamFilter}) && (status = "ended" || status = "started")`,
|
||||
sort: "-created",
|
||||
expand: "tournament,home,away",
|
||||
expand: "tournament,home,away,home.players,away.players",
|
||||
});
|
||||
|
||||
return result.map((match) => transformMatch(match));
|
||||
|
||||
@@ -105,7 +105,7 @@ export function createTeamsService(pb: PocketBase) {
|
||||
const result = await pb.collection("matches").getFullList({
|
||||
filter: `(${teamFilter}) && (status = "ended" || status = "started")`,
|
||||
sort: "-start_time",
|
||||
expand: "tournament,home,away",
|
||||
expand: "tournament,home,away,home.players,away.players",
|
||||
});
|
||||
|
||||
return result.map((match) => transformMatch(match));
|
||||
|
||||
@@ -4,30 +4,34 @@ import Passwordless from "supertokens-web-js/recipe/passwordless";
|
||||
import { appInfo } from "./config";
|
||||
import { logger } from "./";
|
||||
|
||||
let refreshAttemptCount = 0;
|
||||
let refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
export const resetRefreshFlag = () => {
|
||||
refreshAttemptCount = 0;
|
||||
refreshPromise = null;
|
||||
};
|
||||
|
||||
const setupFetchInterceptor = () => {
|
||||
if (typeof window === 'undefined') return;
|
||||
export const getOrCreateRefreshPromise = (refreshFn: () => Promise<boolean>): Promise<boolean> => {
|
||||
if (refreshPromise) {
|
||||
logger.info("Reusing existing refresh promise");
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
//@ts-ignore
|
||||
window.fetch = async (resource: RequestInfo | URL, options?: RequestInit) => {
|
||||
const url = typeof resource === 'string' ? resource :
|
||||
resource instanceof URL ? resource.toString() : resource.url;
|
||||
logger.info("Creating new refresh promise");
|
||||
refreshPromise = refreshFn()
|
||||
.then((result) => {
|
||||
logger.info("Refresh completed successfully:", result);
|
||||
setTimeout(() => {
|
||||
refreshPromise = null;
|
||||
}, 500);
|
||||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Refresh failed:", error);
|
||||
refreshPromise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
if (url.includes('/api/auth/session/refresh')) {
|
||||
refreshAttemptCount++;
|
||||
if (refreshAttemptCount > 1) {
|
||||
throw new Error('Duplicate refresh attempt blocked');
|
||||
}
|
||||
}
|
||||
|
||||
return originalFetch.call(window, resource, options);
|
||||
};
|
||||
return refreshPromise;
|
||||
};
|
||||
|
||||
export const frontendConfig = () => {
|
||||
@@ -53,7 +57,6 @@ export function ensureSuperTokensFrontend() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
if (!initialized) {
|
||||
setupFetchInterceptor();
|
||||
SuperTokens.init(frontendConfig());
|
||||
initialized = true;
|
||||
logger.info("SuperTokens initialized");
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
|
||||
import { ServerResult } from "../types";
|
||||
import toast from '@/lib/sonner'
|
||||
import { logger } from '@/lib/supertokens'
|
||||
|
||||
let isMutationRefreshingSession = false;
|
||||
|
||||
let sessionRefreshRedirect: Promise<void> | null = null;
|
||||
|
||||
export function useServerMutation<TData, TVariables = unknown>(
|
||||
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
||||
@@ -39,24 +41,39 @@ export function useServerMutation<TData, TVariables = unknown>(
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status === 401) {
|
||||
try {
|
||||
const errorData = typeof error.response.data === 'string'
|
||||
? JSON.parse(error.response.data)
|
||||
const errorData = typeof error.response.data === 'string'
|
||||
? JSON.parse(error.response.data)
|
||||
: error.response.data;
|
||||
|
||||
|
||||
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
|
||||
if (!isMutationRefreshingSession) {
|
||||
isMutationRefreshingSession = true;
|
||||
logger.warn("Mutation detected SESSION_REFRESH_REQUIRED");
|
||||
|
||||
if (!sessionRefreshRedirect) {
|
||||
const currentUrl = window.location.pathname + window.location.search;
|
||||
setTimeout(() => {
|
||||
isMutationRefreshingSession = false;
|
||||
}, 1000);
|
||||
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
||||
logger.info("Mutation initiating refresh redirect to:", currentUrl);
|
||||
|
||||
sessionRefreshRedirect = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
sessionRefreshRedirect.finally(() => {
|
||||
setTimeout(() => {
|
||||
sessionRefreshRedirect = null;
|
||||
}, 1000);
|
||||
});
|
||||
} else {
|
||||
logger.info("Mutation: refresh redirect already in progress, waiting...");
|
||||
await sessionRefreshRedirect;
|
||||
}
|
||||
|
||||
throw new Error("SESSION_REFRESH_REQUIRED");
|
||||
}
|
||||
} catch (parseError) {}
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,13 +3,13 @@ import { defineConfig } from 'vite'
|
||||
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => ({
|
||||
server: {
|
||||
port: 3000,
|
||||
allowedHosts: ["dev.flexxon.app", "flexxon.app"]
|
||||
},
|
||||
ssr: {
|
||||
noExternal: true,
|
||||
noExternal: mode === 'production' ? true : ['facehash'],
|
||||
},
|
||||
plugins: [
|
||||
tsConfigPaths({
|
||||
@@ -20,4 +20,4 @@ export default defineConfig({
|
||||
}),
|
||||
react()
|
||||
]
|
||||
})
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user