Skip to content

Commit

Permalink
Enhance offline support with error handling in Navbar; update meta ta…
Browse files Browse the repository at this point in the history
…gs and manifest for improved app description and theme color; add offline HTML page and service worker for caching
  • Loading branch information
chintan992 authored Dec 28, 2024
1 parent 1f12292 commit 0395fd6
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 15 deletions.
9 changes: 5 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f39c12" />
<meta name="description" content="A modern video streaming application" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Stream your favorite movies and TV shows" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="LetsStream" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
Expand Down
21 changes: 14 additions & 7 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"short_name": "Let's Stream",
"name": "Let's Stream - Video Player",
"short_name": "LetsStream",
"name": "LetsStream - Watch Movies & TV Shows",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"sizes": "64x64",
"type": "image/x-icon"
},
{
Expand All @@ -22,11 +22,11 @@
],
"start_url": ".",
"display": "standalone",
"theme_color": "#10b981",
"background_color": "#ecf0f1",
"theme_color": "#000000",
"background_color": "#ffffff",
"orientation": "any",
"categories": ["entertainment", "video"],
"description": "A modern video streaming application",
"description": "Stream your favorite movies and TV shows",
"dir": "ltr",
"lang": "en",
"prefer_related_applications": false,
Expand All @@ -45,5 +45,12 @@
}
]
}
}
},
"shortcuts": [
{
"name": "Search",
"url": "/search",
"description": "Search for movies and TV shows"
}
]
}
31 changes: 31 additions & 0 deletions public/offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - LetsStream</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #f5f6f7;
}
.offline-content {
text-align: center;
padding: 2rem;
}
h1 { color: #374151; }
p { color: #6b7280; }
</style>
</head>
<body>
<div class="offline-content">
<h1>You're Offline</h1>
<p>Please check your internet connection and try again.</p>
</div>
</body>
</html>
115 changes: 115 additions & 0 deletions public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-undef */

// Declare expected global for TypeScript
/// <reference lib="webworker" />

importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');

// Ensure workbox is available
if (typeof workbox === 'undefined') {
// eslint-disable-next-line no-console
console.error('Workbox could not be loaded. Offline support is disabled.');
} else {
workbox.core.setCacheNameDetails({
prefix: 'letsstream',
suffix: 'v1',
precache: 'precache',
runtime: 'runtime'
});

// Pre-cache important routes and assets
workbox.precaching.precacheAndRoute([
{ url: '/', revision: '1.0' },
{ url: '/index.html', revision: '1.0' },
{ url: '/manifest.json', revision: '1.0' },
{ url: '/favicon.ico', revision: '1.0' },
{ url: '/logo192.png', revision: '1.0' },
{ url: '/logo512.png', revision: '1.0' }
]);

// Cache page navigations
workbox.routing.registerRoute(
({ request }) => request.mode === 'navigate',
new workbox.strategies.NetworkFirst({
cacheName: 'pages-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 24 * 60 * 60 // 24 hours
})
]
})
);

// Cache TMDB API responses
workbox.routing.registerRoute(
/^https:\/\/api\.themoviedb\.org\/3\//,
new workbox.strategies.NetworkFirst({
cacheName: 'tmdb-api-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 60 * 60 // 1 hour
}),
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200]
})
]
})
);

// Cache TMDB images
workbox.routing.registerRoute(
/^https:\/\/image\.tmdb\.org\//,
new workbox.strategies.CacheFirst({
cacheName: 'tmdb-images-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 200,
maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days
}),
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200]
})
]
})
);

// Cache static assets (JS, CSS, etc)
workbox.routing.registerRoute(
/\.(?:js|css|woff2?)$/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'static-resources'
})
);

// Handle offline fallback
workbox.routing.setCatchHandler(({ event }) => {
switch (event.request.destination) {
case 'document':
return workbox.precaching.matchPrecache('/offline.html');
case 'image':
return new Response(
`<svg role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg">
<title id="offline-title">Offline</title>
<rect width="100%" height="100%" fill="#f5f6f7"/>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui">
Offline
</text>
</svg>`,
{ headers: { 'Content-Type': 'image/svg+xml' } }
);
default:
return Response.error();
}
});
}

// Skip waiting and claim clients
addEventListener('install', (event) => {
event.waitUntil(skipWaiting());
});

addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
80 changes: 76 additions & 4 deletions src/components/Navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,31 @@ const Navbar = () => {
const [suggestions, setSuggestions] = useState([]);
const [selectedSuggestion, setSelectedSuggestion] = useState(-1);
const [isLoading, setIsLoading] = useState(false);
const [isOffline, setIsOffline] = useState(!navigator.onLine);
const [errorMessage, setErrorMessage] = useState('');
const location = useLocation();
const navigate = useNavigate();

// Add online/offline detection
useEffect(() => {
const handleOnline = () => {
setIsOffline(false);
setErrorMessage('');
};
const handleOffline = () => {
setIsOffline(true);
setErrorMessage('You are offline. Some features may be limited.');
};

window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);

return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);

// Move debounce function definition inside useCallback
const debouncedSearch = useCallback((searchTerm) => {
const debounceTimeout = setTimeout(async () => {
Expand All @@ -29,11 +51,35 @@ const Navbar = () => {
return;
}

if (isOffline) {
setErrorMessage('Search is not available offline');
setIsLoading(false);
return;
}

try {
setIsLoading(true);
setErrorMessage('');

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout

const response = await fetch(
`https://api.themoviedb.org/3/search/multi?api_key=${process.env.REACT_APP_TMDB_API_KEY}&query=${encodeURIComponent(searchTerm)}&page=1`
`https://api.themoviedb.org/3/search/multi?api_key=${process.env.REACT_APP_TMDB_API_KEY}&query=${encodeURIComponent(searchTerm)}&page=1`,
{
signal: controller.signal,
headers: {
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
}
}
);

clearTimeout(timeoutId);

if (!response.ok) {
throw new Error('Search failed. Please try again.');
}

const data = await response.json();

const filteredResults = data.results
Expand All @@ -50,15 +96,19 @@ const Navbar = () => {

setSuggestions(filteredResults);
} catch (error) {
console.error('Error fetching suggestions:', error);
if (error.name === 'AbortError') {
setErrorMessage('Search timed out. Please try again.');
} else {
setErrorMessage(error.message || 'Failed to fetch suggestions');
}
setSuggestions([]);
} finally {
setIsLoading(false);
}
}, 300);

return () => clearTimeout(debounceTimeout);
}, [setSuggestions, setIsLoading]); // Add proper dependencies
}, [isOffline]);

useEffect(() => {
setIsMenuOpen(false);
Expand Down Expand Up @@ -364,8 +414,30 @@ const Navbar = () => {
</kbd>
</div>

{/* Error Message */}
{errorMessage && (
<div className={`mt-2 px-4 py-2 text-sm rounded ${
isDarkMode
? 'bg-red-900/50 text-red-200'
: 'bg-red-100 text-red-600'
}`}>
{errorMessage}
</div>
)}

{/* Offline Banner */}
{isOffline && (
<div className={`mt-2 px-4 py-2 text-sm rounded ${
isDarkMode
? 'bg-gray-800 text-gray-300'
: 'bg-gray-100 text-gray-600'
}`}>
You are currently offline
</div>
)}

{/* Updated Suggestions dropdown */}
{(suggestions.length > 0 || isLoading) && (
{(suggestions.length > 0 || isLoading) && !errorMessage && (
<div className={`mt-2 py-2 ${
isDarkMode ? 'border-t border-gray-700' : 'border-t border-gray-200'
}`}>
Expand Down

0 comments on commit 0395fd6

Please sign in to comment.