Skip to content

Commit

Permalink
Merge pull request #482 from tarunkumar2005/offline-status
Browse files Browse the repository at this point in the history
Offline First Experience with Caching and Sync Status Indicator
  • Loading branch information
dhairyagothi authored Nov 2, 2024
2 parents b2d2ff6 + 494bd7a commit ab71b34
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 0 deletions.
65 changes: 65 additions & 0 deletions frontend/package copy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "megaproject",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@reduxjs/toolkit": "^2.0.1",
"@tinymce/tinymce-react": "^5.1.1",
"appwrite": "^13.0.2",
"caniuse-lite": "^1.0.30001668",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"embla-carousel-react": "^8.3.0",
"framer-motion": "^11.11.7",
"html-react-parser": "^5.1.1",
"lucide-react": "^0.447.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.3",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.3.0",
"react-redux": "^9.1.0",
"react-responsive": "^10.0.0",
"react-router-dom": "^6.21.2",
"react-share": "^5.1.0",
"react-simple-chatbot": "^0.5.0",
"styled-components": "^4.0.0",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.55.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"vite": "^5.4.10"
}
}
196 changes: 196 additions & 0 deletions frontend/public/offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You're Offline - StationGuide</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f9fafb;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}

.offline-container {
background: white;
padding: 3rem;
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
text-align: center;
max-width: 32rem;
width: 100%;
}

.icon-container {
background-color: #eef2ff;
width: 5rem;
height: 5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 2rem;
transition: transform 0.2s;
}

.icon-container:hover {
transform: scale(1.05);
}

.icon {
width: 3rem;
height: 3rem;
color: #4f46e5;
}

h1 {
color: #1f2937;
font-size: 2rem;
margin-bottom: 1rem;
font-weight: 800;
}

p {
color: #6b7280;
margin-bottom: 2rem;
line-height: 1.7;
font-size: 1.1rem;
}

.button-container {
display: flex;
gap: 1rem;
justify-content: center;
}

.primary-button {
background-color: #4f46e5;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}

.primary-button:hover {
background-color: #4338ca;
transform: translateY(-1px);
}

.secondary-button {
background-color: white;
color: #4f46e5;
border: 1px solid #e5e7eb;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}

.secondary-button:hover {
background-color: #f9fafb;
color: #4338ca;
transform: translateY(-1px);
}

.status-text {
margin-top: 2rem;
font-size: 0.875rem;
color: #9ca3af;
}

.pulse {
display: inline-block;
width: 0.5rem;
height: 0.5rem;
background-color: #4f46e5;
border-radius: 50%;
margin-right: 0.5rem;
animation: pulse 2s infinite;
}

@keyframes pulse {
0% {
transform: scale(0.95);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0.95);
opacity: 0.5;
}
}

@media (max-width: 640px) {
.offline-container {
padding: 2rem;
}

.button-container {
flex-direction: column;
}

h1 {
font-size: 1.5rem;
}

p {
font-size: 1rem;
}
}
</style>
</head>
<body>
<div class="offline-container">
<div class="icon-container">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
</svg>
</div>
<h1>You're Currently Offline</h1>
<p>Don't worry! You can still access previously loaded content. We'll automatically reconnect when your internet connection is restored.</p>
<div class="button-container">
<button onclick="window.location.reload()" class="primary-button">
Try Again
</button>
<button onclick="window.history.back()" class="secondary-button">
Go Back
</button>
</div>
<div class="status-text">
<span class="pulse"></span>
Checking connection status...
</div>
</div>
<script>
// Check if we're back online
window.addEventListener('online', function() {
window.location.reload();
});

// Update status text when offline
window.addEventListener('offline', function() {
document.querySelector('.status-text').innerHTML = '<span class="pulse"></span>Waiting for connection...';
});
</script>
</body>
</html>
51 changes: 51 additions & 0 deletions frontend/public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const CACHE_NAME = 'offline-v1';
const OFFLINE_URL = '/offline.html';

self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Cache the offline page
await cache.add(new Request(OFFLINE_URL, { cache: 'reload' }));
})()
);
// Force the waiting service worker to become the active service worker
self.skipWaiting();
});

self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if available
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately
self.clients.claim();
});

self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if available
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}

// Try to fetch the request from the network
return await fetch(event.request);
} catch (error) {
// If fetch fails (offline), get the offline page from cache
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
});
12 changes: 12 additions & 0 deletions frontend/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import { GoogleOAuthProvider } from "@react-oauth/google"

const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful');
})
.catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
}

createRoot(document.getElementById('root')).render(
<StrictMode>
<GoogleOAuthProvider clientId={clientId}>
Expand Down

0 comments on commit ab71b34

Please sign in to comment.