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 && (