From 18072b615b67e07a51352e6c36f4fa1768960a42 Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Fri, 12 Dec 2025 01:04:44 +0000 Subject: [PATCH] feat(pwa): make settings scrollable, scaling improvements --- .../components/Layout/BottomNavigation.tsx | 192 ++++++++++++++++++ frontend/src/hooks/usePWA.ts | 31 +++ frontend/src/pages/SettingsPage.tsx | 31 ++- 3 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/Layout/BottomNavigation.tsx create mode 100644 frontend/src/hooks/usePWA.ts diff --git a/frontend/src/components/Layout/BottomNavigation.tsx b/frontend/src/components/Layout/BottomNavigation.tsx new file mode 100644 index 0000000..f96a5c6 --- /dev/null +++ b/frontend/src/components/Layout/BottomNavigation.tsx @@ -0,0 +1,192 @@ +import React from 'react'; +import { + BottomNavigation as MuiBottomNavigation, + BottomNavigationAction, + Paper, + useTheme, +} from '@mui/material'; +import { + Dashboard as DashboardIcon, + CloudUpload as UploadIcon, + Label as LabelIcon, + Settings as SettingsIcon, +} from '@mui/icons-material'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { usePWA } from '../../hooks/usePWA'; + +const BottomNavigation: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const theme = useTheme(); + const { t } = useTranslation(); + const isPWA = usePWA(); + + // Don't render if not in PWA mode + if (!isPWA) { + return null; + } + + // Map paths to nav values + const getNavValue = (pathname: string): string => { + if (pathname === '/dashboard') return 'dashboard'; + if (pathname === '/upload') return 'upload'; + if (pathname === '/labels') return 'labels'; + if (pathname === '/settings' || pathname === '/profile') return 'settings'; + return 'dashboard'; + }; + + const handleNavigation = (_event: React.SyntheticEvent, newValue: string) => { + switch (newValue) { + case 'dashboard': + navigate('/dashboard'); + break; + case 'upload': + navigate('/upload'); + break; + case 'labels': + navigate('/labels'); + break; + case 'settings': + navigate('/settings'); + break; + } + }; + + return ( + + + } + sx={{ + '&.Mui-selected': { + '& .MuiBottomNavigationAction-label': { + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + backgroundClip: 'text', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + fontWeight: 600, + }, + }, + }} + /> + } + sx={{ + '&.Mui-selected': { + '& .MuiBottomNavigationAction-label': { + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + backgroundClip: 'text', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + fontWeight: 600, + }, + }, + }} + /> + } + sx={{ + '&.Mui-selected': { + '& .MuiBottomNavigationAction-label': { + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + backgroundClip: 'text', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + fontWeight: 600, + }, + }, + }} + /> + } + sx={{ + '&.Mui-selected': { + '& .MuiBottomNavigationAction-label': { + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + backgroundClip: 'text', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + fontWeight: 600, + }, + }, + }} + /> + + + ); +}; + +export default BottomNavigation; diff --git a/frontend/src/hooks/usePWA.ts b/frontend/src/hooks/usePWA.ts new file mode 100644 index 0000000..64bdec7 --- /dev/null +++ b/frontend/src/hooks/usePWA.ts @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; + +/** + * Hook to detect if the app is running in PWA/standalone mode + * @returns boolean indicating if running as installed PWA + */ +export const usePWA = (): boolean => { + const [isPWA, setIsPWA] = useState(false); + + useEffect(() => { + const checkPWAMode = () => { + // Check if running in standalone mode (installed PWA) + const isStandalone = window.matchMedia('(display-mode: standalone)').matches; + // iOS Safari specific check + const isIOSStandalone = (window.navigator as any).standalone === true; + + setIsPWA(isStandalone || isIOSStandalone); + }; + + checkPWAMode(); + + // Listen for display mode changes + const mediaQuery = window.matchMedia('(display-mode: standalone)'); + const handleChange = () => checkPWAMode(); + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + return isPWA; +}; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index ace59f9..8ddbd41 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -47,6 +47,7 @@ import { useAuth } from '../contexts/AuthContext'; import api, { queueService, ErrorHelper, ErrorCodes, userWatchService, UserWatchDirectoryResponse } from '../services/api'; import OcrLanguageSelector from '../components/OcrLanguageSelector'; import LanguageSelector from '../components/LanguageSelector'; +import { usePWA } from '../hooks/usePWA'; import { useTranslation } from 'react-i18next'; interface User { @@ -194,6 +195,7 @@ function useDebounce any>(func: T, delay: number): const SettingsPage: React.FC = () => { const { t } = useTranslation(); const { user: currentUser } = useAuth(); + const isPWA = usePWA(); const [tabValue, setTabValue] = useState(0); const [settings, setSettings] = useState({ ocrLanguage: 'eng', @@ -837,20 +839,41 @@ const SettingsPage: React.FC = () => { }; return ( - - + + {t('settings.title')} - + - + {tabValue === 0 && (