Add PWA support and fix PO BOX display in address

- Add comprehensive PWA configuration with manifest, service worker, and meta tags
- Replace Android APK download with cross-platform PWA installation
- Fix church address to include PO BOX information from API
- Update contact page and footer to properly display multi-line addresses
- Enable native app-like experience on all platforms without app store requirements
This commit is contained in:
Benjamin Slingo 2025-08-26 16:49:47 -04:00
parent 5f6430a4ee
commit 16caf6c3c4
7 changed files with 280 additions and 16 deletions

View file

@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#d4af37" />
<stop offset="100%" stop-color="#b8941f" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="256" cy="256" r="240" fill="#1a1a1a" stroke="url(#gradient)" stroke-width="8"/>
<!-- Church building -->
<rect x="180" y="300" width="152" height="120" fill="url(#gradient)" rx="4"/>
<!-- Church roof -->
<polygon points="256,200 160,280 352,280" fill="#d4af37"/>
<!-- Cross on top -->
<rect x="248" y="160" width="16" height="60" fill="#fff" rx="2"/>
<rect x="234" y="174" width="44" height="16" fill="#fff" rx="2"/>
<!-- Door -->
<rect x="236" y="340" width="40" height="80" fill="#1a1a1a" rx="20"/>
<!-- Windows -->
<rect x="200" y="320" width="24" height="32" fill="#1a1a1a" rx="4"/>
<rect x="288" y="320" width="24" height="32" fill="#1a1a1a" rx="4"/>
<!-- Bell tower -->
<rect x="230" y="220" width="52" height="80" fill="url(#gradient)" rx="4"/>
<rect x="238" y="210" width="36" height="20" fill="#d4af37" rx="2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,63 @@
{
"name": "Rockville Tolland SDA Church",
"short_name": "RTSDA Church",
"description": "Official website and app for Rockville Tolland Seventh-day Adventist Church",
"start_url": "/",
"display": "standalone",
"background_color": "#1a1a1a",
"theme_color": "#d4af37",
"orientation": "portrait-primary",
"categories": ["religion", "lifestyle", "education"],
"lang": "en-US",
"scope": "/",
"icons": [
{
"src": "/favicon.svg",
"sizes": "72x72",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/favicon.svg",
"sizes": "96x96",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/favicon.svg",
"sizes": "128x128",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/favicon.svg",
"sizes": "144x144",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/favicon.svg",
"sizes": "152x152",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/favicon.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "any maskable"
},
{
"src": "/favicon.svg",
"sizes": "384x384",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/favicon.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}

View file

@ -0,0 +1,43 @@
const CACHE_NAME = 'rtsda-church-v1';
const urlsToCache = [
'/',
'/about',
'/contact',
'/sermons',
'/events',
'/live',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});

View file

@ -149,7 +149,11 @@ const currentYear = new Date().getFullYear();
{address && (
<div class="flex items-start space-x-2 text-sm text-gray-300">
<i data-lucide="map-pin" class="w-4 h-4 text-gold-400 mt-0.5 flex-shrink-0"></i>
<span>{address}</span>
<div class="leading-relaxed">
{address.split('\n').map(line => (
<div class="mb-1 last:mb-0">{line}</div>
))}
</div>
</div>
)}
{phone && (
@ -222,17 +226,15 @@ const currentYear = new Date().getFullYear();
</div>
</a>
<!-- Android APK Download -->
<button onclick="downloadApk()" class="app-download-btn w-full text-left">
<!-- PWA Installation -->
<button onclick="installPWA()" class="app-download-btn w-full text-left">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-primary-600 to-primary-700 rounded-lg flex items-center justify-center">
<svg viewBox="0 0 24 24" class="w-4 h-4 fill-gold-400">
<path d="M17.6 9.48l1.84-3.18c.16-.31.04-.69-.26-.85a.637.637 0 0 0-.83.22l-1.88 3.24a11.463 11.463 0 0 0-8.94 0L5.65 5.67a.643.643 0 0 0-.87-.2c-.28.18-.37.54-.22.83L6.4 9.48A10.78 10.78 0 0 0 1 18h22a10.78 10.78 0 0 0-5.4-8.52zM7 15.25a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5zm10 0a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5z"/>
</svg>
<i data-lucide="smartphone" class="w-4 h-4 text-gold-400"></i>
</div>
<div class="text-left">
<div class="text-xs text-gray-400">Download APK</div>
<div class="text-sm font-medium text-white">Android</div>
<div class="text-xs text-gray-400">Install as App</div>
<div class="text-sm font-medium text-white">Mobile & Desktop</div>
</div>
</div>
</button>
@ -279,8 +281,71 @@ const currentYear = new Date().getFullYear();
</style>
<script is:inline>
// Make downloadApk function globally available
window.downloadApk = function() {
window.location.href = 'https://api.rockvilletollandsda.church/uploads/rtsda_android/current';
let deferredPrompt = null;
// Listen for the beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (e) => {
console.log('beforeinstallprompt event fired');
e.preventDefault();
deferredPrompt = e;
});
// PWA installation handler
window.installPWA = function() {
if (deferredPrompt) {
// Show the install prompt
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt');
} else {
console.log('User dismissed the install prompt');
}
deferredPrompt = null;
});
} else {
// Show manual instructions for different platforms
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
let instructions = '';
if (isIOS) {
instructions = `To install this app on iOS:
1. Tap the Share button (□↗) in Safari
2. Scroll down and tap "Add to Home Screen"
3. Tap "Add" to confirm
4. The app will appear on your home screen
This creates a full-screen app experience!`;
} else if (isMobile) {
instructions = `To install this app on Android:
1. Open this website in Chrome browser
2. Tap the menu (⋮) in the top right
3. Select "Add to Home screen" or "Install app"
4. Follow the prompts to install
5. The app will appear on your home screen
This creates a full-screen app experience!`;
} else {
instructions = `To install this app on Desktop:
Chrome/Edge:
1. Look for the install icon (⊕) in the address bar
2. Click it and select "Install"
3. The app will open in its own window
Firefox/Safari:
1. Click the menu button
2. Look for "Install" or "Add to Home Screen" option
3. Follow the prompts to install
This creates a dedicated app window without browser tabs!`;
}
alert(instructions);
}
};
</script>

View file

@ -16,6 +16,29 @@ const { title, description = 'Proclaiming the Three Angels\' Message with love a
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#d4af37" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="RTSDA Church" />
<meta name="application-name" content="RTSDA Church" />
<meta name="msapplication-TileColor" content="#d4af37" />
<!-- Web App Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="152x152" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="144x144" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="120x120" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="114x114" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="76x76" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="72x72" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="60x60" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="57x57" href="/favicon.svg" />
<!-- Preload Critical Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@ -117,6 +140,19 @@ const { title, description = 'Proclaiming the Three Angels\' Message with love a
document.querySelectorAll('[data-animate]').forEach(el => {
observer.observe(el);
});
// Register Service Worker for PWA
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered: ', registration);
})
.catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
});
});
}
});
</script>
</body>

View file

@ -18,6 +18,7 @@ try {
address = getChurchAddress();
phone = getContactPhone();
// Get the base email from church-core and make it dynamic based on current domain
const baseEmail = getContactEmail();
const currentUrl = Astro.url.hostname;
@ -152,7 +153,11 @@ try {
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Visit Us</h3>
<p class="text-gray-600 dark:text-gray-300">{address}</p>
<div class="text-gray-600 dark:text-gray-300">
{address.split('\n').map(line => (
<p class="mb-1 last:mb-0">{line}</p>
))}
</div>
</div>
</div>
)}

View file

@ -893,11 +893,31 @@ pub fn get_donation_url() -> String {
.unwrap_or_else(|| "https://adventistgiving.org/donate/ANRTOL".to_string())
}
/// Get church address with fallback
/// Get church address with fallback - includes PO BOX if available
pub fn get_church_address() -> String {
get_cached_config()
.and_then(|config| config.church_address)
.unwrap_or_else(|| "115 Snipsic Lake Road, Tolland, CT 06084".to_string())
// Get the raw config JSON and parse it directly
let config_json = fetch_config_json();
if let Ok(config_value) = serde_json::from_str::<serde_json::Value>(&config_json) {
let mut address_lines = Vec::new();
// Add physical address if available
if let Some(church_address) = config_value.get("church_address").and_then(|v| v.as_str()) {
address_lines.push(church_address.to_string());
}
// Add PO BOX as second line if available
if let Some(po_box) = config_value.get("po_box").and_then(|v| v.as_str()) {
address_lines.push(po_box.to_string());
}
if !address_lines.is_empty() {
return address_lines.join("\n");
}
}
// Fallback if no config available
"9 Hartford Tpke Tolland CT 06084".to_string()
}
/// Get coordinates from config coordinates object or return empty vector