diff --git a/app/src/App.tsx b/app/src/App.tsx
index e21ef608f..854939bad 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -22,6 +22,7 @@ import {
Content,
AuthProvider,
Modal,
+ InfoRedirect,
InvitePage,
LoginPage,
SignupPage,
@@ -210,9 +211,7 @@ function App() {
>
{tagsApi && }
-
-
-
+
@@ -258,6 +257,14 @@ function App() {
}
/>
+
+
+
+ }
+ />
} />
} />
} />
diff --git a/app/src/ModalContent.tsx b/app/src/ModalContent.tsx
index c79a850b9..ec59cf1bd 100644
--- a/app/src/ModalContent.tsx
+++ b/app/src/ModalContent.tsx
@@ -4,7 +4,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
import { TextView } from 'utopia-ui'
import { config } from './config'
@@ -61,21 +62,13 @@ export function Welcome1({ clickAction1, map }: ChapterProps) {
)
}
-const close = () => {
- const myModal = document.getElementById('my_modal_3') as HTMLDialogElement
- myModal.close()
-}
-
export const ModalContent = ({ map }: { map: any }) => {
- useEffect(() => {
- const myModal = document.getElementById('my_modal_3') as HTMLDialogElement
- if (map.info_open) {
- myModal.showModal()
- }
- }, [map.info_open])
+ const navigate = useNavigate()
+ const [chapter] = useState(1)
- const [chapter, setChapter] = useState(1)
- // const setQuestsOpen = useSetQuestOpen()
+ const close = () => {
+ void navigate('/')
+ }
const ActiveChapter = () => {
switch (chapter) {
@@ -85,10 +78,6 @@ export const ModalContent = ({ map }: { map: any }) => {
map={map}
clickAction1={() => {
close()
- setTimeout(() => {
- // setQuestsOpen(true);
- setChapter(1)
- }, 1000)
}}
/>
)
diff --git a/cypress/e2e/info-modal/info-modal.cy.ts b/cypress/e2e/info-modal/info-modal.cy.ts
new file mode 100644
index 000000000..50bda6b8b
--- /dev/null
+++ b/cypress/e2e/info-modal/info-modal.cy.ts
@@ -0,0 +1,50 @@
+///
+
+/**
+ * Info Modal Route E2E Tests
+ *
+ * Validates the route-based info modal introduced by PR #657:
+ * - Route /info renders the modal (E1)
+ * - NavBar ? link navigates to /info (E2)
+ * - Content "Close" button navigates back to / (E3)
+ */
+
+describe('Info Modal Route', () => {
+ it('E1: visiting /info renders the info modal', () => {
+ cy.visit('/info')
+
+ cy.get('.tw\\:card', { timeout: 15000 }).should('be.visible')
+ cy.get('.tw\\:backdrop-brightness-75').should('exist')
+ cy.contains('Close').should('be.visible')
+ cy.location('pathname').should('eq', '/info')
+ })
+
+ it('E2: NavBar ? icon navigates to /info', () => {
+ cy.visit('/')
+ cy.waitForMapReady()
+
+ // Dismiss auto-opened modal if info_open is true in backend
+ cy.get('body').then(($body) => {
+ if ($body.find('.tw\\:backdrop-brightness-75').length > 0) {
+ cy.get('.tw\\:card button').contains('✕').click()
+ cy.location('pathname').should('eq', '/')
+ }
+ })
+
+ cy.get('a[href="/info"]').should('be.visible').click()
+
+ cy.location('pathname').should('eq', '/info')
+ cy.get('.tw\\:card', { timeout: 10000 }).should('be.visible')
+ })
+
+ it('E3: content "Close" button closes modal and navigates to /', () => {
+ cy.visit('/info')
+ cy.get('.tw\\:card', { timeout: 15000 }).should('be.visible')
+
+ cy.contains('label', 'Close').click()
+
+ cy.location('pathname').should('eq', '/')
+ cy.get('.tw\\:backdrop-brightness-75').should('not.exist')
+ })
+})
+
diff --git a/lib/src/Components/AppShell/InfoRedirect.spec.tsx b/lib/src/Components/AppShell/InfoRedirect.spec.tsx
new file mode 100644
index 000000000..4f0f62497
--- /dev/null
+++ b/lib/src/Components/AppShell/InfoRedirect.spec.tsx
@@ -0,0 +1,78 @@
+import { render, cleanup } from '@testing-library/react'
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+
+import { InfoRedirect } from './InfoRedirect'
+
+// --- Mocks ---
+
+const mockNavigate = vi.fn()
+
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom')
+ return {
+ ...actual,
+ useNavigate: () => mockNavigate,
+ }
+})
+
+// Save original location so we can restore it after each test
+const originalLocation = window.location
+
+// Helper to set window.location.pathname for tests
+function setPathname(pathname: string) {
+ Object.defineProperty(window, 'location', {
+ value: { pathname, search: '', hash: '', href: `http://localhost${pathname}` },
+ writable: true,
+ configurable: true,
+ })
+}
+
+// --- Tests ---
+
+describe('', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ setPathname('/')
+ })
+
+ afterEach(() => {
+ cleanup()
+ // Restore original window.location to avoid leaking into other test files
+ Object.defineProperty(window, 'location', {
+ value: originalLocation,
+ writable: true,
+ configurable: true,
+ })
+ })
+
+ it('U1: navigates to /info when enabled and pathname is "/"', () => {
+ render()
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1)
+ expect(mockNavigate).toHaveBeenCalledWith('/info')
+ })
+
+ it('U2: does NOT navigate when enabled is false', () => {
+ render()
+
+ expect(mockNavigate).not.toHaveBeenCalled()
+ })
+
+ it('U3: does NOT navigate when pathname is not "/"', () => {
+ setPathname('/login')
+
+ render()
+
+ expect(mockNavigate).not.toHaveBeenCalled()
+ })
+
+ it('U4: only navigates once even if re-rendered', () => {
+ const { rerender } = render()
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1)
+
+ rerender()
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/lib/src/Components/AppShell/InfoRedirect.tsx b/lib/src/Components/AppShell/InfoRedirect.tsx
new file mode 100644
index 000000000..efd17ca54
--- /dev/null
+++ b/lib/src/Components/AppShell/InfoRedirect.tsx
@@ -0,0 +1,27 @@
+import { useEffect, useRef } from 'react'
+import { useNavigate } from 'react-router-dom'
+
+interface InfoRedirectProps {
+ /** If true, redirects to /info route on initial page load (once) */
+ enabled?: boolean
+}
+
+/**
+ * Redirects to /info route on initial page load when enabled.
+ * Only redirects once per session to avoid redirect loops.
+ * Place this component inside the Router context but outside of Routes.
+ * @category AppShell
+ */
+export function InfoRedirect({ enabled }: InfoRedirectProps) {
+ const navigate = useNavigate()
+ const hasRedirected = useRef(false)
+
+ useEffect(() => {
+ if (enabled && window.location.pathname === '/' && !hasRedirected.current) {
+ hasRedirected.current = true
+ void navigate('/info')
+ }
+ }, [enabled, navigate])
+
+ return null
+}
diff --git a/lib/src/Components/AppShell/Modal.tsx b/lib/src/Components/AppShell/Modal.tsx
new file mode 100644
index 000000000..d3f082dc8
--- /dev/null
+++ b/lib/src/Components/AppShell/Modal.tsx
@@ -0,0 +1,16 @@
+import { MapOverlayPage } from '#components/Templates'
+
+/**
+ * @category AppShell
+ */
+export function Modal({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/lib/src/Components/AppShell/NavBar.tsx b/lib/src/Components/AppShell/NavBar.tsx
index 17eecc01a..307a96e77 100644
--- a/lib/src/Components/AppShell/NavBar.tsx
+++ b/lib/src/Components/AppShell/NavBar.tsx
@@ -50,14 +50,9 @@ export default function NavBar({ appName }: { appName: string }) {
{appName}
-
+
diff --git a/lib/src/Components/AppShell/index.tsx b/lib/src/Components/AppShell/index.tsx
index 7f27e9d56..2ccadc04c 100644
--- a/lib/src/Components/AppShell/index.tsx
+++ b/lib/src/Components/AppShell/index.tsx
@@ -1,4 +1,6 @@
export * from './AppShell'
export { SideBar } from './SideBar'
export { Content } from './Content'
+export { InfoRedirect } from './InfoRedirect'
+export { Modal } from './Modal'
export { default as SVG } from 'react-inlinesvg'
diff --git a/lib/src/Components/Gaming/Modal.tsx b/lib/src/Components/Gaming/Modal.tsx
deleted file mode 100644
index 32f660587..000000000
--- a/lib/src/Components/Gaming/Modal.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useEffect } from 'react'
-
-/**
- * @category Gaming
- */
-export function Modal({
- children,
- showOnStartup,
-}: {
- children: React.ReactNode
- showOnStartup?: boolean
-}) {
- useEffect(() => {
- if (showOnStartup) {
- window.my_modal_3.showModal()
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- return (
- <>
- {/* You can open the modal using ID.showModal() method */}
-
- >
- )
-}
diff --git a/lib/src/Components/Gaming/index.tsx b/lib/src/Components/Gaming/index.tsx
index 78c873a96..9fffb315c 100644
--- a/lib/src/Components/Gaming/index.tsx
+++ b/lib/src/Components/Gaming/index.tsx
@@ -1,2 +1 @@
-export { Modal } from './Modal'
export { Quests } from './Quests'
diff --git a/lib/src/index.tsx b/lib/src/index.tsx
index e62e6af6f..6d6c3e14a 100644
--- a/lib/src/index.tsx
+++ b/lib/src/index.tsx
@@ -10,11 +10,3 @@ export * from './Components/Input'
export * from './Components/Item'
export * from './Components/Onboarding'
export * from './Components/Profile'
-
-declare global {
- interface Window {
- my_modal_3: {
- showModal(): void
- }
- }
-}