Skip to content

Commit a3c0e03

Browse files
committed
Auto sync: sync changes across Sathi apps
1 parent 6d837f7 commit a3c0e03

3 files changed

Lines changed: 232 additions & 79 deletions

File tree

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ <h2 data-i18n="feedbackTitle">Family feedback</h2>
336336
.catch(() => loadRuntime(fallbackBuild));
337337
})();
338338
</script>
339+
<script defer src="notification-prompt.js"></script>
340+
<script defer src="sathi-navigator.js"></script>
339341
</body>
340342
</html>
341343

notification-prompt.js

Lines changed: 6 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,8 @@
44
const DISMISS_KEY = `${location.pathname}:notification-prompt-dismissed-until`;
55
const GRANTED_KEY = `${location.pathname}:notification-prompt-granted`;
66
const DAY = 24 * 60 * 60 * 1000;
7-
const APP_SEGMENT = location.pathname.split("/").filter(Boolean)[0] || "";
8-
const APP_ID = APP_SEGMENT ? APP_SEGMENT.toLowerCase() : "";
9-
const INSTALL_KEY = APP_ID ? `sathi-installed-${APP_ID}` : "";
10-
const LAST_OPEN_KEY = APP_ID ? `sathi-last-opened-${APP_ID}` : "";
11-
let refreshing = false;
127

13-
function markInstalled() {
14-
if (INSTALL_KEY) {
15-
localStorage.setItem(INSTALL_KEY, "true");
16-
}
17-
}
18-
19-
function markOpened() {
20-
if (LAST_OPEN_KEY) {
21-
localStorage.setItem(LAST_OPEN_KEY, String(Date.now()));
22-
}
23-
}
24-
25-
async function refreshInstalledShell() {
26-
if (!("serviceWorker" in navigator)) return;
27-
try {
28-
const registration = await navigator.serviceWorker.getRegistration();
29-
await registration?.update();
30-
} catch (error) {
31-
console.error("Service worker refresh check failed", error);
32-
}
33-
}
34-
35-
async function registerServiceWorker() {
36-
if (!("serviceWorker" in navigator) || window.__sathiSwManaged) return null;
37-
38-
window.__sathiSwManaged = true;
39-
navigator.serviceWorker.addEventListener("controllerchange", () => {
40-
if (refreshing) return;
41-
refreshing = true;
42-
window.location.reload();
43-
});
44-
45-
try {
46-
const registration = await navigator.serviceWorker.register("./sw.js", { updateViaCache: "none" });
47-
registration.addEventListener("updatefound", () => {
48-
const worker = registration.installing;
49-
if (!worker) return;
50-
worker.addEventListener("statechange", () => {
51-
if (worker.state === "installed" && navigator.serviceWorker.controller) {
52-
worker.postMessage({ type: "SKIP_WAITING" });
53-
}
54-
});
55-
});
56-
await registration.update();
57-
return registration;
58-
} catch (error) {
59-
console.error("Service worker registration failed", error);
60-
return null;
61-
}
62-
}
8+
if (!("Notification" in window)) return;
639

6410
function saveDismiss(days) {
6511
localStorage.setItem(DISMISS_KEY, String(Date.now() + days * DAY));
@@ -74,6 +20,10 @@
7420
document.getElementById(PROMPT_ID)?.remove();
7521
}
7622

23+
function welcomeMessage() {
24+
return document.title ? `${document.title} notifications are on.` : "Notifications are on.";
25+
}
26+
7727
async function showWelcomeNotification() {
7828
try {
7929
if ("serviceWorker" in navigator) {
@@ -95,7 +45,6 @@
9545
}
9646

9747
function injectPrompt() {
98-
if (!("Notification" in window)) return;
9948
if (document.getElementById(PROMPT_ID) || isDismissed()) return;
10049
if (Notification.permission === "granted") {
10150
localStorage.setItem(GRANTED_KEY, "true");
@@ -223,29 +172,7 @@
223172
document.body.appendChild(prompt);
224173
}
225174

226-
markOpened();
227-
228-
if (window.matchMedia("(display-mode: standalone)").matches) {
229-
markInstalled();
230-
}
231-
232-
window.addEventListener("appinstalled", markInstalled);
233-
document.addEventListener("visibilitychange", () => {
234-
if (document.visibilityState === "visible") {
235-
markOpened();
236-
void refreshInstalledShell();
237-
}
238-
});
239-
window.addEventListener("focus", () => {
240-
markOpened();
241-
void refreshInstalledShell();
242-
});
243-
244175
window.addEventListener("load", () => {
245-
markOpened();
246-
void registerServiceWorker();
247-
if ("Notification" in window) {
248-
window.setTimeout(injectPrompt, 1400);
249-
}
176+
window.setTimeout(injectPrompt, 1400);
250177
}, { once: true });
251178
})();

sathi-navigator.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"use strict";
2+
(() => {
3+
const NAV_ID = "sathi-global-navigator";
4+
const OVERLAY_ID = "sathi-nav-overlay";
5+
const DRAWER_ID = "sathi-nav-drawer";
6+
7+
const APPS = [
8+
{ title: "Aapka Sathi", url: "https://snakeeye-sudo.github.io/Aapka-Sathi/", icon: "🏠", desc: "Family Hub" },
9+
{ title: "Pariksha Sathi", url: "https://snakeeye-sudo.github.io/pariksha-sathi/", icon: "📚", desc: "Study Planner" },
10+
{ title: "Rozgar Sathi", url: "https://snakeeye-sudo.github.io/rozgar-sathi/", icon: "💼", desc: "Job Portal" },
11+
{ title: "Samachar Sathi", url: "https://snakeeye-sudo.github.io/Samachar-Sathi/", icon: "📰", desc: "Daily News" },
12+
{ title: "Hisaab Sathi", url: "https://snakeeye-sudo.github.io/Hisaab-Sathi/", icon: "💸", desc: "Daily Ledger" },
13+
{ title: "Antariksh Sathi", url: "https://snakeeye-sudo.github.io/Antariksh-Sathi/", icon: "🚀", desc: "Space Guide" },
14+
{ title: "Ganit Sathi", url: "https://snakeeye-sudo.github.io/Ganit-Sathi/", icon: "🔢", desc: "Calculator" },
15+
{ title: "Jal Sathi", url: "https://snakeeye-sudo.github.io/Jal-Sathi/", icon: "💧", desc: "Hydration Hub" },
16+
{ title: "Mann Sathi", url: "https://snakeeye-sudo.github.io/Mann-Sathi/", icon: "🧠", desc: "Journal & Mood" },
17+
{ title: "Sanket Sathi", url: "https://snakeeye-sudo.github.io/Sanket-Sathi/", icon: "⚡", desc: "Morse Studio" },
18+
{ title: "Sikka Sathi", url: "https://snakeeye-sudo.github.io/Sikka-Sathi/", icon: "🪙", desc: "Quick Flip" },
19+
{ title: "Khel Sathi", url: "https://snakeeye-sudo.github.io/Khel-Sathi/", icon: "🎮", desc: "Classic Games" },
20+
{ title: "Dhyan Sathi", url: "https://snakeeye-sudo.github.io/Dhyan-Sathi/", icon: "🧘", desc: "Focus Timer" },
21+
{ title: "Mausam Sathi", url: "https://snakeeye-sudo.github.io/Mausam-Sathi/", icon: "🌤️", desc: "Weather Watch" },
22+
{ title: "Paltu Sathi", url: "https://snakeeye-sudo.github.io/Paltu-Sathi/", icon: "🐾", desc: "Virtual Pet" },
23+
{ title: "Panchang Sathi", url: "https://snakeeye-sudo.github.io/Panchang-Sathi/", icon: "🕉️", desc: "Daily Tithi" },
24+
{ title: "Ank Sathi", url: "https://snakeeye-sudo.github.io/Ank-Sathi/", icon: "🔮", desc: "Numerology" }
25+
];
26+
27+
function toggle() {
28+
const overlay = document.getElementById(OVERLAY_ID);
29+
const drawer = document.getElementById(DRAWER_ID);
30+
const isOpen = overlay.getAttribute("aria-hidden") === "false";
31+
32+
overlay.setAttribute("aria-hidden", String(isOpen));
33+
overlay.style.opacity = isOpen ? "0" : "1";
34+
overlay.style.pointerEvents = isOpen ? "none" : "auto";
35+
drawer.style.transform = isOpen ? "translateX(-100%)" : "translateX(0)";
36+
}
37+
38+
function inject() {
39+
if (document.getElementById(NAV_ID)) return;
40+
41+
const nav = document.createElement("div");
42+
nav.id = NAV_ID;
43+
nav.innerHTML = `
44+
<style>
45+
:root {
46+
--sathi-glass: rgba(15, 23, 42, 0.85);
47+
--sathi-border: rgba(255, 255, 255, 0.1);
48+
--sathi-accent: #f59e0b;
49+
}
50+
#sathi-nav-trigger {
51+
position: fixed;
52+
top: 14px;
53+
left: 14px;
54+
z-index: 10001;
55+
width: 48px;
56+
height: 48px;
57+
border-radius: 14px;
58+
background: var(--sathi-glass);
59+
border: 1px solid var(--sathi-border);
60+
backdrop-filter: blur(12px);
61+
-webkit-backdrop-filter: blur(12px);
62+
color: white;
63+
display: flex;
64+
align-items: center;
65+
justify-content: center;
66+
cursor: pointer;
67+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
68+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
69+
}
70+
#sathi-nav-trigger:hover {
71+
background: rgba(15, 23, 42, 0.95);
72+
transform: translateY(-2px);
73+
border-color: var(--sathi-accent);
74+
}
75+
#\${OVERLAY_ID} {
76+
position: fixed;
77+
inset: 0;
78+
background: rgba(0, 0, 0, 0.6);
79+
backdrop-filter: blur(4px);
80+
-webkit-backdrop-filter: blur(4px);
81+
z-index: 10002;
82+
opacity: 0;
83+
pointer-events: none;
84+
transition: opacity 0.4s ease;
85+
}
86+
#\${DRAWER_ID} {
87+
position: fixed;
88+
top: 0;
89+
left: 0;
90+
bottom: 0;
91+
width: min(340px, 85vw);
92+
background: #0f172a;
93+
z-index: 10003;
94+
transform: translateX(-100%);
95+
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
96+
border-right: 1px solid var(--sathi-border);
97+
display: flex;
98+
flex-direction: column;
99+
box-shadow: 20px 0 60px rgba(0,0,0,0.5);
100+
}
101+
#sathi-nav-header {
102+
padding: 30px 24px;
103+
border-bottom: 1px solid var(--sathi-border);
104+
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), transparent);
105+
}
106+
#sathi-nav-header h2 {
107+
margin: 0;
108+
font-family: 'Syne', sans-serif;
109+
font-weight: 800;
110+
font-size: 1.5rem;
111+
color: var(--sathi-accent);
112+
}
113+
#sathi-nav-header p {
114+
margin: 4px 0 0;
115+
font-size: 0.85rem;
116+
color: rgba(255,255,255,0.6);
117+
}
118+
#sathi-nav-list {
119+
flex: 1;
120+
overflow-y: auto;
121+
padding: 16px;
122+
}
123+
.sathi-nav-item {
124+
display: flex;
125+
align-items: center;
126+
gap: 16px;
127+
padding: 12px 14px;
128+
margin-bottom: 4px;
129+
border-radius: 12px;
130+
color: white;
131+
text-decoration: none;
132+
transition: all 0.2s ease;
133+
border: 1px solid transparent;
134+
}
135+
.sathi-nav-item:hover {
136+
background: rgba(255,255,255,0.05);
137+
border-color: var(--sathi-border);
138+
transform: translateX(4px);
139+
}
140+
.sathi-nav-item.active {
141+
background: rgba(245, 158, 11, 0.15);
142+
border-color: rgba(245, 158, 11, 0.3);
143+
color: var(--sathi-accent);
144+
}
145+
.sathi-app-icon {
146+
width: 42px;
147+
height: 42px;
148+
background: rgba(255,255,255,0.05);
149+
border-radius: 10px;
150+
display: flex;
151+
align-items: center;
152+
justify-content: center;
153+
font-size: 1.4rem;
154+
}
155+
.sathi-app-info {
156+
display: flex;
157+
flex-direction: column;
158+
}
159+
.sathi-app-name {
160+
font-weight: 600;
161+
font-size: 1rem;
162+
}
163+
.sathi-app-desc {
164+
font-size: 0.75rem;
165+
color: rgba(255,255,255,0.5);
166+
}
167+
#sathi-nav-footer {
168+
padding: 20px;
169+
border-top: 1px solid var(--sathi-border);
170+
font-size: 0.8rem;
171+
color: rgba(255,255,255,0.4);
172+
text-align: center;
173+
}
174+
</style>
175+
<div id="sathi-nav-trigger" title="Open Sathi Menu">
176+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
177+
<line x1="3" y1="12" x2="21" y2="12"></line>
178+
<line x1="3" y1="6" x2="21" y2="6"></line>
179+
<line x1="3" y1="18" x2="21" y2="18"></line>
180+
</svg>
181+
</div>
182+
<div id="\${OVERLAY_ID}" aria-hidden="true"></div>
183+
<div id="\${DRAWER_ID}">
184+
<div id="sathi-nav-header">
185+
<h2>Aapka Sathi</h2>
186+
<p>Family of Helpful Indian Apps</p>
187+
</div>
188+
<div id="sathi-nav-list">
189+
\${APPS.map(app => {
190+
const isActive = location.href.includes(app.url) || (app.url === "https://snakeeye-sudo.github.io/Aapka-Sathi/" && (location.pathname === "/" || location.pathname.includes("Aapka-Sathi")));
191+
return \`
192+
<a href="\${app.url}" class="sathi-nav-item \${isActive ? 'active' : ''}">
193+
<div class="sathi-app-icon">\${app.icon}</div>
194+
<div class="sathi-app-info">
195+
<span class="sathi-app-name">\${app.title}</span>
196+
<span class="sathi-app-desc">\${app.desc}</span>
197+
</div>
198+
</a>
199+
\`;
200+
}).join("")}
201+
</div>
202+
<div id="sathi-nav-footer">
203+
Developed by Er. Sangam Krishna &copy; 2026
204+
</div>
205+
</div>
206+
`;
207+
208+
document.body.appendChild(nav);
209+
210+
// Event listeners
211+
document.getElementById("sathi-nav-trigger").addEventListener("click", toggle);
212+
document.getElementById(OVERLAY_ID).addEventListener("click", toggle);
213+
214+
// Auto-hide trigger if it covers important UI (optional logic can go here)
215+
}
216+
217+
// Use Timeout to ensure DOM and original app styles are ready
218+
if (document.readyState === 'loading') {
219+
window.addEventListener('scroll', () => {}, {passive: true}); // dummy
220+
window.addEventListener("DOMContentLoaded", () => setTimeout(inject, 1200));
221+
} else {
222+
setTimeout(inject, 1200);
223+
}
224+
})();

0 commit comments

Comments
 (0)