Skip to content

Commit

Permalink
PWA Support - Service Worker + Offline Mode
Browse files Browse the repository at this point in the history
  • Loading branch information
0xGingi committed Dec 24, 2024
1 parent 1aaad66 commit 9627354
Show file tree
Hide file tree
Showing 8 changed files with 502 additions and 75 deletions.
27 changes: 27 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,33 @@

<!-- Main App Script -->
<script defer type="module" src="/src/main.tsx"></script>

<!-- PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="TrustyNotes">

<!-- PWA Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#1e1e1e">

<!-- Register Service Worker -->
<script>
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);
});
});
}
</script>
</head>

<body>
Expand Down
60 changes: 60 additions & 0 deletions public/offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrustyNotes - Offline</title>
<style>
body {
font-family: Inter, system-ui, -apple-system, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
background: var(--background, #ffffff);
color: var(--text, #1e293b);
}
.container {
max-width: 500px;
}
.icon {
font-size: 48px;
margin-bottom: 20px;
}
h1 {
margin-bottom: 16px;
}
p {
margin-bottom: 24px;
color: var(--text-secondary, #64748b);
}
button {
padding: 8px 16px;
border-radius: 8px;
border: none;
background: var(--primary-color, #3b82f6);
color: white;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
body {
--background: #1e1e1e;
--text: #ffffff;
--text-secondary: #a1a1aa;
}
}
</style>
</head>
<body>
<div class="container">
<div class="icon">📱</div>
<h1>You're Offline</h1>
<p>Don't worry! Your notes are safely stored and will sync when you're back online.</p>
<button onclick="window.location.reload()">Try Again</button>
</div>
</body>
</html>
42 changes: 41 additions & 1 deletion public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "TrustyNotes",
"short_name": "TrustyNotes",
"description": "Secure, encrypted note-taking app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1e1e1e",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"shortcuts": [
{
"name": "New Note",
"short_name": "New",
"description": "Create a new note",
"url": "/?new=true",
"icons": [{ "src": "/icons/new-note.png", "sizes": "192x192" }]
}
],
"screenshots": [
{
"src": "/screenshots/app.png",
"sizes": "1280x720",
"type": "image/png",
"platform": "wide",
"label": "TrustyNotes App Interface"
}
]
}
23 changes: 23 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { SyncSettings as SyncSettingsType } from './types/sync';
import { WebStorageService } from './services/webStorage';
import './styles/richtext.css';
import { MobileNav } from './components/MobileNav';
import { notifications } from '@mantine/notifications';
import { InstallPrompt } from './components/InstallPrompt';

interface Note {
id?: number;
Expand Down Expand Up @@ -373,6 +375,26 @@ async function deleteNote(noteId: number) {
}
}

useEffect(() => {
WebStorageService.initializeOfflineSupport();

window.addEventListener('online', () => {
notifications.show({
title: 'Back Online',
message: 'Your changes will be synced automatically.',
color: 'green'
});
});

window.addEventListener('offline', () => {
notifications.show({
title: 'Offline Mode',
message: 'Changes will be saved locally and synced when online.',
color: 'yellow'
});
});
}, []);

return (
<AppShell
header={isMobile ? { height: 60 } : undefined}
Expand Down Expand Up @@ -609,6 +631,7 @@ async function deleteNote(noteId: number) {

<AppShell.Main>
<Stack h="100vh" gap={0} style={{ overflow: 'hidden' }}>
{isMobile && <InstallPrompt />}
<Box
p={isMobile ? "xs" : "md"}
style={{
Expand Down
84 changes: 84 additions & 0 deletions src/components/InstallPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState, useEffect } from 'react';
import { Button, Paper, Group, Text, Stack } from '@mantine/core';
import { IconDeviceMobile } from '@tabler/icons-react';

interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [showIOSPrompt, setShowIOSPrompt] = useState(false);

useEffect(() => {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !/CriOS/.test(navigator.userAgent);
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;

if (isIOS && !isStandalone) {
setShowIOSPrompt(true);
}

const handleBeforeInstall = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};

window.addEventListener('beforeinstallprompt', handleBeforeInstall);

return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
};
}, []);

const handleInstall = async () => {
if (!deferredPrompt) return;

deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;

if (outcome === 'accepted') {
setDeferredPrompt(null);
}
};

if (!deferredPrompt && !showIOSPrompt) return null;

return (
<Paper p="md" withBorder>
<Stack gap="xs">
<Group>
<IconDeviceMobile size={24} />
<Text fw={500}>Install TrustyNotes</Text>
</Group>

{showIOSPrompt ? (
<>
<Text size="sm">
Install TrustyNotes on your iOS device: tap the share button
and select "Add to Home Screen"
</Text>
<Button
variant="light"
onClick={() => setShowIOSPrompt(false)}
>
Got it
</Button>
</>
) : (
<>
<Text size="sm">
Install TrustyNotes for quick access and offline use
</Text>
<Button
onClick={handleInstall}
leftSection={<IconDeviceMobile size={16} />}
>
Install App
</Button>
</>
)}
</Stack>
</Paper>
);
}
Loading

0 comments on commit 9627354

Please sign in to comment.