feat: Add mobile scroll to top button and gradient header background
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { Download } from '@mui/icons-material';
|
||||
import {
|
||||
alpha,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Fade,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Typography
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
@@ -26,12 +29,15 @@ const DownloadsMenu: React.FC<DownloadsMenuProps> = ({
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useLanguage();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={onClose}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
elevation: 0,
|
||||
@@ -42,6 +48,8 @@ const DownloadsMenu: React.FC<DownloadsMenuProps> = ({
|
||||
width: 320,
|
||||
maxHeight: '50vh',
|
||||
overflowY: 'auto',
|
||||
bgcolor: !isMobile ? alpha(theme.palette.background.paper, 0.7) : 'background.paper',
|
||||
backdropFilter: !isMobile ? 'blur(10px)' : 'none',
|
||||
'& .MuiAvatar-root': {
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -56,7 +64,7 @@ const DownloadsMenu: React.FC<DownloadsMenuProps> = ({
|
||||
right: 14,
|
||||
width: 10,
|
||||
height: 10,
|
||||
bgcolor: 'background.paper',
|
||||
bgcolor: !isMobile ? alpha(theme.palette.background.paper, 0.7) : 'background.paper',
|
||||
transform: 'translateY(-50%) rotate(45deg)',
|
||||
zIndex: 0,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Help, Settings, Subscriptions, VideoLibrary } from '@mui/icons-material';
|
||||
import {
|
||||
alpha,
|
||||
Fade,
|
||||
Menu,
|
||||
MenuItem
|
||||
MenuItem,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
@@ -18,12 +21,15 @@ const ManageMenu: React.FC<ManageMenuProps> = ({
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useLanguage();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={onClose}
|
||||
disableScrollLock
|
||||
slotProps={{
|
||||
paper: {
|
||||
elevation: 0,
|
||||
@@ -32,6 +38,8 @@ const ManageMenu: React.FC<ManageMenuProps> = ({
|
||||
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
|
||||
mt: 1.5,
|
||||
width: 320,
|
||||
bgcolor: !isMobile ? alpha(theme.palette.background.paper, 0.7) : 'background.paper',
|
||||
backdropFilter: !isMobile ? 'blur(10px)' : 'none',
|
||||
'&:before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
@@ -40,7 +48,7 @@ const ManageMenu: React.FC<ManageMenuProps> = ({
|
||||
right: 14,
|
||||
width: 10,
|
||||
height: 10,
|
||||
bgcolor: 'background.paper',
|
||||
bgcolor: !isMobile ? alpha(theme.palette.background.paper, 0.7) : 'background.paper',
|
||||
transform: 'translateY(-50%) rotate(45deg)',
|
||||
zIndex: 0,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Clear, Search } from '@mui/icons-material';
|
||||
import {
|
||||
alpha,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { FormEvent } from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
@@ -34,6 +37,8 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { visitorMode } = useVisitorMode();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
return (
|
||||
<Box component="form" onSubmit={onSubmit} sx={{ flexGrow: 1, display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
@@ -47,6 +52,12 @@ const SearchInput: React.FC<SearchInputProps> = ({
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
size="small"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: !isMobile ? alpha(theme.palette.background.paper, 0.1) : 'background.paper',
|
||||
backdropFilter: !isMobile ? 'blur(10px)' : 'none',
|
||||
}
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Menu as MenuIcon } from '@mui/icons-material';
|
||||
import { Menu as MenuIcon, VerticalAlignTop } from '@mui/icons-material';
|
||||
import {
|
||||
alpha,
|
||||
AppBar,
|
||||
Box,
|
||||
ClickAwayListener,
|
||||
Fab,
|
||||
IconButton,
|
||||
Slide,
|
||||
Toolbar,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
@@ -11,6 +14,7 @@ import {
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { useThemeContext } from '../../contexts/ThemeContext';
|
||||
import { useVideo } from '../../contexts/VideoContext';
|
||||
import { useVisitorMode } from '../../contexts/VisitorModeContext';
|
||||
import ActionButtons from './ActionButtons';
|
||||
@@ -36,8 +40,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
const [manageAnchorEl, setManageAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState<boolean>(false);
|
||||
const [websiteName, setWebsiteName] = useState('MyTube');
|
||||
const [isScrolled, setIsScrolled] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { mode: themeMode } = useThemeContext();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const { t } = useLanguage();
|
||||
const { visitorMode } = useVisitorMode();
|
||||
@@ -63,6 +69,27 @@ const Header: React.FC<HeaderProps> = ({
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
// Scroll detection - only for mobile
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
setIsScrolled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
setIsScrolled(scrollTop > 50); // Threshold of 50px
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
// Check initial scroll position
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [isMobile]);
|
||||
|
||||
const handleDownloadsClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
@@ -120,12 +147,96 @@ const Header: React.FC<HeaderProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Get background color for gradient
|
||||
const backgroundColor = themeMode === 'dark'
|
||||
? theme.palette.background.default
|
||||
: theme.palette.background.paper;
|
||||
|
||||
// Create gradient from top (0% transparency) to bottom (100% transparency)
|
||||
const gradientBackground = `linear-gradient(to bottom, ${backgroundColor} 0%, ${alpha(backgroundColor, 0)} 100%)`;
|
||||
|
||||
// Desktop background: 30% transparent (70% opacity)
|
||||
const desktopBackgroundColor = !isMobile
|
||||
? alpha(theme.palette.background.paper, 0.7)
|
||||
: 'background.paper';
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClickAwayListener onClickAway={() => setMobileMenuOpen(false)}>
|
||||
<AppBar position="sticky" color="default" elevation={0} sx={{ bgcolor: 'background.paper', borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Toolbar sx={{ flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'stretch' : 'center', py: isMobile ? 1 : 0 }}>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
color="default"
|
||||
elevation={0}
|
||||
sx={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
zIndex: (theme) => theme.zIndex.appBar,
|
||||
bgcolor: (isMobile && isScrolled) ? 'transparent' : desktopBackgroundColor,
|
||||
backgroundImage: (isMobile && isScrolled) ? gradientBackground : 'none',
|
||||
borderBottom: (isMobile && isScrolled) ? 0 : 1,
|
||||
borderColor: 'divider',
|
||||
transition: 'background-color 0.3s ease-in-out, background-image 0.3s ease-in-out, border-bottom 0.3s ease-in-out, backdrop-filter 0.3s ease-in-out, height 0.3s ease-in-out',
|
||||
backdropFilter: (isMobile && isScrolled) ? 'none' : 'blur(10px)',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'stretch' : 'center',
|
||||
py: isMobile ? (isScrolled ? 0.5 : 1) : 0,
|
||||
minHeight: isMobile
|
||||
? (isScrolled ? '40px !important' : undefined)
|
||||
: undefined,
|
||||
transition: 'min-height 0.3s ease-in-out, padding 0.3s ease-in-out',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{(isMobile && isScrolled) ? (
|
||||
// Simplified header when scrolled
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
py: 0,
|
||||
px: 2,
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
'& img': {
|
||||
height: '24px !important',
|
||||
transition: 'height 0.3s ease-in-out',
|
||||
},
|
||||
'& .MuiTypography-h5': {
|
||||
fontSize: '1rem !important',
|
||||
transition: 'font-size 0.3s ease-in-out',
|
||||
},
|
||||
}}>
|
||||
<Logo websiteName={websiteName} onResetSearch={onResetSearch} />
|
||||
</Box>
|
||||
) : (
|
||||
// Full header when at top
|
||||
<>
|
||||
{/* Top Bar for Mobile / Main Bar for Desktop */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: isMobile ? '100%' : 'auto', flexGrow: isMobile ? 0 : 0, mr: isMobile ? 0 : 2 }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: isMobile ? '100%' : 'auto',
|
||||
flexGrow: isMobile ? 0 : 0,
|
||||
mr: isMobile ? 0 : 2,
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
'& img': {
|
||||
transition: 'height 0.3s ease-in-out',
|
||||
},
|
||||
'& .MuiTypography-h5': {
|
||||
transition: 'font-size 0.3s ease-in-out',
|
||||
},
|
||||
}}>
|
||||
<Logo websiteName={websiteName} onResetSearch={onResetSearch} />
|
||||
|
||||
{isMobile && (
|
||||
@@ -199,9 +310,53 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onTagToggle={handleTagToggle}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</ClickAwayListener>
|
||||
{/* Spacer to prevent content from going under fixed header */}
|
||||
<Box
|
||||
sx={{
|
||||
height: () => {
|
||||
// Get the actual toolbar height - it varies based on mobile/desktop and scrolled state
|
||||
if (isMobile && isScrolled) {
|
||||
return '40px'; // Simplified header height on mobile (1/2 of normal)
|
||||
}
|
||||
// Mobile normal header: default Toolbar (64px) + padding (16px) + content ≈ 50px (user adjusted)
|
||||
// Desktop: 64px
|
||||
return isMobile ? '50px' : '64px';
|
||||
},
|
||||
flexShrink: 0,
|
||||
transition: 'height 0.3s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Scroll to top button - mobile only */}
|
||||
<Slide direction="up" in={isMobile && isScrolled} mountOnEnter unmountOnExit>
|
||||
<Fab
|
||||
color="primary"
|
||||
size="small"
|
||||
aria-label="scroll to top"
|
||||
onClick={() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
zIndex: (theme) => theme.zIndex.speedDial,
|
||||
display: { xs: 'flex', md: 'none' },
|
||||
opacity: 0.8,
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VerticalAlignTop />
|
||||
</Fab>
|
||||
</Slide>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
"dev:frontend": "cd frontend && npm run dev",
|
||||
"dev:backend": "cd backend && npm run dev",
|
||||
"install:all": "npm install && cd frontend && npm install && cd ../backend && npm install",
|
||||
"build": "cd frontend && npm run build"
|
||||
"build": "cd frontend && npm run build",
|
||||
"test": "concurrently \"npm run test:backend\" \"npm run test:frontend\"",
|
||||
"test:frontend": "cd frontend && npm run test",
|
||||
"test:backend": "cd backend && npm run test"
|
||||
},
|
||||
"keywords": [
|
||||
"youtube",
|
||||
|
||||
Reference in New Issue
Block a user