Revert "Merge branch 'feat-pwa-support' into main"
This reverts commit90a4892a18, reversing changes made tobd1f7e469e.
This commit is contained in:
parent
90a4892a18
commit
0c56c9e816
|
|
@ -3,24 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
<!-- PWA Manifest -->
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
|
|
||||||
<!-- Primary Meta Tags -->
|
|
||||||
<meta name="description" content="AI-powered document management with OCR and intelligent search" />
|
|
||||||
<meta name="theme-color" content="#6366f1" />
|
|
||||||
|
|
||||||
<!-- iOS PWA Meta Tags -->
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Readur" />
|
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
||||||
|
|
||||||
<!-- Android PWA Meta Tags -->
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,6 @@
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
"vite-plugin-pwa": "^1.1.0",
|
"vitest": "^0.28.0"
|
||||||
"vitest": "^0.28.0",
|
|
||||||
"workbox-window": "^7.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB |
|
|
@ -1,77 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Readur - Document Intelligence Platform",
|
|
||||||
"short_name": "Readur",
|
|
||||||
"description": "AI-powered document management with OCR and intelligent search",
|
|
||||||
"start_url": "/",
|
|
||||||
"scope": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"theme_color": "#6366f1",
|
|
||||||
"orientation": "portrait-primary",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/readur-32.png",
|
|
||||||
"sizes": "32x32",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/readur-64.png",
|
|
||||||
"sizes": "64x64",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-192-maskable.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-512-maskable.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": ["productivity", "utilities", "business"],
|
|
||||||
"shortcuts": [
|
|
||||||
{
|
|
||||||
"name": "Upload Document",
|
|
||||||
"short_name": "Upload",
|
|
||||||
"description": "Upload a new document",
|
|
||||||
"url": "/upload",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-192.png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Search Documents",
|
|
||||||
"short_name": "Search",
|
|
||||||
"description": "Search your documents",
|
|
||||||
"url": "/search",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/icons/icon-192.png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"screenshots": [],
|
|
||||||
"display_override": ["standalone", "minimal-ui"],
|
|
||||||
"prefer_related_applications": false
|
|
||||||
}
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
||||||
<title>Offline - Readur</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: #fff;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: env(safe-area-inset-top, 20px) env(safe-area-inset-right, 20px) env(safe-area-inset-bottom, 20px) env(safe-area-inset-left, 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
text-align: center;
|
|
||||||
max-width: 500px;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
margin: 0 auto 30px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon svg {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
stroke: #fff;
|
|
||||||
stroke-width: 2;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-button {
|
|
||||||
background: #fff;
|
|
||||||
color: #667eea;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px 32px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.retry-button:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon svg {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="icon">
|
|
||||||
<svg viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.288 15.038a5.25 5.25 0 017.424 0M5.106 11.856c3.807-3.808 9.98-3.808 13.788 0M1.924 8.674c5.565-5.565 14.587-5.565 20.152 0M12.53 18.22l-.53.53-.53-.53a.75.75 0 011.06 0z" />
|
|
||||||
<line x1="2" y1="2" x2="22" y2="22" stroke-linecap="round" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>You're Offline</h1>
|
|
||||||
<p>It looks like you've lost your internet connection. Don't worry, Readur will be back once you're online again.</p>
|
|
||||||
|
|
||||||
<div class="status">
|
|
||||||
<p class="status-text" id="status">Checking connection...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="retry-button" onclick="window.location.reload()">
|
|
||||||
Try Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function updateConnectionStatus() {
|
|
||||||
const statusEl = document.getElementById('status');
|
|
||||||
if (navigator.onLine) {
|
|
||||||
statusEl.textContent = 'Connection restored! Click "Try Again" to continue.';
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = 'No internet connection detected.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status immediately
|
|
||||||
updateConnectionStatus();
|
|
||||||
|
|
||||||
// Listen for online/offline events
|
|
||||||
window.addEventListener('online', updateConnectionStatus);
|
|
||||||
window.addEventListener('offline', updateConnectionStatus);
|
|
||||||
|
|
||||||
// Auto-reload when connection is restored
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Alert,
|
Alert,
|
||||||
Paper,
|
Paper,
|
||||||
IconButton,
|
|
||||||
useTheme,
|
|
||||||
useMediaQuery,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
|
||||||
ZoomIn as ZoomInIcon,
|
|
||||||
ZoomOut as ZoomOutIcon,
|
|
||||||
RestartAlt as ResetIcon,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { documentService } from '../services/api';
|
import { documentService } from '../services/api';
|
||||||
|
|
||||||
interface DocumentViewerProps {
|
interface DocumentViewerProps {
|
||||||
|
|
@ -63,7 +55,29 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||||
|
|
||||||
// Handle images
|
// Handle images
|
||||||
if (mimeType.startsWith('image/')) {
|
if (mimeType.startsWith('image/')) {
|
||||||
return <ImageViewer documentUrl={documentUrl} filename={filename} />;
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '60vh',
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={documentUrl}
|
||||||
|
alt={filename}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle PDFs
|
// Handle PDFs
|
||||||
|
|
@ -138,200 +152,6 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Component for viewing images with touch gestures
|
|
||||||
const ImageViewer: React.FC<{ documentUrl: string; filename: string }> = ({
|
|
||||||
documentUrl,
|
|
||||||
filename,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const [scale, setScale] = useState(1);
|
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
||||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const imageRef = useRef<HTMLImageElement>(null);
|
|
||||||
const lastTouchDistanceRef = useRef<number | null>(null);
|
|
||||||
const lastTapTimeRef = useRef<number>(0);
|
|
||||||
|
|
||||||
// Reset zoom and position
|
|
||||||
const handleReset = () => {
|
|
||||||
setScale(1);
|
|
||||||
setPosition({ x: 0, y: 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zoom in
|
|
||||||
const handleZoomIn = () => {
|
|
||||||
setScale((prev) => Math.min(prev + 0.5, 5));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zoom out
|
|
||||||
const handleZoomOut = () => {
|
|
||||||
setScale((prev) => Math.max(prev - 0.5, 0.5));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle double tap to zoom
|
|
||||||
const handleDoubleClick = (e: React.MouseEvent) => {
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastTap = now - lastTapTimeRef.current;
|
|
||||||
|
|
||||||
if (timeSinceLastTap < 300) {
|
|
||||||
// Double tap detected
|
|
||||||
if (scale === 1) {
|
|
||||||
setScale(2);
|
|
||||||
} else {
|
|
||||||
handleReset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastTapTimeRef.current = now;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle pinch-to-zoom
|
|
||||||
const handleTouchStart = (e: React.TouchEvent) => {
|
|
||||||
if (e.touches.length === 2) {
|
|
||||||
const distance = Math.hypot(
|
|
||||||
e.touches[0].clientX - e.touches[1].clientX,
|
|
||||||
e.touches[0].clientY - e.touches[1].clientY
|
|
||||||
);
|
|
||||||
lastTouchDistanceRef.current = distance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchMove = (e: React.TouchEvent) => {
|
|
||||||
if (e.touches.length === 2 && lastTouchDistanceRef.current) {
|
|
||||||
e.preventDefault();
|
|
||||||
const distance = Math.hypot(
|
|
||||||
e.touches[0].clientX - e.touches[1].clientX,
|
|
||||||
e.touches[0].clientY - e.touches[1].clientY
|
|
||||||
);
|
|
||||||
const scaleDelta = distance / lastTouchDistanceRef.current;
|
|
||||||
setScale((prev) => Math.max(0.5, Math.min(5, prev * scaleDelta)));
|
|
||||||
lastTouchDistanceRef.current = distance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
|
||||||
lastTouchDistanceRef.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle wheel zoom
|
|
||||||
const handleWheel = (e: React.WheelEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
||||||
setScale((prev) => Math.max(0.5, Math.min(5, prev + delta)));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ position: 'relative', width: '100%', minHeight: '60vh' }}>
|
|
||||||
{/* Zoom Controls */}
|
|
||||||
{isMobile && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 16,
|
|
||||||
right: 16,
|
|
||||||
zIndex: 10,
|
|
||||||
display: 'flex',
|
|
||||||
gap: 1,
|
|
||||||
background: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
borderRadius: 2,
|
|
||||||
padding: '4px',
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={handleZoomOut}
|
|
||||||
disabled={scale <= 0.5}
|
|
||||||
sx={{
|
|
||||||
color: 'white',
|
|
||||||
'&:disabled': { color: 'rgba(255,255,255,0.3)' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ZoomOutIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={handleReset}
|
|
||||||
sx={{ color: 'white' }}
|
|
||||||
>
|
|
||||||
<ResetIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={handleZoomIn}
|
|
||||||
disabled={scale >= 5}
|
|
||||||
sx={{
|
|
||||||
color: 'white',
|
|
||||||
'&:disabled': { color: 'rgba(255,255,255,0.3)' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ZoomInIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Image Container */}
|
|
||||||
<Box
|
|
||||||
ref={imageContainerRef}
|
|
||||||
onClick={handleDoubleClick}
|
|
||||||
onTouchStart={handleTouchStart}
|
|
||||||
onTouchMove={handleTouchMove}
|
|
||||||
onTouchEnd={handleTouchEnd}
|
|
||||||
onWheel={handleWheel}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
minHeight: '60vh',
|
|
||||||
p: 2,
|
|
||||||
overflow: 'auto',
|
|
||||||
cursor: scale > 1 ? 'move' : 'zoom-in',
|
|
||||||
touchAction: 'none',
|
|
||||||
userSelect: 'none',
|
|
||||||
WebkitUserSelect: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
ref={imageRef}
|
|
||||||
src={documentUrl}
|
|
||||||
alt={filename}
|
|
||||||
draggable={false}
|
|
||||||
style={{
|
|
||||||
maxWidth: scale === 1 ? '100%' : 'none',
|
|
||||||
maxHeight: scale === 1 ? '100%' : 'none',
|
|
||||||
objectFit: 'contain',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
|
||||||
transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)`,
|
|
||||||
transition: scale === 1 ? 'transform 0.3s ease-out' : 'none',
|
|
||||||
transformOrigin: 'center center',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Zoom Indicator */}
|
|
||||||
{isMobile && scale !== 1 && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 16,
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
background: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
color: 'white',
|
|
||||||
padding: '6px 16px',
|
|
||||||
borderRadius: 2,
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
fontWeight: 500,
|
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Math.round(scale * 100)}%
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Component for viewing text files
|
// Component for viewing text files
|
||||||
const TextFileViewer: React.FC<{ documentUrl: string; filename: string }> = ({
|
const TextFileViewer: React.FC<{ documentUrl: string; filename: string }> = ({
|
||||||
documentUrl,
|
documentUrl,
|
||||||
|
|
|
||||||
|
|
@ -367,8 +367,8 @@ const GlobalSearchBar: React.FC<GlobalSearchBarProps> = ({ sx, ...props }) => {
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minWidth: { xs: 0, sm: 400, md: 600 },
|
minWidth: 600,
|
||||||
maxWidth: { xs: '100%', sm: 600, md: 800, lg: 1200 },
|
maxWidth: 1200,
|
||||||
'& .MuiOutlinedInput-root': {
|
'& .MuiOutlinedInput-root': {
|
||||||
background: theme.palette.mode === 'light'
|
background: theme.palette.mode === 'light'
|
||||||
? 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.90) 100%)'
|
? 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.90) 100%)'
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ import GlobalSearchBar from '../GlobalSearchBar';
|
||||||
import ThemeToggle from '../ThemeToggle/ThemeToggle';
|
import ThemeToggle from '../ThemeToggle/ThemeToggle';
|
||||||
import NotificationPanel from '../Notifications/NotificationPanel';
|
import NotificationPanel from '../Notifications/NotificationPanel';
|
||||||
import LanguageSwitcher from '../LanguageSwitcher';
|
import LanguageSwitcher from '../LanguageSwitcher';
|
||||||
import BottomNavigation from './BottomNavigation';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const drawerWidth = 280;
|
const drawerWidth = 280;
|
||||||
|
|
@ -422,15 +421,9 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
||||||
boxShadow: theme.palette.mode === 'light'
|
boxShadow: theme.palette.mode === 'light'
|
||||||
? '0 4px 32px rgba(0,0,0,0.04)'
|
? '0 4px 32px rgba(0,0,0,0.04)'
|
||||||
: '0 4px 32px rgba(0,0,0,0.2)',
|
: '0 4px 32px rgba(0,0,0,0.2)',
|
||||||
// iOS safe area support
|
|
||||||
paddingTop: 'env(safe-area-inset-top, 0px)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar sx={{
|
<Toolbar>
|
||||||
minHeight: { xs: '56px', sm: '64px' },
|
|
||||||
paddingLeft: { xs: '8px', sm: '16px' },
|
|
||||||
paddingRight: { xs: '8px', sm: '16px' },
|
|
||||||
}}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="open drawer"
|
aria-label="open drawer"
|
||||||
|
|
@ -458,15 +451,8 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
||||||
: t('navigation.dashboard')}
|
: t('navigation.dashboard')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Global Search Bar - Hidden on mobile, use search page instead */}
|
{/* Global Search Bar */}
|
||||||
<Box sx={{
|
<Box sx={{ flexGrow: 2, display: 'flex', justifyContent: 'center', mx: 1, flex: '1 1 auto' }}>
|
||||||
flexGrow: 2,
|
|
||||||
display: { xs: 'none', md: 'flex' },
|
|
||||||
justifyContent: 'center',
|
|
||||||
mx: 1,
|
|
||||||
flex: '1 1 auto',
|
|
||||||
minWidth: 0 // Allow flex item to shrink below content size
|
|
||||||
}}>
|
|
||||||
<GlobalSearchBar />
|
<GlobalSearchBar />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -671,37 +657,18 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
||||||
width: { md: `calc(100% - ${drawerWidth}px)` },
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
backgroundColor: 'background.default',
|
backgroundColor: 'background.default',
|
||||||
// Add padding for bottom navigation on mobile
|
|
||||||
paddingBottom: {
|
|
||||||
xs: 'calc(64px + env(safe-area-inset-bottom, 0px))',
|
|
||||||
md: 0,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Box sx={{
|
<Box sx={{ p: 3 }}>
|
||||||
p: { xs: 2, sm: 3 },
|
|
||||||
// iOS safe area support for notched devices
|
|
||||||
paddingLeft: {
|
|
||||||
xs: 'max(16px, env(safe-area-inset-left, 0px))',
|
|
||||||
sm: 'max(24px, env(safe-area-inset-left, 0px))',
|
|
||||||
},
|
|
||||||
paddingRight: {
|
|
||||||
xs: 'max(16px, env(safe-area-inset-right, 0px))',
|
|
||||||
sm: 'max(24px, env(safe-area-inset-right, 0px))',
|
|
||||||
},
|
|
||||||
}}>
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Bottom Navigation (Mobile Only) */}
|
|
||||||
<BottomNavigation />
|
|
||||||
|
|
||||||
{/* Notification Panel */}
|
{/* Notification Panel */}
|
||||||
<NotificationPanel
|
<NotificationPanel
|
||||||
anchorEl={notificationAnchorEl}
|
anchorEl={notificationAnchorEl}
|
||||||
onClose={handleNotificationClose}
|
onClose={handleNotificationClose}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
BottomNavigation as MuiBottomNavigation,
|
|
||||||
BottomNavigationAction,
|
|
||||||
Paper,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import {
|
|
||||||
Dashboard as DashboardIcon,
|
|
||||||
CloudUpload as UploadIcon,
|
|
||||||
Search as SearchIcon,
|
|
||||||
Settings as SettingsIcon,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const BottomNavigation: React.FC = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// Map paths to nav values
|
|
||||||
const getNavValue = (pathname: string): string => {
|
|
||||||
if (pathname === '/dashboard') return 'dashboard';
|
|
||||||
if (pathname === '/upload') return 'upload';
|
|
||||||
if (pathname === '/search' || pathname === '/documents') return 'search';
|
|
||||||
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 'search':
|
|
||||||
navigate('/documents');
|
|
||||||
break;
|
|
||||||
case 'settings':
|
|
||||||
navigate('/settings');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 1100,
|
|
||||||
display: { xs: 'block', md: 'none' },
|
|
||||||
background: theme.palette.mode === 'light'
|
|
||||||
? 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(248,250,252,0.98) 100%)'
|
|
||||||
: 'linear-gradient(180deg, rgba(30,30,30,0.98) 0%, rgba(18,18,18,0.98) 100%)',
|
|
||||||
backdropFilter: 'blur(20px)',
|
|
||||||
borderTop: theme.palette.mode === 'light'
|
|
||||||
? '1px solid rgba(226,232,240,0.5)'
|
|
||||||
: '1px solid rgba(255,255,255,0.1)',
|
|
||||||
boxShadow: theme.palette.mode === 'light'
|
|
||||||
? '0 -4px 32px rgba(0,0,0,0.08)'
|
|
||||||
: '0 -4px 32px rgba(0,0,0,0.3)',
|
|
||||||
// iOS safe area support
|
|
||||||
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
||||||
}}
|
|
||||||
elevation={0}
|
|
||||||
>
|
|
||||||
<MuiBottomNavigation
|
|
||||||
value={getNavValue(location.pathname)}
|
|
||||||
onChange={handleNavigation}
|
|
||||||
sx={{
|
|
||||||
background: 'transparent',
|
|
||||||
height: '64px',
|
|
||||||
'& .MuiBottomNavigationAction-root': {
|
|
||||||
color: 'text.secondary',
|
|
||||||
minWidth: 'auto',
|
|
||||||
padding: '8px 12px',
|
|
||||||
gap: '4px',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
'& .MuiBottomNavigationAction-label': {
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
fontWeight: 500,
|
|
||||||
letterSpacing: '0.025em',
|
|
||||||
marginTop: '4px',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'& .MuiSvgIcon-root': {
|
|
||||||
fontSize: '1.5rem',
|
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
},
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: '#6366f1',
|
|
||||||
'& .MuiSvgIcon-root': {
|
|
||||||
transform: 'scale(1.1)',
|
|
||||||
filter: 'drop-shadow(0 2px 8px rgba(99,102,241,0.3))',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// iOS-style touch feedback
|
|
||||||
'@media (pointer: coarse)': {
|
|
||||||
minHeight: '56px',
|
|
||||||
'&:active': {
|
|
||||||
transform: 'scale(0.95)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label={t('navigation.dashboard')}
|
|
||||||
value="dashboard"
|
|
||||||
icon={<DashboardIcon />}
|
|
||||||
sx={{
|
|
||||||
'&.Mui-selected': {
|
|
||||||
'& .MuiBottomNavigationAction-label': {
|
|
||||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
||||||
backgroundClip: 'text',
|
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
WebkitTextFillColor: 'transparent',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label={t('navigation.upload')}
|
|
||||||
value="upload"
|
|
||||||
icon={<UploadIcon />}
|
|
||||||
sx={{
|
|
||||||
'&.Mui-selected': {
|
|
||||||
'& .MuiBottomNavigationAction-label': {
|
|
||||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
||||||
backgroundClip: 'text',
|
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
WebkitTextFillColor: 'transparent',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label={t('navigation.documents')}
|
|
||||||
value="search"
|
|
||||||
icon={<SearchIcon />}
|
|
||||||
sx={{
|
|
||||||
'&.Mui-selected': {
|
|
||||||
'& .MuiBottomNavigationAction-label': {
|
|
||||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
||||||
backgroundClip: 'text',
|
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
WebkitTextFillColor: 'transparent',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BottomNavigationAction
|
|
||||||
label={t('settings.title')}
|
|
||||||
value="settings"
|
|
||||||
icon={<SettingsIcon />}
|
|
||||||
sx={{
|
|
||||||
'&.Mui-selected': {
|
|
||||||
'& .MuiBottomNavigationAction-label': {
|
|
||||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
|
||||||
backgroundClip: 'text',
|
|
||||||
WebkitBackgroundClip: 'text',
|
|
||||||
WebkitTextFillColor: 'transparent',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</MuiBottomNavigation>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BottomNavigation;
|
|
||||||
|
|
@ -2,97 +2,6 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* ============================================
|
|
||||||
PWA & iOS Safe Area Support
|
|
||||||
============================================ */
|
|
||||||
|
|
||||||
/* Ensure the entire app respects iOS safe areas */
|
|
||||||
:root {
|
|
||||||
/* Define safe area insets for iOS devices */
|
|
||||||
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
|
||||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
|
||||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
|
||||||
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* PWA-specific styles */
|
|
||||||
@media all and (display-mode: standalone) {
|
|
||||||
/* Remove system tap highlight on iOS PWA */
|
|
||||||
* {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent pull-to-refresh on iOS */
|
|
||||||
body {
|
|
||||||
overscroll-behavior-y: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improve iOS PWA scrolling performance */
|
|
||||||
html {
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS-specific optimizations */
|
|
||||||
@supports (-webkit-touch-callout: none) {
|
|
||||||
/* Disable callout on iOS when long-pressing */
|
|
||||||
* {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Allow callout on text content */
|
|
||||||
p, span, div[contenteditable="true"] {
|
|
||||||
-webkit-touch-callout: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth momentum scrolling on iOS */
|
|
||||||
.scrollable-content {
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix iOS input zoom issue */
|
|
||||||
input, textarea, select {
|
|
||||||
font-size: 16px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Touch-optimized tap targets (minimum 44x44px for iOS) */
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
button,
|
|
||||||
a,
|
|
||||||
.MuiIconButton-root,
|
|
||||||
.MuiButton-root,
|
|
||||||
.MuiChip-root.MuiChip-clickable {
|
|
||||||
min-height: 44px;
|
|
||||||
min-width: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bottom navigation touch targets */
|
|
||||||
.MuiBottomNavigationAction-root {
|
|
||||||
min-height: 56px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prevent iOS double-tap zoom on buttons */
|
|
||||||
button,
|
|
||||||
input[type="button"],
|
|
||||||
input[type="submit"] {
|
|
||||||
touch-action: manipulation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS status bar color adaptation */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
html {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced search responsiveness styles */
|
/* Enhanced search responsiveness styles */
|
||||||
.search-input-responsive {
|
.search-input-responsive {
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
|
||||||
|
|
||||||
// Support environment variables for development
|
// Support environment variables for development
|
||||||
const BACKEND_PORT = process.env.BACKEND_PORT || '8000'
|
const BACKEND_PORT = process.env.BACKEND_PORT || '8000'
|
||||||
|
|
@ -9,113 +8,7 @@ const CLIENT_PORT = process.env.CLIENT_PORT || '5173'
|
||||||
const PROXY_TARGET = process.env.VITE_API_PROXY_TARGET || `http://localhost:${BACKEND_PORT}`
|
const PROXY_TARGET = process.env.VITE_API_PROXY_TARGET || `http://localhost:${BACKEND_PORT}`
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react()],
|
||||||
react(),
|
|
||||||
VitePWA({
|
|
||||||
registerType: 'autoUpdate',
|
|
||||||
includeAssets: ['favicon.ico', 'readur-32.png', 'readur-64.png', 'icons/*.png', 'offline.html'],
|
|
||||||
manifest: {
|
|
||||||
name: 'Readur - Document Intelligence Platform',
|
|
||||||
short_name: 'Readur',
|
|
||||||
description: 'AI-powered document management with OCR and intelligent search',
|
|
||||||
theme_color: '#6366f1',
|
|
||||||
background_color: '#ffffff',
|
|
||||||
display: 'standalone',
|
|
||||||
orientation: 'portrait-primary',
|
|
||||||
scope: '/',
|
|
||||||
start_url: '/',
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: '/readur-32.png',
|
|
||||||
sizes: '32x32',
|
|
||||||
type: 'image/png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/readur-64.png',
|
|
||||||
sizes: '64x64',
|
|
||||||
type: 'image/png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/icons/icon-192.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'any'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/icons/icon-512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'any'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/icons/icon-192-maskable.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'maskable'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/icons/icon-512-maskable.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'maskable'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
workbox: {
|
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
|
||||||
globIgnores: ['**/readur.png'],
|
|
||||||
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024, // 3 MB limit
|
|
||||||
// Exclude auth routes from navigation fallback and caching
|
|
||||||
navigateFallbackDenylist: [/^\/api\/auth\//],
|
|
||||||
runtimeCaching: [
|
|
||||||
{
|
|
||||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
|
|
||||||
handler: 'CacheFirst',
|
|
||||||
options: {
|
|
||||||
cacheName: 'google-fonts-cache',
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 10,
|
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year
|
|
||||||
},
|
|
||||||
cacheableResponse: {
|
|
||||||
statuses: [0, 200]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
|
|
||||||
handler: 'CacheFirst',
|
|
||||||
options: {
|
|
||||||
cacheName: 'gstatic-fonts-cache',
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 10,
|
|
||||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year
|
|
||||||
},
|
|
||||||
cacheableResponse: {
|
|
||||||
statuses: [0, 200]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Only cache non-auth API routes
|
|
||||||
urlPattern: /^\/api\/(?!auth\/).*/,
|
|
||||||
handler: 'NetworkFirst',
|
|
||||||
options: {
|
|
||||||
cacheName: 'api-cache',
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 100,
|
|
||||||
maxAgeSeconds: 60 * 5 // 5 minutes
|
|
||||||
},
|
|
||||||
networkTimeoutSeconds: 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
devOptions: {
|
|
||||||
enabled: false // Disable PWA in dev mode for faster development
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['src/test/setup.ts'],
|
setupFiles: ['src/test/setup.ts'],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue