diff --git a/public/images/20261024_banner_open_day.webp b/public/images/20261024_banner_open_day.webp new file mode 100644 index 0000000..4f82b3a Binary files /dev/null and b/public/images/20261024_banner_open_day.webp differ diff --git a/src/components/molecules/EventDialog/EventDialog.test.tsx b/src/components/molecules/EventDialog/EventDialog.test.tsx new file mode 100644 index 0000000..0460136 --- /dev/null +++ b/src/components/molecules/EventDialog/EventDialog.test.tsx @@ -0,0 +1,61 @@ +import { act, fireEvent, render, screen } from '@testing-library/react' + +import EventDialog from './index.js' + +describe('EventDialog', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('appears after a delay', () => { + vi.setSystemTime(new Date(2026, 9, 19)) + render() + + expect(screen.queryByRole('img')).not.toBeInTheDocument() + + act(() => { + vi.advanceTimersByTime(3000) + }) + + expect(screen.getByRole('img', { name: /open day/i })).toBeInTheDocument() + }) + + it('can be dismissed', () => { + vi.setSystemTime(new Date(2026, 9, 19)) + render() + act(() => { + vi.advanceTimersByTime(3000) + }) + + fireEvent.click(screen.getByRole('button', { name: /close/i })) + act(() => { + vi.runAllTimers() + }) + + expect(screen.queryByRole('img')).not.toBeInTheDocument() + }) + + it('still appears on October 25th', () => { + vi.setSystemTime(new Date(2026, 9, 25, 23, 59)) + render() + act(() => { + vi.advanceTimersByTime(3000) + }) + + expect(screen.getByRole('img', { name: /open day/i })).toBeInTheDocument() + }) + + it('does not appear after October 25th', () => { + vi.setSystemTime(new Date(2026, 9, 26)) + render() + act(() => { + vi.advanceTimersByTime(10000) + }) + + expect(screen.queryByRole('img')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/molecules/EventDialog/index.tsx b/src/components/molecules/EventDialog/index.tsx new file mode 100644 index 0000000..67d668a --- /dev/null +++ b/src/components/molecules/EventDialog/index.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react' +import { Box, Dialog, IconButton } from '@mui/material' +import CloseIcon from '@mui/icons-material/Close' + +const SHOW_DELAY_MS = 3000 +// Local midnight after Oct 25th, so the event is promoted through the whole day. +const EXPIRES_AT = new Date(2026, 9, 26) + +const EventDialog = () => { + const [open, setOpen] = useState(false) + + useEffect(() => { + if (Date.now() >= EXPIRES_AT.getTime()) return + const timer = setTimeout(() => setOpen(true), SHOW_DELAY_MS) + return () => clearTimeout(timer) + }, []) + + return ( + setOpen(false)} maxWidth={false} aria-label="Open Day event"> + setOpen(false)} + sx={{ position: 'absolute', top: 4, right: 4, bgcolor: 'background.paper', '&:hover': { bgcolor: 'background.paper' } }}> + + + + + ) +} + +export default EventDialog diff --git a/src/components/templates/RouteTracker/index.tsx b/src/components/templates/RouteTracker/index.tsx index 49e55a0..4504aac 100644 --- a/src/components/templates/RouteTracker/index.tsx +++ b/src/components/templates/RouteTracker/index.tsx @@ -1,6 +1,8 @@ import { useEffect } from 'react' import { Outlet, useLocation } from 'react-router-dom' +import EventDialog from 'components/molecules/EventDialog/index.js' + /** * Route tracking component that wraps all routes and includes analytics page tracking */ @@ -11,7 +13,12 @@ const RouteTracker = () => { window.scrollTo(0, 0) }, [location]) - return + return ( + <> + + + + ) } export default RouteTracker