|
import React, { useState } from "react"; |
|
import { |
|
AppBar, |
|
Toolbar, |
|
Box, |
|
Link as MuiLink, |
|
IconButton, |
|
Tooltip, |
|
ButtonBase, |
|
Typography, |
|
} from "@mui/material"; |
|
import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; |
|
import OpenInNewIcon from "@mui/icons-material/OpenInNew"; |
|
import LightModeOutlinedIcon from "@mui/icons-material/LightModeOutlined"; |
|
import DarkModeOutlinedIcon from "@mui/icons-material/DarkModeOutlined"; |
|
import { alpha } from "@mui/material/styles"; |
|
import MenuIcon from "@mui/icons-material/Menu"; |
|
import { Menu, MenuItem, useMediaQuery, useTheme } from "@mui/material"; |
|
|
|
const Navigation = ({ onToggleTheme, mode }) => { |
|
const location = useLocation(); |
|
const navigate = useNavigate(); |
|
const [searchParams] = useSearchParams(); |
|
const [anchorEl, setAnchorEl] = useState(null); |
|
const theme = useTheme(); |
|
const isMobile = useMediaQuery(theme.breakpoints.down("md")); |
|
const [hasChanged, setHasChanged] = useState(false); |
|
|
|
const handleThemeToggle = () => { |
|
setHasChanged(true); |
|
onToggleTheme(); |
|
}; |
|
|
|
const iconStyle = { |
|
fontSize: "1.125rem", |
|
...(hasChanged && { |
|
animation: "rotateIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)", |
|
"@keyframes rotateIn": { |
|
"0%": { |
|
opacity: 0, |
|
transform: |
|
mode === "light" |
|
? "rotate(-90deg) scale(0.8)" |
|
: "rotate(90deg) scale(0.8)", |
|
}, |
|
"100%": { |
|
opacity: 1, |
|
transform: "rotate(0) scale(1)", |
|
}, |
|
}, |
|
}), |
|
}; |
|
|
|
|
|
const syncUrlWithParent = (queryString, hash) => { |
|
|
|
const isHFSpace = window.location !== window.parent.location; |
|
if (isHFSpace) { |
|
try { |
|
|
|
const fullPath = `${queryString}${hash ? "#" + hash : ""}`; |
|
window.parent.postMessage( |
|
{ |
|
type: "urlUpdate", |
|
path: fullPath, |
|
}, |
|
"https://huggingface.co" |
|
); |
|
} catch (e) { |
|
console.warn("Unable to sync URL with parent:", e); |
|
} |
|
} |
|
}; |
|
|
|
const linkStyle = (isActive = false) => ({ |
|
textDecoration: "none", |
|
color: isActive ? "text.primary" : "text.secondary", |
|
fontSize: "0.8125rem", |
|
opacity: isActive ? 1 : 0.8, |
|
display: "flex", |
|
alignItems: "center", |
|
gap: 0.5, |
|
paddingBottom: "2px", |
|
cursor: "pointer", |
|
position: "relative", |
|
"&:hover": { |
|
opacity: 1, |
|
color: "text.primary", |
|
}, |
|
"&::after": isActive |
|
? { |
|
content: '""', |
|
position: "absolute", |
|
bottom: "-4px", |
|
left: "0", |
|
width: "100%", |
|
height: "2px", |
|
backgroundColor: (theme) => |
|
alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.3 : 0.2 |
|
), |
|
borderRadius: "2px", |
|
} |
|
: {}, |
|
}); |
|
|
|
const Separator = () => ( |
|
<Box |
|
sx={(theme) => ({ |
|
width: "4px", |
|
height: "4px", |
|
borderRadius: "100%", |
|
backgroundColor: alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.2 : 0.15 |
|
), |
|
})} |
|
/> |
|
); |
|
|
|
const handleNavigation = (path) => (e) => { |
|
e.preventDefault(); |
|
const searchString = searchParams.toString(); |
|
const queryString = searchString ? `?${searchString}` : ""; |
|
const newPath = `${path}${queryString}`; |
|
|
|
|
|
navigate(newPath); |
|
|
|
|
|
if (window.location !== window.parent.location) { |
|
syncUrlWithParent(queryString, newPath); |
|
} |
|
}; |
|
|
|
const handleMenuOpen = (event) => { |
|
setAnchorEl(event.currentTarget); |
|
}; |
|
|
|
const handleMenuClose = () => { |
|
setAnchorEl(null); |
|
}; |
|
|
|
return ( |
|
<AppBar |
|
position="static" |
|
sx={{ |
|
backgroundColor: "transparent", |
|
boxShadow: "none", |
|
width: "100%", |
|
}} |
|
> |
|
<Toolbar sx={{ justifyContent: "center" }}> |
|
{isMobile ? ( |
|
<Box |
|
sx={{ |
|
display: "flex", |
|
width: "100%", |
|
justifyContent: "space-between", |
|
alignItems: "center", |
|
}} |
|
> |
|
<IconButton |
|
onClick={handleMenuOpen} |
|
sx={{ color: "text.secondary" }} |
|
> |
|
<MenuIcon /> |
|
</IconButton> |
|
|
|
<Menu |
|
anchorEl={anchorEl} |
|
open={Boolean(anchorEl)} |
|
onClose={handleMenuClose} |
|
PaperProps={{ |
|
elevation: 3, |
|
sx: { |
|
mt: 1.5, |
|
minWidth: 220, |
|
borderRadius: "12px", |
|
border: (theme) => |
|
`1px solid ${alpha(theme.palette.divider, 0.1)}`, |
|
backgroundColor: (theme) => |
|
theme.palette.mode === "dark" |
|
? alpha(theme.palette.background.paper, 0.8) |
|
: theme.palette.background.paper, |
|
backdropFilter: "blur(20px)", |
|
"& .MuiList-root": { |
|
py: 1, |
|
}, |
|
"& .MuiMenuItem-root": { |
|
px: 2, |
|
py: 1, |
|
fontSize: "0.8125rem", |
|
color: "text.secondary", |
|
transition: "all 0.2s ease-in-out", |
|
position: "relative", |
|
"&:hover": { |
|
backgroundColor: (theme) => |
|
alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.1 : 0.06 |
|
), |
|
color: "text.primary", |
|
}, |
|
"&.Mui-selected": { |
|
backgroundColor: "transparent", |
|
color: "text.primary", |
|
"&::after": { |
|
content: '""', |
|
position: "absolute", |
|
left: "8px", |
|
width: "4px", |
|
height: "100%", |
|
top: "0", |
|
backgroundColor: (theme) => |
|
alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.3 : 0.2 |
|
), |
|
borderRadius: "2px", |
|
}, |
|
"&:hover": { |
|
backgroundColor: (theme) => |
|
alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.1 : 0.06 |
|
), |
|
}, |
|
}, |
|
}, |
|
}, |
|
}} |
|
transformOrigin={{ horizontal: "left", vertical: "top" }} |
|
anchorOrigin={{ horizontal: "left", vertical: "bottom" }} |
|
> |
|
{/* Navigation Section */} |
|
<Box sx={{ px: 2, pb: 1.5, pt: 0.5 }}> |
|
<Typography variant="caption" sx={{ color: "text.disabled" }}> |
|
Navigation |
|
</Typography> |
|
</Box> |
|
<MenuItem |
|
onClick={(e) => { |
|
handleNavigation("/")(e); |
|
handleMenuClose(); |
|
}} |
|
selected={location.pathname === "/"} |
|
> |
|
Leaderboard |
|
</MenuItem> |
|
<MenuItem |
|
onClick={(e) => { |
|
handleNavigation("/add")(e); |
|
handleMenuClose(); |
|
}} |
|
selected={location.pathname === "/add"} |
|
> |
|
Submit model |
|
</MenuItem> |
|
<MenuItem |
|
onClick={(e) => { |
|
handleNavigation("/quote")(e); |
|
handleMenuClose(); |
|
}} |
|
selected={location.pathname === "/quote"} |
|
> |
|
Citations |
|
</MenuItem> |
|
|
|
{/* Separator */} |
|
<Box |
|
sx={{ |
|
my: 1, |
|
borderTop: (theme) => |
|
`1px solid ${alpha(theme.palette.divider, 0.1)}`, |
|
}} |
|
/> |
|
|
|
{/* External Links Section */} |
|
<Box sx={{ px: 2, pb: 1.5 }}> |
|
<Typography variant="caption" sx={{ color: "text.disabled" }}> |
|
External links |
|
</Typography> |
|
</Box> |
|
</Menu> |
|
|
|
<Tooltip |
|
title={ |
|
mode === "light" |
|
? "Switch to dark mode" |
|
: "Switch to light mode" |
|
} |
|
> |
|
<ButtonBase |
|
onClick={handleThemeToggle} |
|
sx={(theme) => ({ |
|
color: "text.secondary", |
|
borderRadius: "100%", |
|
padding: 0, |
|
width: "36px", |
|
height: "36px", |
|
display: "flex", |
|
alignItems: "center", |
|
justifyContent: "center", |
|
transition: "all 0.2s ease-in-out", |
|
"&:hover": { |
|
color: "text.primary", |
|
backgroundColor: alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.1 : 0.06 |
|
), |
|
}, |
|
"&.MuiButtonBase-root": { |
|
overflow: "hidden", |
|
}, |
|
"& .MuiTouchRipple-root": { |
|
color: alpha(theme.palette.text.primary, 0.3), |
|
}, |
|
})} |
|
> |
|
{mode === "light" ? ( |
|
<DarkModeOutlinedIcon sx={iconStyle} /> |
|
) : ( |
|
<LightModeOutlinedIcon sx={iconStyle} /> |
|
)} |
|
</ButtonBase> |
|
</Tooltip> |
|
</Box> |
|
) : ( |
|
// Desktop version |
|
<Box |
|
sx={{ |
|
display: "flex", |
|
gap: 2.5, |
|
alignItems: "center", |
|
padding: "0.5rem 0", |
|
}} |
|
> |
|
{/* Internal navigation */} |
|
<Box sx={{ display: "flex", gap: 2.5, alignItems: "center" }}> |
|
<Box |
|
onClick={handleNavigation("/")} |
|
sx={linkStyle(location.pathname === "/")} |
|
> |
|
Leaderboard |
|
</Box> |
|
<Box |
|
onClick={handleNavigation("/add")} |
|
sx={linkStyle(location.pathname === "/add")} |
|
> |
|
Submit model |
|
</Box> |
|
<Box |
|
onClick={handleNavigation("/quote")} |
|
sx={linkStyle(location.pathname === "/quote")} |
|
> |
|
Citations |
|
</Box> |
|
</Box> |
|
|
|
<Separator /> |
|
|
|
|
|
{/* Dark mode toggle */} |
|
<Tooltip |
|
title={ |
|
mode === "light" |
|
? "Switch to dark mode" |
|
: "Switch to light mode" |
|
} |
|
> |
|
<ButtonBase |
|
onClick={handleThemeToggle} |
|
sx={(theme) => ({ |
|
color: "text.secondary", |
|
borderRadius: "100%", |
|
padding: 0, |
|
width: "36px", |
|
height: "36px", |
|
display: "flex", |
|
alignItems: "center", |
|
justifyContent: "center", |
|
transition: "all 0.2s ease-in-out", |
|
"&:hover": { |
|
color: "text.primary", |
|
backgroundColor: alpha( |
|
theme.palette.text.primary, |
|
theme.palette.mode === "dark" ? 0.1 : 0.06 |
|
), |
|
}, |
|
"&.MuiButtonBase-root": { |
|
overflow: "hidden", |
|
}, |
|
"& .MuiTouchRipple-root": { |
|
color: alpha(theme.palette.text.primary, 0.3), |
|
}, |
|
})} |
|
> |
|
{mode === "light" ? ( |
|
<DarkModeOutlinedIcon sx={iconStyle} /> |
|
) : ( |
|
<LightModeOutlinedIcon sx={iconStyle} /> |
|
)} |
|
</ButtonBase> |
|
</Tooltip> |
|
</Box> |
|
)} |
|
</Toolbar> |
|
</AppBar> |
|
); |
|
}; |
|
|
|
export default Navigation; |
|
|