Skip to content

Commit 08df50a

Browse files
committed
feat: implement PWA for desktop installation with college logo
1 parent 77233d7 commit 08df50a

6 files changed

Lines changed: 220 additions & 5 deletions

File tree

public/icon-192.png

107 KB
Loading

public/icon-512.png

107 KB
Loading

public/manifest.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "College of Engineering Poonjar - E-Learning Platform",
3+
"short_name": "CEP Notes",
4+
"description": "Access study materials, notes, and resources for College of Engineering Poonjar students",
5+
"start_url": "/",
6+
"display": "standalone",
7+
"background_color": "#ffffff",
8+
"theme_color": "#2563eb",
9+
"orientation": "portrait-primary",
10+
"icons": [
11+
{
12+
"src": "/icon-192.png",
13+
"sizes": "192x192",
14+
"type": "image/png",
15+
"purpose": "any maskable"
16+
},
17+
{
18+
"src": "/icon-512.png",
19+
"sizes": "512x512",
20+
"type": "image/png",
21+
"purpose": "any maskable"
22+
}
23+
],
24+
"categories": [
25+
"education",
26+
"productivity"
27+
],
28+
"screenshots": [],
29+
"shortcuts": [
30+
{
31+
"name": "Browse Notes",
32+
"url": "/#browse",
33+
"description": "Browse available study materials"
34+
},
35+
{
36+
"name": "My Uploads",
37+
"url": "/dashboard",
38+
"description": "View your uploaded files"
39+
}
40+
]
41+
}

public/sw.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Service Worker for PWA
2+
const CACHE_NAME = 'cep-notes-v1';
3+
const urlsToCache = [
4+
'/',
5+
'/dashboard',
6+
'/logo.png'
7+
];
8+
9+
// Install event - cache essential files
10+
self.addEventListener('install', (event) => {
11+
event.waitUntil(
12+
caches.open(CACHE_NAME)
13+
.then((cache) => cache.addAll(urlsToCache))
14+
.then(() => self.skipWaiting())
15+
);
16+
});
17+
18+
// Activate event - clean up old caches
19+
self.addEventListener('activate', (event) => {
20+
event.waitUntil(
21+
caches.keys().then((cacheNames) => {
22+
return Promise.all(
23+
cacheNames.map((cacheName) => {
24+
if (cacheName !== CACHE_NAME) {
25+
return caches.delete(cacheName);
26+
}
27+
})
28+
);
29+
}).then(() => self.clients.claim())
30+
);
31+
});
32+
33+
// Fetch event - network first, fallback to cache
34+
self.addEventListener('fetch', (event) => {
35+
event.respondWith(
36+
fetch(event.request)
37+
.then((response) => {
38+
// Clone the response
39+
const responseToCache = response.clone();
40+
41+
caches.open(CACHE_NAME).then((cache) => {
42+
cache.put(event.request, responseToCache);
43+
});
44+
45+
return response;
46+
})
47+
.catch(() => {
48+
return caches.match(event.request);
49+
})
50+
);
51+
});

src/app/layout.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,23 @@ export const metadata: Metadata = {
1212
icons: {
1313
icon: "/logo.png",
1414
},
15+
manifest: "/manifest.json",
16+
themeColor: "#2563eb",
17+
appleWebApp: {
18+
capable: true,
19+
statusBarStyle: "default",
20+
title: "CEP Notes",
21+
},
1522
other: {
1623
"google-adsense-account": "ca-pub-6253589071371136",
1724
},
1825
};
1926

20-
import DisableDevTools from "@/components/common/DisableDevTools";
2127
import { UndoProvider } from "@/context/UndoContext";
2228
import { ThemeProvider } from "@/context/ThemeContext";
23-
24-
2529
import { ToastProvider } from "@/context/ToastContext";
26-
27-
// ...
30+
import InstallPrompt from "@/components/common/InstallPrompt";
31+
import Script from "next/script";
2832

2933
export default function RootLayout({
3034
children,
@@ -59,6 +63,7 @@ export default function RootLayout({
5963
<ToastProvider>
6064
<Navbar />
6165
<main style={{ padding: "2rem 0" }}>{children}</main>
66+
<InstallPrompt />
6267
{/*
6368
AdSense disabled due to incompatibility with Next.js SSR
6469
See: https://github.com/vercel/next.js/discussions/38256
@@ -67,6 +72,19 @@ export default function RootLayout({
6772
</ToastProvider>
6873
</UndoProvider>
6974
</ThemeProvider>
75+
76+
{/* Register Service Worker */}
77+
<Script id="register-sw" strategy="afterInteractive">
78+
{`
79+
if ('serviceWorker' in navigator) {
80+
window.addEventListener('load', () => {
81+
navigator.serviceWorker.register('/sw.js')
82+
.then(reg => console.log('SW registered'))
83+
.catch(err => console.log('SW registration failed'));
84+
});
85+
}
86+
`}
87+
</Script>
7088
</body>
7189
</html>
7290
);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { Download, X } from "lucide-react";
5+
6+
export default function InstallPrompt() {
7+
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
8+
const [showInstall, setShowInstall] = useState(false);
9+
10+
useEffect(() => {
11+
const handler = (e: Event) => {
12+
e.preventDefault();
13+
setDeferredPrompt(e);
14+
setShowInstall(true);
15+
};
16+
17+
window.addEventListener('beforeinstallprompt', handler);
18+
19+
// Check if already installed
20+
if (window.matchMedia('(display-mode: standalone)').matches) {
21+
setShowInstall(false);
22+
}
23+
24+
return () => window.removeEventListener('beforeinstallprompt', handler);
25+
}, []);
26+
27+
const handleInstall = async () => {
28+
if (!deferredPrompt) return;
29+
30+
deferredPrompt.prompt();
31+
const { outcome } = await deferredPrompt.userChoice;
32+
33+
if (outcome === 'accepted') {
34+
setShowInstall(false);
35+
}
36+
37+
setDeferredPrompt(null);
38+
};
39+
40+
if (!showInstall) return null;
41+
42+
return (
43+
<div style={{
44+
position: 'fixed',
45+
bottom: '20px',
46+
right: '20px',
47+
background: 'var(--primary)',
48+
color: 'white',
49+
padding: '1rem 1.5rem',
50+
borderRadius: '12px',
51+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
52+
display: 'flex',
53+
alignItems: 'center',
54+
gap: '1rem',
55+
zIndex: 1000,
56+
maxWidth: '350px',
57+
animation: 'slideIn 0.3s ease'
58+
}}>
59+
<Download size={24} />
60+
<div style={{ flex: 1 }}>
61+
<p style={{ fontWeight: 600, marginBottom: '0.25rem' }}>Install CEP Notes</p>
62+
<p style={{ fontSize: '0.875rem', opacity: 0.9 }}>Get quick access from your desktop</p>
63+
</div>
64+
<button
65+
onClick={handleInstall}
66+
style={{
67+
background: 'white',
68+
color: 'var(--primary)',
69+
border: 'none',
70+
padding: '0.5rem 1rem',
71+
borderRadius: '6px',
72+
fontWeight: 600,
73+
cursor: 'pointer'
74+
}}
75+
>
76+
Install
77+
</button>
78+
<button
79+
onClick={() => setShowInstall(false)}
80+
style={{
81+
background: 'transparent',
82+
border: 'none',
83+
color: 'white',
84+
cursor: 'pointer',
85+
padding: '0.5rem'
86+
}}
87+
>
88+
<X size={20} />
89+
</button>
90+
91+
<style jsx>{`
92+
@keyframes slideIn {
93+
from {
94+
transform: translateY(100px);
95+
opacity: 0;
96+
}
97+
to {
98+
transform: translateY(0);
99+
opacity: 1;
100+
}
101+
}
102+
`}</style>
103+
</div>
104+
);
105+
}

0 commit comments

Comments
 (0)