Skip to content

Commit 31576fc

Browse files
committed
feat: add full PWA support with install banner
- Update manifest with start_url, scope, id, maskable icon - Add service worker for offline caching - Add install prompt banner (native on Android, manual instructions on iOS) - Add iOS-specific meta tags for standalone mode
1 parent c1dcc36 commit 31576fc

File tree

3 files changed

+291
-6
lines changed

3 files changed

+291
-6
lines changed

makeitwork.cloud/index.html

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
2121
<link rel="manifest" href="/site.webmanifest" />
2222
<meta name="theme-color" content="#0a0a0a" />
23+
<!-- iOS PWA -->
24+
<meta name="apple-mobile-web-app-capable" content="yes" />
25+
<meta
26+
name="apple-mobile-web-app-status-bar-style"
27+
content="black-translucent"
28+
/>
29+
<meta name="apple-mobile-web-app-title" content="Make IT Work" />
30+
<link rel="apple-touch-startup-image" href="/android-chrome-512x512.png" />
2331
<link
2432
rel="stylesheet"
2533
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap"
@@ -415,6 +423,110 @@
415423
::-webkit-scrollbar-thumb:hover {
416424
background: var(--gold-dim);
417425
}
426+
427+
/* Install Banner */
428+
.install-banner {
429+
position: fixed;
430+
bottom: 0;
431+
left: 0;
432+
right: 0;
433+
background: var(--bg-card);
434+
border-top: 1px solid var(--border-gold);
435+
padding: 1rem;
436+
display: flex;
437+
align-items: center;
438+
justify-content: space-between;
439+
gap: 1rem;
440+
transform: translateY(100%);
441+
transition: transform 0.3s ease;
442+
z-index: 1000;
443+
}
444+
445+
.install-banner.visible {
446+
transform: translateY(0);
447+
}
448+
449+
.install-banner-content {
450+
display: flex;
451+
align-items: center;
452+
gap: 0.75rem;
453+
flex: 1;
454+
min-width: 0;
455+
}
456+
457+
.install-banner-icon {
458+
width: 40px;
459+
height: 40px;
460+
border-radius: 8px;
461+
flex-shrink: 0;
462+
}
463+
464+
.install-banner-text {
465+
flex: 1;
466+
min-width: 0;
467+
}
468+
469+
.install-banner-title {
470+
font-size: 0.85rem;
471+
font-weight: 500;
472+
color: var(--gold);
473+
}
474+
475+
.install-banner-subtitle {
476+
font-size: 0.7rem;
477+
color: var(--text-muted);
478+
}
479+
480+
.install-banner-actions {
481+
display: flex;
482+
gap: 0.5rem;
483+
flex-shrink: 0;
484+
}
485+
486+
.install-btn {
487+
background: var(--gold-dim);
488+
color: var(--bg-dark);
489+
border: none;
490+
padding: 0.5rem 1rem;
491+
font-family: inherit;
492+
font-size: 0.75rem;
493+
font-weight: 600;
494+
cursor: pointer;
495+
transition: all 0.2s ease;
496+
}
497+
498+
.install-btn:hover {
499+
background: var(--gold);
500+
}
501+
502+
.install-dismiss {
503+
background: transparent;
504+
color: var(--text-dim);
505+
border: 1px solid var(--border-dim);
506+
padding: 0.5rem 0.75rem;
507+
font-family: inherit;
508+
font-size: 0.75rem;
509+
cursor: pointer;
510+
transition: all 0.2s ease;
511+
}
512+
513+
.install-dismiss:hover {
514+
border-color: var(--text-muted);
515+
color: var(--text-muted);
516+
}
517+
518+
/* iOS install instructions */
519+
.ios-instructions {
520+
display: none;
521+
font-size: 0.7rem;
522+
color: var(--text-muted);
523+
text-align: center;
524+
padding: 0.5rem;
525+
}
526+
527+
.ios-instructions i {
528+
color: var(--gold-dim);
529+
}
418530
</style>
419531
</head>
420532
<body>
@@ -647,5 +759,100 @@ <h1 class="site-title">MAKE IT WORK</h1>
647759
></a>
648760
</nav>
649761
</main>
762+
763+
<!-- Install Banner -->
764+
<div id="installBanner" class="install-banner">
765+
<div class="install-banner-content">
766+
<img
767+
src="/android-chrome-192x192.png"
768+
alt="Make IT Work"
769+
class="install-banner-icon"
770+
/>
771+
<div class="install-banner-text">
772+
<div class="install-banner-title">Install Make IT Work</div>
773+
<div class="install-banner-subtitle">
774+
Add to your home screen for quick access
775+
</div>
776+
<div id="iosInstructions" class="ios-instructions">
777+
Tap <i class="fa fa-share-from-square"></i> then "Add to Home
778+
Screen"
779+
</div>
780+
</div>
781+
</div>
782+
<div class="install-banner-actions">
783+
<button id="installBtn" class="install-btn">Install</button>
784+
<button id="dismissBtn" class="install-dismiss">Not now</button>
785+
</div>
786+
</div>
787+
788+
<script>
789+
// Service Worker Registration
790+
if ("serviceWorker" in navigator) {
791+
navigator.serviceWorker.register("/sw.js").catch(() => {});
792+
}
793+
794+
// Install Prompt Logic
795+
(() => {
796+
const banner = document.getElementById("installBanner");
797+
const installBtn = document.getElementById("installBtn");
798+
const dismissBtn = document.getElementById("dismissBtn");
799+
const iosInstructions = document.getElementById("iosInstructions");
800+
801+
let deferredPrompt = null;
802+
const DISMISSED_KEY = "pwa-install-dismissed";
803+
const DISMISS_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days
804+
805+
// Check if already installed or dismissed
806+
const isStandalone =
807+
window.matchMedia("(display-mode: standalone)").matches ||
808+
navigator.standalone === true;
809+
const dismissedAt = localStorage.getItem(DISMISSED_KEY);
810+
const wasDismissed =
811+
dismissedAt &&
812+
Date.now() - parseInt(dismissedAt, 10) < DISMISS_DURATION;
813+
814+
if (isStandalone || wasDismissed) return;
815+
816+
// Detect iOS
817+
const isIOS =
818+
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
819+
820+
if (isIOS) {
821+
// iOS: Show banner with manual instructions
822+
iosInstructions.style.display = "block";
823+
installBtn.style.display = "none";
824+
setTimeout(() => banner.classList.add("visible"), 1000);
825+
} else {
826+
// Android/Desktop: Use beforeinstallprompt
827+
window.addEventListener("beforeinstallprompt", (e) => {
828+
e.preventDefault();
829+
deferredPrompt = e;
830+
setTimeout(() => banner.classList.add("visible"), 1000);
831+
});
832+
833+
installBtn.addEventListener("click", async () => {
834+
if (!deferredPrompt) return;
835+
deferredPrompt.prompt();
836+
const { outcome } = await deferredPrompt.userChoice;
837+
if (outcome === "accepted") {
838+
banner.classList.remove("visible");
839+
}
840+
deferredPrompt = null;
841+
});
842+
}
843+
844+
// Dismiss button
845+
dismissBtn.addEventListener("click", () => {
846+
localStorage.setItem(DISMISSED_KEY, Date.now().toString());
847+
banner.classList.remove("visible");
848+
});
849+
850+
// Hide if installed later
851+
window.addEventListener("appinstalled", () => {
852+
banner.classList.remove("visible");
853+
deferredPrompt = null;
854+
});
855+
})();
856+
</script>
650857
</body>
651858
</html>

makeitwork.cloud/site.webmanifest

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
{
22
"name": "Make IT Work",
3-
"short_name": "MIW",
3+
"short_name": "Make IT Work",
4+
"description": "Make IT Work Cloud - DevOps Portal",
5+
"start_url": "/",
6+
"scope": "/",
7+
"id": "/",
8+
"display": "standalone",
9+
"orientation": "portrait-primary",
10+
"theme_color": "#0a0a0a",
11+
"background_color": "#0a0a0a",
412
"icons": [
513
{
614
"src": "/android-chrome-192x192.png",
715
"sizes": "192x192",
8-
"type": "image/png"
16+
"type": "image/png",
17+
"purpose": "any"
18+
},
19+
{
20+
"src": "/android-chrome-512x512.png",
21+
"sizes": "512x512",
22+
"type": "image/png",
23+
"purpose": "any"
924
},
1025
{
1126
"src": "/android-chrome-512x512.png",
1227
"sizes": "512x512",
13-
"type": "image/png"
28+
"type": "image/png",
29+
"purpose": "maskable"
1430
}
1531
],
16-
"theme_color": "#0a0a0a",
17-
"background_color": "#0a0a0a",
18-
"display": "standalone"
32+
"categories": ["utilities", "productivity"]
1933
}

makeitwork.cloud/sw.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const CACHE_NAME = 'miw-cache-v1';
2+
const ASSETS_TO_CACHE = [
3+
'/',
4+
'/index.html',
5+
'/site.webmanifest',
6+
'/favicon.ico',
7+
'/favicon-16x16.png',
8+
'/favicon-32x32.png',
9+
'/apple-touch-icon.png',
10+
'/android-chrome-192x192.png',
11+
'/android-chrome-512x512.png',
12+
'/Web437_IBM_EGA_8x8.woff'
13+
];
14+
15+
// Install: cache core assets
16+
self.addEventListener('install', (event) => {
17+
event.waitUntil(
18+
caches.open(CACHE_NAME).then((cache) => {
19+
return cache.addAll(ASSETS_TO_CACHE);
20+
})
21+
);
22+
self.skipWaiting();
23+
});
24+
25+
// Activate: clean old caches
26+
self.addEventListener('activate', (event) => {
27+
event.waitUntil(
28+
caches.keys().then((cacheNames) => {
29+
return Promise.all(
30+
cacheNames
31+
.filter((name) => name !== CACHE_NAME)
32+
.map((name) => caches.delete(name))
33+
);
34+
})
35+
);
36+
self.clients.claim();
37+
});
38+
39+
// Fetch: network-first with cache fallback
40+
self.addEventListener('fetch', (event) => {
41+
// Skip non-GET requests
42+
if (event.request.method !== 'GET') return;
43+
44+
// Skip cross-origin requests (CDN fonts, Font Awesome, etc.)
45+
if (!event.request.url.startsWith(self.location.origin)) return;
46+
47+
event.respondWith(
48+
fetch(event.request)
49+
.then((response) => {
50+
// Clone and cache successful responses
51+
if (response.ok) {
52+
const responseClone = response.clone();
53+
caches.open(CACHE_NAME).then((cache) => {
54+
cache.put(event.request, responseClone);
55+
});
56+
}
57+
return response;
58+
})
59+
.catch(() => {
60+
// Fallback to cache on network failure
61+
return caches.match(event.request);
62+
})
63+
);
64+
});

0 commit comments

Comments
 (0)