Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/components/Analysis/AnalysisGameList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { MaiaGameListEntry } from 'src/types'
import { formatMaiaName, formatSavedName } from 'src/lib/maiaNames'
import { useRouter } from 'next/router'
import { AnalysisGameListName } from 'src/constants/analysis'

interface GameData {
game_id: string
Expand Down Expand Up @@ -59,6 +60,9 @@ interface AnalysisGameListProps {
onGameSelected?: () => void // Called when a game is selected (for mobile popup closing)
refreshTrigger?: number // Used to trigger refresh when custom analysis is added
embedded?: boolean // Render without outer card container
// Switches to this list; a new nonce switches again (the site's Analysis
// menu asks for a list each time it's used).
requestedList?: { list: AnalysisGameListName; nonce: number } | null
}

export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
Expand All @@ -68,6 +72,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
refreshTrigger,
loadNewWorldChampionshipGame,
embedded = false,
requestedList,
}) => {
const router = useRouter()
const { analysisLichessList, analysisTournamentList } =
Expand Down Expand Up @@ -165,9 +170,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
}
}, [analysisTournamentList, currentId, listKeys])

const [selected, setSelected] = useState<
'tournament' | 'lichess' | 'play' | 'hb' | 'custom' | 'favorites'
>(() => {
const [selected, setSelected] = useState<AnalysisGameListName>(() => {
if (currentId?.[1] === 'custom') {
return 'custom'
} else if (currentId?.[1] === 'lichess') {
Expand All @@ -181,6 +184,9 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
}
return 'tournament'
})
useEffect(() => {
if (requestedList) setSelected(requestedList.list)
}, [requestedList])
const [loadingIndex, setLoadingIndex] = useState<number | null>(null)
const [openIndex, setOpenIndex] = useState<number | null>(initialOpenIndex)

Expand Down Expand Up @@ -802,7 +808,7 @@ export const AnalysisGameList: React.FC<AnalysisGameListProps> = ({
selected={selected}
setSelected={setSelected}
/>
<div className="grid flex-1 grid-cols-5">
<div className="flex flex-1">
<Header
label="Play"
name="play"
Expand Down Expand Up @@ -1122,7 +1128,7 @@ const Header = memo(function Header({
return (
<button
onClick={() => setSelected(name)}
className={`relative flex items-center justify-center py-1 ${selected === name ? 'bg-white/10' : 'bg-white/5 hover:bg-white/10'} ${name === 'favorites' ? 'px-3' : ''}`}
className={`relative flex items-center justify-center whitespace-nowrap py-1 ${name === 'favorites' ? 'flex-none px-2' : 'flex-auto px-1'} ${selected === name ? 'bg-white/10' : 'bg-white/5 hover:bg-white/10'}`}
>
<div className="flex items-center justify-start">
<p
Expand Down
70 changes: 64 additions & 6 deletions src/components/Common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import { ModalContext } from 'src/contexts/ModalContext'
import { WindowSizeContext } from 'src/contexts/WindowSizeContext'
import { LeaderboardNavBadge } from '../Leaderboard/LeaderboardNavBadge'
import { useLeaderboardStatus } from 'src/hooks/useLeaderboardStatus'
import {
ANALYSIS_MENU_ITEMS,
AnalysisMenuRequest,
OPEN_ANALYSIS_LIST_EVENT,
OPEN_ANALYSIS_LIST_KEY,
} from 'src/constants/analysis'

// A header menu opens on hover, keyboard focus or a click (or tap), and
// closes when the pointer or the focus leaves it, or on Escape.
Expand All @@ -40,6 +46,23 @@ export const Header: React.FC = () => {
const [showMenu, setShowMenu] = useState(false)
const [showPlayDropdown, setShowPlayDropdown] = useState(false)
const [showMoreDropdown, setShowMoreDropdown] = useState(false)
const [showAnalysisDropdown, setShowAnalysisDropdown] = useState(false)

const openAnalysisList = (request: AnalysisMenuRequest) => {
setShowAnalysisDropdown(false)
if (router.pathname === '/analysis/[...id]') {
window.dispatchEvent(
new CustomEvent(OPEN_ANALYSIS_LIST_EVENT, { detail: request }),
)
return
}
try {
sessionStorage.setItem(OPEN_ANALYSIS_LIST_KEY, request)
} catch {
// Without storage the analysis page still opens, on its usual tab.
}
router.push('/analysis')
}
const [showProfileDropdown, setShowProfileDropdown] = useState(false)
const { isMobile, width } = useContext(WindowSizeContext)

Expand Down Expand Up @@ -243,12 +266,47 @@ export const Header: React.FC = () => {
)}
</AnimatePresence>
</div>
<Link
href="/analysis"
className={`px-2 py-1 transition-all duration-200 hover:!text-primary ${router.pathname.startsWith('/analysis') ? '!text-primary' : '!text-primary/80'}`}
>
ANALYSIS
</Link>
<div className="relative" {...menuProps(setShowAnalysisDropdown)}>
<Link
href="/analysis"
aria-haspopup="menu"
aria-expanded={showAnalysisDropdown}
onClick={() => setShowAnalysisDropdown(false)}
className={`-gap-1 flex items-center px-2 py-1 transition-all duration-200 hover:!text-primary ${router.pathname.startsWith('/analysis') ? '!text-primary' : '!text-primary/80'}`}
>
<p>ANALYSIS</p>
<motion.i
className="material-symbols-outlined text-sm"
animate={{ rotate: showAnalysisDropdown ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
arrow_drop_down
</motion.i>
</Link>
<AnimatePresence>
{showAnalysisDropdown && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute left-0 top-[100%] z-30 w-56 overflow-hidden rounded-md border border-glass-border bg-backdrop"
>
<div className="divide-y divide-glass-border tracking-wide">
{ANALYSIS_MENU_ITEMS.map((item) => (
<button
key={item.request}
onClick={() => openAnalysisList(item.request)}
className="flex w-full items-center justify-start px-3 py-2 text-sm text-primary transition-colors hover:bg-glass"
>
{item.label}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link
href="/puzzles"
className={`px-2 py-1 transition-all duration-200 hover:!text-primary ${router.pathname.startsWith('/puzzles') ? '!text-primary' : '!text-primary/80'}`}
Expand Down
36 changes: 36 additions & 0 deletions src/constants/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,39 @@ export const ANALYSIS_LEFT_PANEL_TABS = [
{ id: 'moves', name: 'Moves' },
{ id: 'select-game', name: 'Select Game' },
] as const

// The analysis page's game lists, as its game picker names them.
export type AnalysisGameListName =
| 'tournament'
| 'lichess'
| 'play'
| 'hb'
| 'custom'
| 'favorites'

// What the site's Analysis menu can open: one of the lists, or the box for
// pasting a PGN or FEN.
export type AnalysisMenuRequest = AnalysisGameListName | 'import'

export const ANALYSIS_MENU_ITEMS: readonly {
request: AnalysisMenuRequest
label: string
}[] = [
{ request: 'play', label: 'My games vs Maia' },
{ request: 'hb', label: 'My Hand and Brain games' },
{ request: 'lichess', label: 'My Lichess games' },
{ request: 'custom', label: 'My imported games' },
{ request: 'favorites', label: 'Favorites' },
{ request: 'tournament', label: 'World Championship games' },
{ request: 'import', label: 'Paste a PGN or FEN' },
]

export const isAnalysisMenuRequest = (
value: unknown,
): value is AnalysisMenuRequest =>
ANALYSIS_MENU_ITEMS.some((item) => item.request === value)

// On the analysis page the menu sends this event; from any other page it
// leaves the request here and opens the analysis page, which reads it.
export const OPEN_ANALYSIS_LIST_EVENT = 'maia:open-analysis-list' as const
export const OPEN_ANALYSIS_LIST_KEY = 'maia.openAnalysisList' as const
52 changes: 49 additions & 3 deletions src/pages/analysis/[...id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ import {
useTablebase,
} from 'src/hooks'
import { tourConfigs } from 'src/constants/tours'
import { ANALYSIS_LEFT_PANEL_TABS } from 'src/constants/analysis'
import {
ANALYSIS_LEFT_PANEL_TABS,
AnalysisGameListName,
AnalysisMenuRequest,
OPEN_ANALYSIS_LIST_EVENT,
OPEN_ANALYSIS_LIST_KEY,
isAnalysisMenuRequest,
} from 'src/constants/analysis'
import type { DrawBrushes, DrawShape } from 'chessground/draw'
import { MAIA_MODELS } from 'src/constants/common'
import { applyEngineAnalysisData } from 'src/lib/analysis'
Expand Down Expand Up @@ -1408,6 +1415,45 @@ const Analysis: React.FC<Props> = ({
const [desktopLeftPanelTab, setDesktopLeftPanelTab] = useState<
(typeof ANALYSIS_LEFT_PANEL_TABS)[number]['id']
>(ANALYSIS_LEFT_PANEL_TABS[0].id)
const [showGameListMobile, setShowGameListMobile] = useState(false)

// The site's Analysis menu opens one of the game lists (or the box for
// pasting a PGN or FEN) with an event here, or from another page through
// session storage.
const [gameListRequest, setGameListRequest] = useState<{
list: AnalysisGameListName
nonce: number
} | null>(null)
useEffect(() => {
const open = (request: AnalysisMenuRequest) => {
if (request === 'import') {
setShowCustomModal(true)
return
}
setGameListRequest({ list: request, nonce: Date.now() })
if (useMobileStyleAnalysisLayout) {
setShowGameListMobile(true)
} else {
setDesktopLeftPanelTab('select-game')
}
}
const onRequest = (event: Event) => {
const request = (event as CustomEvent<unknown>).detail
if (isAnalysisMenuRequest(request)) open(request)
}
window.addEventListener(OPEN_ANALYSIS_LIST_EVENT, onRequest)

let pending: string | null = null
try {
pending = sessionStorage.getItem(OPEN_ANALYSIS_LIST_KEY)
if (pending) sessionStorage.removeItem(OPEN_ANALYSIS_LIST_KEY)
} catch {
// Storage can be blocked; the menu still works on this page.
}
if (isAnalysisMenuRequest(pending)) open(pending)

return () => window.removeEventListener(OPEN_ANALYSIS_LIST_EVENT, onRequest)
}, [useMobileStyleAnalysisLayout])
const showDesktopMovesAfterNavigation = useCallback(() => {
if (desktopLeftPanelTab !== 'select-game') return

Expand Down Expand Up @@ -1733,6 +1779,7 @@ const Analysis: React.FC<Props> = ({
}
onCustomAnalysis={() => setShowCustomModal(true)}
refreshTrigger={refreshTrigger}
requestedList={gameListRequest}
embedded
/>
</div>
Expand Down Expand Up @@ -1988,8 +2035,6 @@ const Analysis: React.FC<Props> = ({
</motion.div>
)

const [showGameListMobile, setShowGameListMobile] = useState(false)

const mobileLayout = (
<motion.div
variants={containerVariants}
Expand Down Expand Up @@ -2047,6 +2092,7 @@ const Analysis: React.FC<Props> = ({
}}
onGameSelected={() => setShowGameListMobile(false)}
refreshTrigger={refreshTrigger}
requestedList={gameListRequest}
/>
</div>
</div>
Expand Down
Loading