From de858ec62a31e97b9772cb89d787501569134242 Mon Sep 17 00:00:00 2001 From: xmreur Date: Sun, 14 Jun 2026 18:04:46 +0200 Subject: [PATCH] fix: proposed fix for competing UI on mobile --- apps/web/components/home/InGameScene.tsx | 7 +- apps/web/components/ui/MinimapPanel.test.tsx | 122 ++++++++++++ apps/web/components/ui/MinimapPanel.tsx | 172 +++++++++-------- apps/web/lib/runtime-config.test.ts | 106 +++++------ apps/web/lib/runtime-config.ts | 186 +++++++++---------- 5 files changed, 369 insertions(+), 224 deletions(-) create mode 100644 apps/web/components/ui/MinimapPanel.test.tsx diff --git a/apps/web/components/home/InGameScene.tsx b/apps/web/components/home/InGameScene.tsx index e0a4ba3..b4b3425 100644 --- a/apps/web/components/home/InGameScene.tsx +++ b/apps/web/components/home/InGameScene.tsx @@ -463,7 +463,12 @@ export default function InGameScene({ {uiPhase === 'live_round' && ( - + {guessMapNode} )} diff --git a/apps/web/components/ui/MinimapPanel.test.tsx b/apps/web/components/ui/MinimapPanel.test.tsx new file mode 100644 index 0000000..884da46 --- /dev/null +++ b/apps/web/components/ui/MinimapPanel.test.tsx @@ -0,0 +1,122 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import MinimapPanel from './MinimapPanel'; + +function mockMatchMedia(matches: boolean) { + Object.defineProperty(window, 'matchMedia', { + configurable: true, + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +} + +type MinimapPanelProps = { + onFinalize?: () => void; + canFinalizeGuess?: boolean; + guessSubmitted?: boolean; + roundKey?: string; +}; + +function renderPanel(overrides: MinimapPanelProps = {}) { + const onFinalize = overrides.onFinalize ?? vi.fn(); + const props = { + onFinalize, + canFinalizeGuess: overrides.canFinalizeGuess ?? false, + guessSubmitted: overrides.guessSubmitted ?? false, + roundKey: overrides.roundKey, + }; + + const view = render( + +
Map
+
, + ); + + return { ...view, onFinalize }; +} + +describe('MinimapPanel', () => { + afterEach(() => { + cleanup(); + }); + + describe('mobile', () => { + beforeEach(() => { + mockMatchMedia(false); + }); + + it('shows only the Guess FAB when the map is closed', () => { + renderPanel(); + + expect(screen.getByRole('button', { name: 'Open map to place guess' })).toBeInTheDocument(); + expect(screen.queryByTestId('guess-map')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Back to Street View' })).not.toBeInTheDocument(); + }); + + it('opens the fullscreen map when the Guess FAB is clicked', () => { + renderPanel(); + + fireEvent.click(screen.getByRole('button', { name: 'Open map to place guess' })); + + expect(screen.getByTestId('guess-map')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Back to Street View' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Place Pin' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Open map to place guess' })).not.toBeInTheDocument(); + }); + + it('returns to Street View when Back is clicked', () => { + renderPanel(); + + fireEvent.click(screen.getByRole('button', { name: 'Open map to place guess' })); + fireEvent.click(screen.getByRole('button', { name: 'Back to Street View' })); + + expect(screen.getByRole('button', { name: 'Open map to place guess' })).toBeInTheDocument(); + expect(screen.queryByTestId('guess-map')).not.toBeInTheDocument(); + }); + + it('closes the map when roundKey changes', () => { + const { rerender, onFinalize } = renderPanel({ roundKey: 'round-1' }); + + fireEvent.click(screen.getByRole('button', { name: 'Open map to place guess' })); + expect(screen.getByTestId('guess-map')).toBeInTheDocument(); + + rerender( + +
Map
+
, + ); + + expect(screen.getByRole('button', { name: 'Open map to place guess' })).toBeInTheDocument(); + expect(screen.queryByTestId('guess-map')).not.toBeInTheDocument(); + }); + }); + + describe('desktop', () => { + beforeEach(() => { + mockMatchMedia(true); + }); + + it('renders the minimap panel without the mobile Guess FAB', () => { + renderPanel(); + + expect(screen.getByTestId('guess-map')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Place Pin' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Open map to place guess' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Back to Street View' })).not.toBeInTheDocument(); + }); + }); +}); diff --git a/apps/web/components/ui/MinimapPanel.tsx b/apps/web/components/ui/MinimapPanel.tsx index 7d39751..3023c98 100644 --- a/apps/web/components/ui/MinimapPanel.tsx +++ b/apps/web/components/ui/MinimapPanel.tsx @@ -1,22 +1,32 @@ import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, ReactNode } from 'react'; import { useEffect, useRef, useState } from 'react'; +import { ChevronLeft, MapPin } from 'lucide-react'; type Props = { children: ReactNode; onFinalize: () => void; canFinalizeGuess: boolean; guessSubmitted: boolean; + roundKey?: string; }; -export default function MinimapPanel({ children, onFinalize, canFinalizeGuess, guessSubmitted }: Props) { - const RIGHT_GUTTER_PX = 80; - const GESTURE_MOVE_THRESHOLD_PX = 6; +const GESTURE_MOVE_THRESHOLD_PX = 6; +const finalizeButtonClassName = + 'font-hud relative z-10 min-h-11 w-full rounded-pill border border-emerald-200/35 bg-cta-gradient px-6 py-2 text-center text-sm uppercase tracking-[0.15em] text-white shadow-elev-3 transition hover:brightness-110 disabled:cursor-not-allowed'; + +export default function MinimapPanel({ + children, + onFinalize, + canFinalizeGuess, + guessSubmitted, + roundKey, +}: Props) { const panelRef = useRef(null); const activePointerRef = useRef<{ id: number; x: number; y: number } | null>(null); const resizeLockedRef = useRef(false); const suppressNextPanelClickRef = useRef(false); const [desktopHovered, setDesktopHovered] = useState(false); - const [mobileExpanded, setMobileExpanded] = useState(false); + const [mapViewOpen, setMapViewOpen] = useState(false); const [isDesktop, setIsDesktop] = useState(false); useEffect(() => { @@ -26,7 +36,7 @@ export default function MinimapPanel({ children, onFinalize, canFinalizeGuess, g const syncViewport = () => { const desktop = mediaQuery.matches; setIsDesktop(desktop); - if (desktop) setMobileExpanded(false); + if (desktop) setMapViewOpen(false); }; syncViewport(); @@ -40,6 +50,10 @@ export default function MinimapPanel({ children, onFinalize, canFinalizeGuess, g return () => mediaQuery.removeListener(syncViewport); }, []); + useEffect(() => { + setMapViewOpen(false); + }, [roundKey]); + useEffect(() => { if (typeof window === 'undefined') return; @@ -70,15 +84,15 @@ export default function MinimapPanel({ children, onFinalize, canFinalizeGuess, g }; }, [isDesktop]); - const expanded = isDesktop ? desktopHovered : mobileExpanded; - const reserveRightGutter = isDesktop || !mobileExpanded; + const expanded = desktopHovered; const finalizeLabel = guessSubmitted ? 'Waiting for opponent...' : canFinalizeGuess ? 'Guess' : 'Place Pin'; + const finalizeDisabledClassName = guessSubmitted ? 'opacity-45' : 'disabled:opacity-70'; const beginPointerGesture = (event: ReactPointerEvent) => { activePointerRef.current = { id: event.pointerId, x: event.clientX, - y: event.clientY + y: event.clientY, }; resizeLockedRef.current = true; suppressNextPanelClickRef.current = false; @@ -99,87 +113,91 @@ export default function MinimapPanel({ children, onFinalize, canFinalizeGuess, g if (activePointerRef.current?.id === event.pointerId) activePointerRef.current = null; }; - const handlePanelClick = () => { - if (suppressNextPanelClickRef.current) { - suppressNextPanelClickRef.current = false; - return; - } - if (!isDesktop) setMobileExpanded(true); - }; - - const handleBackdropClick = () => { - if (suppressNextPanelClickRef.current) { - suppressNextPanelClickRef.current = false; - return; - } - setMobileExpanded(false); - }; - const handleFinalizeClick = (event: ReactMouseEvent) => { event.stopPropagation(); onFinalize(); }; - return ( - <> - {!isDesktop && mobileExpanded ? ( + const finalizeButton = ( + + ); + + if (!isDesktop) { + if (!mapViewOpen) { + return ( + ); + } + + return ( + <> +
+
+ {children} +
+ {finalizeButton}
+ + ); + } + + return ( +
{ + if (resizeLockedRef.current || event.buttons !== 0) { + resizeLockedRef.current = true; + return; + } + setDesktopHovered(true); + }} + onMouseLeave={(event) => { + if (resizeLockedRef.current || event.buttons !== 0) { + resizeLockedRef.current = true; + return; + } + setDesktopHovered(false); + }} + className={`absolute bottom-0 right-0 z-30 flex w-full flex-col gap-2 p-3 transition-[width,height] duration-150 ease-out md:bottom-4 md:right-4 md:h-[min(33vh,360px)] md:w-[min(34vw,460px)] md:p-0 ${ + expanded ? 'md:h-[min(52vh,560px)] md:w-[min(90vw,800px)]' : '' + }`} + > +
+ {children}
- + {finalizeButton} +
); } diff --git a/apps/web/lib/runtime-config.test.ts b/apps/web/lib/runtime-config.test.ts index fd43200..77271ba 100644 --- a/apps/web/lib/runtime-config.test.ts +++ b/apps/web/lib/runtime-config.test.ts @@ -2,65 +2,65 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createRuntimeConfig, normalizeHTTPBase, normalizeWSBase } from './runtime-config'; describe('runtime-config', () => { - const originalAppVersion = process.env.NEXT_PUBLIC_APP_VERSION; - const originalGitSha = process.env.NEXT_PUBLIC_GIT_SHA; + const originalAppVersion = process.env.NEXT_PUBLIC_APP_VERSION; + const originalGitSha = process.env.NEXT_PUBLIC_GIT_SHA; - beforeEach(() => { - vi.unstubAllEnvs(); - delete process.env.NEXT_PUBLIC_APP_VERSION; - delete process.env.NEXT_PUBLIC_GIT_SHA; - }); - - afterEach(() => { - vi.unstubAllEnvs(); - if (originalAppVersion === undefined) { - delete process.env.NEXT_PUBLIC_APP_VERSION; - } else { - process.env.NEXT_PUBLIC_APP_VERSION = originalAppVersion; - } - if (originalGitSha === undefined) { - delete process.env.NEXT_PUBLIC_GIT_SHA; - } else { - process.env.NEXT_PUBLIC_GIT_SHA = originalGitSha; - } - }); + beforeEach(() => { + vi.unstubAllEnvs(); + delete process.env.NEXT_PUBLIC_APP_VERSION; + delete process.env.NEXT_PUBLIC_GIT_SHA; + }); - it('builds config from provided runtime overrides', () => { - const config = createRuntimeConfig({ - NEXT_PUBLIC_QUEUE_URL: 'https://queue.example.com', - NEXT_PUBLIC_REALTIME_URL: 'wss://realtime.example.com', - NEXT_PUBLIC_API_URL: 'https://api.example.com', - NEXT_PUBLIC_GOOGLE_CLIENT_ID: 'google-client', - NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS: 'https://one.test, https://two.test', - NEXT_PUBLIC_TURNSTILE_SITE_KEY: 'turnstile-site-key', - NEXT_PUBLIC_GOOGLE_EMBED_KEY: 'embed-key', - NEXT_PUBLIC_APP_VERSION: 'sha-123' + afterEach(() => { + vi.unstubAllEnvs(); + if (originalAppVersion === undefined) { + delete process.env.NEXT_PUBLIC_APP_VERSION; + } else { + process.env.NEXT_PUBLIC_APP_VERSION = originalAppVersion; + } + if (originalGitSha === undefined) { + delete process.env.NEXT_PUBLIC_GIT_SHA; + } else { + process.env.NEXT_PUBLIC_GIT_SHA = originalGitSha; + } }); - expect(config.queueURL).toBe('https://queue.example.com'); - expect(config.realtimeBaseURL).toBe('wss://realtime.example.com'); - expect(config.apiURL).toBe('https://api.example.com'); - expect(config.googleClientId).toBe('google-client'); - expect(config.googleAllowedOrigins).toEqual(['https://one.test', 'https://two.test']); - expect(config.turnstileSiteKey).toBe('turnstile-site-key'); - expect(config.googleEmbedKey).toBe('embed-key'); - expect(config.appVersion).toBe('sha-123'); - }); + it('builds config from provided runtime overrides', () => { + const config = createRuntimeConfig({ + NEXT_PUBLIC_QUEUE_URL: 'https://queue.example.com', + NEXT_PUBLIC_REALTIME_URL: 'wss://realtime.example.com', + NEXT_PUBLIC_API_URL: 'https://api.example.com', + NEXT_PUBLIC_GOOGLE_CLIENT_ID: 'google-client', + NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS: 'https://one.test, https://two.test', + NEXT_PUBLIC_TURNSTILE_SITE_KEY: 'turnstile-site-key', + NEXT_PUBLIC_GOOGLE_EMBED_KEY: 'embed-key', + NEXT_PUBLIC_APP_VERSION: 'sha-123' + }); - it('ignores placeholder runtime values', () => { - const config = createRuntimeConfig({ - NEXT_PUBLIC_API_URL: 'REPLACE_WITH_API_URL', - NEXT_PUBLIC_APP_VERSION: 'REPLACE_WITH_SHA' + expect(config.queueURL).toBe('https://queue.example.com'); + expect(config.realtimeBaseURL).toBe('wss://realtime.example.com'); + expect(config.apiURL).toBe('https://api.example.com'); + expect(config.googleClientId).toBe('google-client'); + expect(config.googleAllowedOrigins).toEqual(['https://one.test', 'https://two.test']); + expect(config.turnstileSiteKey).toBe('turnstile-site-key'); + expect(config.googleEmbedKey).toBe('embed-key'); + expect(config.appVersion).toBe('sha-123'); }); - expect(config.apiURL).toBe('http://localhost:8080'); - expect(config.appVersion).toBe('dev'); - }); + it('ignores placeholder runtime values', () => { + const config = createRuntimeConfig({ + NEXT_PUBLIC_API_URL: 'REPLACE_WITH_API_URL', + NEXT_PUBLIC_APP_VERSION: 'REPLACE_WITH_SHA' + }); - it('normalizes websocket and http base urls', () => { - expect(normalizeHTTPBase('ws://localhost:8090')).toBe('http://localhost:8090'); - expect(normalizeHTTPBase('wss://example.com')).toBe('https://example.com'); - expect(normalizeWSBase('http://localhost:8092')).toBe('ws://localhost:8092'); - expect(normalizeWSBase('https://example.com')).toBe('wss://example.com'); - }); + expect(config.apiURL).toBe('http://localhost:8080'); + expect(config.appVersion).toBe('dev'); + }); + + it('normalizes websocket and http base urls', () => { + expect(normalizeHTTPBase('ws://localhost:8090')).toBe('http://localhost:8090'); + expect(normalizeHTTPBase('wss://example.com')).toBe('https://example.com'); + expect(normalizeWSBase('http://localhost:8092')).toBe('ws://localhost:8092'); + expect(normalizeWSBase('https://example.com')).toBe('wss://example.com'); + }); }); diff --git a/apps/web/lib/runtime-config.ts b/apps/web/lib/runtime-config.ts index b3a4c87..e246ead 100644 --- a/apps/web/lib/runtime-config.ts +++ b/apps/web/lib/runtime-config.ts @@ -1,121 +1,121 @@ declare global { - interface Window { - __GEODUELS_CONFIG__?: Partial; - } + interface Window { + __GEODUELS_CONFIG__?: Partial; + } } export type WindowRuntimeConfig = { - NEXT_PUBLIC_QUEUE_URL: string; - NEXT_PUBLIC_REALTIME_URL: string; - NEXT_PUBLIC_API_URL: string; - NEXT_PUBLIC_GOOGLE_CLIENT_ID: string; - NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS: string; - NEXT_PUBLIC_DISCORD_CLIENT_ID: string; - NEXT_PUBLIC_TURNSTILE_SITE_KEY: string; - NEXT_PUBLIC_GOOGLE_EMBED_KEY: string; - NEXT_PUBLIC_APP_VERSION: string; + NEXT_PUBLIC_QUEUE_URL: string; + NEXT_PUBLIC_REALTIME_URL: string; + NEXT_PUBLIC_API_URL: string; + NEXT_PUBLIC_GOOGLE_CLIENT_ID: string; + NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS: string; + NEXT_PUBLIC_DISCORD_CLIENT_ID: string; + NEXT_PUBLIC_TURNSTILE_SITE_KEY: string; + NEXT_PUBLIC_GOOGLE_EMBED_KEY: string; + NEXT_PUBLIC_APP_VERSION: string; }; export type RuntimeConfig = { - queueURL: string; - realtimeBaseURL: string; - apiURL: string; - googleClientId: string; - googleAllowedOrigins: string[]; - discordClientId: string; - turnstileSiteKey: string; - googleEmbedKey: string; - appVersion: string; - roundDurationMs: number; - maxHP: number; - queueHeartbeatIntervalMs: number; - socketHeartbeatIntervalMs: number; - socketStaleAfterMs: number; - connectionErrorMessage: string; - gameConnectionErrorMessage: string; + queueURL: string; + realtimeBaseURL: string; + apiURL: string; + googleClientId: string; + googleAllowedOrigins: string[]; + discordClientId: string; + turnstileSiteKey: string; + googleEmbedKey: string; + appVersion: string; + roundDurationMs: number; + maxHP: number; + queueHeartbeatIntervalMs: number; + socketHeartbeatIntervalMs: number; + socketStaleAfterMs: number; + connectionErrorMessage: string; + gameConnectionErrorMessage: string; }; let browserRuntimeConfig: RuntimeConfig | null = null; function splitOrigins(value: string) { - return value - .split(',') - .map((origin) => origin.trim()) - .filter(Boolean); + return value + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean); } function readWindowRuntimeConfig(source?: Partial) { - const runtimeSource = source ?? (typeof window !== 'undefined' ? window.__GEODUELS_CONFIG__ : undefined) ?? {}; - const runtimeEntries = Object.entries(runtimeSource).filter(([, value]) => { - if (value === undefined || value === '') return false; - if (typeof value === 'string' && value.startsWith('REPLACE_WITH_')) return false; - return true; - }); - return Object.fromEntries(runtimeEntries) as Partial; + const runtimeSource = source ?? (typeof window !== 'undefined' ? window.__GEODUELS_CONFIG__ : undefined) ?? {}; + const runtimeEntries = Object.entries(runtimeSource).filter(([, value]) => { + if (value === undefined || value === '') return false; + if (typeof value === 'string' && value.startsWith('REPLACE_WITH_')) return false; + return true; + }); + return Object.fromEntries(runtimeEntries) as Partial; } export function createRuntimeConfig(source?: Partial): RuntimeConfig { - const defaults: WindowRuntimeConfig = { - NEXT_PUBLIC_QUEUE_URL: process.env.NEXT_PUBLIC_QUEUE_URL || 'http://localhost:8090', - NEXT_PUBLIC_REALTIME_URL: process.env.NEXT_PUBLIC_REALTIME_URL || 'http://localhost:8092', - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080', - NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '', - NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS: process.env.NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS || '', - NEXT_PUBLIC_DISCORD_CLIENT_ID: process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID || '', - NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || '', - NEXT_PUBLIC_GOOGLE_EMBED_KEY: process.env.NEXT_PUBLIC_GOOGLE_EMBED_KEY || 'NO_KEY_DEFINED', - NEXT_PUBLIC_APP_VERSION: - process.env.NEXT_PUBLIC_APP_VERSION || (process.env.NEXT_PUBLIC_GIT_SHA || 'dev').slice(0, 12) - }; - const publicRuntimeConfig = { - ...defaults, - ...readWindowRuntimeConfig(source) - }; - const config: RuntimeConfig = { - queueURL: publicRuntimeConfig.NEXT_PUBLIC_QUEUE_URL, - realtimeBaseURL: publicRuntimeConfig.NEXT_PUBLIC_REALTIME_URL, - apiURL: publicRuntimeConfig.NEXT_PUBLIC_API_URL, - googleClientId: publicRuntimeConfig.NEXT_PUBLIC_GOOGLE_CLIENT_ID, - googleAllowedOrigins: splitOrigins(publicRuntimeConfig.NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS), - discordClientId: publicRuntimeConfig.NEXT_PUBLIC_DISCORD_CLIENT_ID, - turnstileSiteKey: publicRuntimeConfig.NEXT_PUBLIC_TURNSTILE_SITE_KEY, - googleEmbedKey: publicRuntimeConfig.NEXT_PUBLIC_GOOGLE_EMBED_KEY, - appVersion: publicRuntimeConfig.NEXT_PUBLIC_APP_VERSION, - roundDurationMs: 45_000, - maxHP: 6_000, - queueHeartbeatIntervalMs: 10_000, - socketHeartbeatIntervalMs: 20_000, - socketStaleAfterMs: 35_000, - connectionErrorMessage: 'Connection error', - gameConnectionErrorMessage: 'Connection lost. Reconnecting...' - }; - if (process.env.NODE_ENV !== 'production') { - Object.freeze(config.googleAllowedOrigins); - Object.freeze(config); - } - return config; + const defaults: WindowRuntimeConfig = { + NEXT_PUBLIC_QUEUE_URL: process.env.NEXT_PUBLIC_QUEUE_URL || 'http://localhost:8090', + NEXT_PUBLIC_REALTIME_URL: process.env.NEXT_PUBLIC_REALTIME_URL || 'http://localhost:8092', + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080', + NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '', + NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS: process.env.NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS || '', + NEXT_PUBLIC_DISCORD_CLIENT_ID: process.env.NEXT_PUBLIC_DISCORD_CLIENT_ID || '', + NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || '', + NEXT_PUBLIC_GOOGLE_EMBED_KEY: process.env.NEXT_PUBLIC_GOOGLE_EMBED_KEY || 'NO_KEY_DEFINED', + NEXT_PUBLIC_APP_VERSION: + process.env.NEXT_PUBLIC_APP_VERSION || (process.env.NEXT_PUBLIC_GIT_SHA || 'dev').slice(0, 12) + }; + const publicRuntimeConfig = { + ...defaults, + ...readWindowRuntimeConfig(source) + }; + const config: RuntimeConfig = { + queueURL: publicRuntimeConfig.NEXT_PUBLIC_QUEUE_URL, + realtimeBaseURL: publicRuntimeConfig.NEXT_PUBLIC_REALTIME_URL, + apiURL: publicRuntimeConfig.NEXT_PUBLIC_API_URL, + googleClientId: publicRuntimeConfig.NEXT_PUBLIC_GOOGLE_CLIENT_ID, + googleAllowedOrigins: splitOrigins(publicRuntimeConfig.NEXT_PUBLIC_GOOGLE_ALLOWED_ORIGINS), + discordClientId: publicRuntimeConfig.NEXT_PUBLIC_DISCORD_CLIENT_ID, + turnstileSiteKey: publicRuntimeConfig.NEXT_PUBLIC_TURNSTILE_SITE_KEY, + googleEmbedKey: publicRuntimeConfig.NEXT_PUBLIC_GOOGLE_EMBED_KEY, + appVersion: publicRuntimeConfig.NEXT_PUBLIC_APP_VERSION, + roundDurationMs: 45_000, + maxHP: 6_000, + queueHeartbeatIntervalMs: 10_000, + socketHeartbeatIntervalMs: 20_000, + socketStaleAfterMs: 35_000, + connectionErrorMessage: 'Connection error', + gameConnectionErrorMessage: 'Connection lost. Reconnecting...' + }; + if (process.env.NODE_ENV !== 'production') { + Object.freeze(config.googleAllowedOrigins); + Object.freeze(config); + } + return config; } export function getRuntimeConfig(): RuntimeConfig { - if (typeof window === 'undefined') { - return createRuntimeConfig(); - } - if (!browserRuntimeConfig) { - browserRuntimeConfig = createRuntimeConfig(); - } - return browserRuntimeConfig; + if (typeof window === 'undefined') { + return createRuntimeConfig(); + } + if (!browserRuntimeConfig) { + browserRuntimeConfig = createRuntimeConfig(); + } + return browserRuntimeConfig; } export function normalizeHTTPBase(value: string): string { - if (!value) return ''; - if (value.startsWith('ws://')) return `http://${value.slice(5)}`; - if (value.startsWith('wss://')) return `https://${value.slice(6)}`; - return value; + if (!value) return ''; + if (value.startsWith('ws://')) return `http://${value.slice(5)}`; + if (value.startsWith('wss://')) return `https://${value.slice(6)}`; + return value; } export function normalizeWSBase(value: string): string { - if (!value) return ''; - if (value.startsWith('http://')) return `ws://${value.slice(7)}`; - if (value.startsWith('https://')) return `wss://${value.slice(8)}`; - return value; + if (!value) return ''; + if (value.startsWith('http://')) return `ws://${value.slice(7)}`; + if (value.startsWith('https://')) return `wss://${value.slice(8)}`; + return value; }