From 5206aceded925e0a4b09ad045dae26024b7232ba Mon Sep 17 00:00:00 2001 From: Aram Chia Sarafian Date: Sun, 13 Jul 2025 19:12:00 -0700 Subject: [PATCH] Remove notification system and fix data refresh issues - Removed NotificationPanel component and related files - Removed notifications API endpoint and utils - Fixed data refresh functionality with cache-busting - Added refreshing state with better UI feedback - Added no-cache headers to API endpoint - Improved refresh button with loading state --- app/api/availability/route.ts | 13 +- app/api/notifications/route.ts | 32 ---- app/components/LibraryList.tsx | 2 +- app/components/NotificationPanel.tsx | 275 --------------------------- app/page.tsx | 37 ++-- app/utils/location.ts | 89 +++++++++ app/utils/notifications.ts | 182 ------------------ 7 files changed, 128 insertions(+), 502 deletions(-) delete mode 100644 app/api/notifications/route.ts delete mode 100644 app/components/NotificationPanel.tsx create mode 100644 app/utils/location.ts delete mode 100644 app/utils/notifications.ts diff --git a/app/api/availability/route.ts b/app/api/availability/route.ts index a0b4460..f007b8b 100644 --- a/app/api/availability/route.ts +++ b/app/api/availability/route.ts @@ -1,5 +1,9 @@ import { NextResponse } from 'next/server'; +// Force dynamic rendering to prevent caching +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + // Library branch locations with approximate coordinates const LIBRARY_LOCATIONS: Record = { 'Beckwourth': { lat: 32.7157, lng: -117.1611 }, @@ -127,10 +131,17 @@ export async function GET() { const locations = Array.from(locationMap.values()); - return NextResponse.json({ + const response = NextResponse.json({ locations, lastUpdated: new Date().toISOString(), }); + + // Prevent caching to ensure fresh data + response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + response.headers.set('Pragma', 'no-cache'); + response.headers.set('Expires', '0'); + + return response; } catch (error) { console.error('Error in API route:', error); return NextResponse.json( diff --git a/app/api/notifications/route.ts b/app/api/notifications/route.ts deleted file mode 100644 index 6892c37..0000000 --- a/app/api/notifications/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse } from 'next/server'; -import { NotificationManager } from '../../utils/notifications'; - -// This API endpoint can be used for periodic checking -export async function GET() { - try { - const notifications = NotificationManager.getNotifications(); - - // In a real implementation, you might want to: - // 1. Fetch current availability data - // 2. Check against notification preferences - // 3. Send email notifications - // 4. Return results - - return NextResponse.json({ - notificationCount: notifications.length, - notifications: notifications.map(n => ({ - id: n.id, - libraryName: n.libraryName, - passTypes: n.passTypes, - createdAt: n.createdAt, - lastChecked: n.lastChecked, - })), - }); - } catch (error) { - console.error('Error checking notifications:', error); - return NextResponse.json( - { error: 'Failed to check notifications' }, - { status: 500 } - ); - } -} diff --git a/app/components/LibraryList.tsx b/app/components/LibraryList.tsx index 87097a2..77aff65 100644 --- a/app/components/LibraryList.tsx +++ b/app/components/LibraryList.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import { calculateDistance, getCurrentLocation, getCoordinatesFromZip, isValidSanDiegoZip } from '../utils/notifications'; +import { calculateDistance, getCurrentLocation, getCoordinatesFromZip, isValidSanDiegoZip } from '../utils/location'; interface LocationData { name: string; diff --git a/app/components/NotificationPanel.tsx b/app/components/NotificationPanel.tsx deleted file mode 100644 index f7d75f3..0000000 --- a/app/components/NotificationPanel.tsx +++ /dev/null @@ -1,275 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { NotificationManager, type NotificationRequest, WebPushManager } from '../utils/notifications'; - -interface LocationData { - name: string; - lat: number; - lng: number; - available: boolean; - passType: string; -} - -interface NotificationPanelProps { - locations: LocationData[]; -} - -export default function NotificationPanel({ locations }: { readonly locations: LocationData[] }) { - const [notifications, setNotifications] = useState(() => - NotificationManager.getNotifications() - ); - const [showAddForm, setShowAddForm] = useState(false); - const [selectedLibrary, setSelectedLibrary] = useState(''); - const [selectedPassTypes, setSelectedPassTypes] = useState([]); - const [notificationPermission, setNotificationPermission] = useState( - WebPushManager.isSupported() ? WebPushManager.getPermissionStatus() : 'denied' - ); - - const handlePassTypeChange = (type: string, checked: boolean) => { - if (checked) { - setSelectedPassTypes(prev => [...prev, type]); - } else { - setSelectedPassTypes(prev => prev.filter(t => t !== type)); - } - }; - - const requestNotificationPermission = async () => { - const granted = await WebPushManager.requestPermission(); - setNotificationPermission(granted ? 'granted' : 'denied'); - }; - - const handleAddNotification = () => { - if (!selectedLibrary || selectedPassTypes.length === 0) return; - - const newNotification = NotificationManager.addNotification({ - libraryName: selectedLibrary, - passTypes: selectedPassTypes as ('Pass Only' | 'Backpack Only' | 'Both')[], - }); - - setNotifications(prev => [...prev, newNotification]); - setShowAddForm(false); - setSelectedLibrary(''); - setSelectedPassTypes([]); - }; - - const handleRemoveNotification = (id: string) => { - NotificationManager.removeNotification(id); - setNotifications(prev => prev.filter(n => n.id !== id)); - }; - - const checkNotifications = async () => { - const availableNow = notifications.filter(notification => { - const library = locations.find(loc => loc.name === notification.libraryName); - return library && library.available && ( - notification.passTypes.includes('Both') || - notification.passTypes.some(type => library.passType.includes(type.replace(' Only', ''))) - ); - }); - - if (availableNow.length > 0) { - const message = availableNow.map(n => `${n.libraryName} has passes available!`).join('\n'); - - // Try web notification first - if (notificationPermission === 'granted') { - for (const notification of availableNow) { - await WebPushManager.showNotification( - '🎉 Beach Pass Available!', - `${notification.libraryName} has ${notification.passTypes.join(' and ')} available!`, - { - tag: `park-pass-${notification.id}`, - requireInteraction: true, - } - ); - } - } - - // Also show alert for immediate feedback - alert(`🎉 Great news!\n\n${message}`); - - // Update last checked for all notifications - notifications.forEach(n => NotificationManager.updateLastChecked(n.id)); - } else { - alert('No passes currently available for your requested libraries.'); - } - }; - - return ( -
-

- 🔔 Notifications ({notifications.length}) - -

- - {/* Notification Permission Status */} - {WebPushManager.isSupported() && ( -
-
- Push Notifications: {notificationPermission === 'granted' ? '✅ Enabled' : '❌ Disabled'} -
- {notificationPermission !== 'granted' && ( -
-
- Enable push notifications to get alerts when passes become available. -
- -
- )} -
- )} - - {showAddForm && ( -
-
-
- Library: -
- -
- -
-
- Pass Types: -
- {['Pass Only', 'Backpack Only', 'Both'].map(type => ( -
- handlePassTypeChange(type, e.target.checked)} - style={{ marginRight: '6px' }} - id={`passtype-${type}`} - /> - -
- ))} -
- - -
- )} - - {notifications.length > 0 && ( - <> -
- {notifications.map(notification => ( -
-
-
{notification.libraryName}
-
- {notification.passTypes.join(', ')} -
-
- -
- ))} -
- - - - )} -
- ); -} diff --git a/app/page.tsx b/app/page.tsx index 6b181df..8834a74 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,6 @@ import dynamic from 'next/dynamic'; import { useState, useEffect } from 'react'; import LibraryList from './components/LibraryList'; -import NotificationPanel from './components/NotificationPanel'; // Dynamically import the map component to avoid SSR issues const MapComponent = dynamic(() => import('./components/MapComponent'), { @@ -27,6 +26,7 @@ interface ApiResponse { export default function Home() { const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(''); // Start with menu collapsed only on mobile @@ -42,10 +42,22 @@ export default function Home() { fetchAvailability(); }, []); - const fetchAvailability = async () => { + const fetchAvailability = async (isRefresh = false) => { try { - setLoading(true); - const response = await fetch('/api/availability'); + if (isRefresh) { + setRefreshing(true); + } else { + setLoading(true); + } + setError(null); + // Add cache-busting parameter to ensure fresh data + const timestamp = new Date().getTime(); + const response = await fetch(`/api/availability?t=${timestamp}`, { + cache: 'no-cache', + headers: { + 'Cache-Control': 'no-cache', + }, + }); if (!response.ok) { throw new Error('Failed to fetch data'); } @@ -56,7 +68,11 @@ export default function Home() { } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { - setLoading(false); + if (isRefresh) { + setRefreshing(false); + } else { + setLoading(false); + } } }; @@ -111,8 +127,6 @@ export default function Home() { - - {lastUpdated && (

Last updated: {new Date(lastUpdated).toLocaleString()} @@ -120,19 +134,20 @@ export default function Home() { )} )} diff --git a/app/utils/location.ts b/app/utils/location.ts new file mode 100644 index 0000000..746adec --- /dev/null +++ b/app/utils/location.ts @@ -0,0 +1,89 @@ +// Geolocation utilities + +export function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 3959; // Earth's radius in miles + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLng = (lng2 - lng1) * Math.PI / 180; + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLng / 2) * Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +export async function getCurrentLocation(): Promise<{ lat: number; lng: number } | null> { + return new Promise((resolve) => { + if (!navigator.geolocation) { + resolve(null); + return; + } + + navigator.geolocation.getCurrentPosition( + (position) => { + resolve({ + lat: position.coords.latitude, + lng: position.coords.longitude, + }); + }, + () => { + resolve(null); + }, + { timeout: 10000 } + ); + }); +} + +// San Diego ZIP code validation and coordinates +const SAN_DIEGO_ZIP_COORDINATES: Record = { + '92101': { lat: 32.7157, lng: -117.1611 }, // Downtown + '92102': { lat: 32.7030, lng: -117.1289 }, // Logan Heights + '92103': { lat: 32.7328, lng: -117.1461 }, // Core-Columbia + '92104': { lat: 32.7482, lng: -117.0704 }, // College-Rolando + '92105': { lat: 32.7089, lng: -117.1242 }, // Southeastern San Diego + '92106': { lat: 32.7330, lng: -117.1430 }, // Point Loma + '92107': { lat: 32.7469, lng: -117.1978 }, // Ocean Beach + '92108': { lat: 32.7469, lng: -117.1978 }, // Mission Valley + '92109': { lat: 32.7714, lng: -117.1789 }, // Pacific Beach + '92110': { lat: 32.7714, lng: -117.1789 }, // Mission Bay + '92111': { lat: 32.8328, lng: -117.2050 }, // Clairemont + '92113': { lat: 32.6747, lng: -117.0742 }, // Southeastern San Diego + '92114': { lat: 32.7030, lng: -117.1289 }, // Southeastern San Diego + '92115': { lat: 32.7411, lng: -117.1045 }, // City Heights + '92116': { lat: 32.7644, lng: -117.1164 }, // Kensington + '92117': { lat: 32.8328, lng: -117.2050 }, // Clairemont + '92118': { lat: 32.6144, lng: -117.0845 }, // Coronado + '92119': { lat: 32.7482, lng: -117.0704 }, // College Area + '92120': { lat: 32.7482, lng: -117.0704 }, // College Area + '92121': { lat: 32.9340, lng: -117.2340 }, // Sorrento Valley + '92122': { lat: 32.8344, lng: -117.2544 }, // La Jolla + '92123': { lat: 32.7714, lng: -117.1789 }, // Serra Mesa + '92124': { lat: 32.7714, lng: -117.1789 }, // Serra Mesa + '92126': { lat: 32.9286, lng: -117.1311 }, // Carmel Mountain + '92127': { lat: 33.0200, lng: -117.1156 }, // Rancho Bernardo + '92128': { lat: 32.958034, lng: -117.121975 }, // Rancho Penasquitos + '92129': { lat: 32.958034, lng: -117.121975 }, // Rancho Penasquitos + '92130': { lat: 32.9340, lng: -117.2340 }, // Carmel Valley + '92131': { lat: 32.9286, lng: -117.1311 }, // Carmel Mountain + '92132': { lat: 32.8344, lng: -117.2544 }, // La Jolla + '92133': { lat: 32.7157, lng: -117.1611 }, // Naval Base + '92134': { lat: 32.7157, lng: -117.1611 }, // Naval Base + '92135': { lat: 32.7157, lng: -117.1611 }, // Naval Base + '92136': { lat: 32.7330, lng: -117.1430 }, // Point Loma + '92137': { lat: 32.7330, lng: -117.1430 }, // Point Loma + '92138': { lat: 32.6144, lng: -117.0845 }, // Naval Air Station + '92139': { lat: 32.6747, lng: -117.0742 }, // Paradise Hills + '92140': { lat: 32.6144, lng: -117.0845 }, // Coronado + '92145': { lat: 32.7157, lng: -117.1611 }, // Naval Medical Center + '92147': { lat: 32.7330, lng: -117.1430 }, // Point Loma + '92154': { lat: 32.5592, lng: -117.0431 }, // San Ysidro + '92155': { lat: 32.5592, lng: -117.0431 }, // San Ysidro + '92173': { lat: 32.6781, lng: -117.0200 }, // Skyline Hills +}; + +export function isValidSanDiegoZip(zip: string): boolean { + return zip in SAN_DIEGO_ZIP_COORDINATES; +} + +export function getCoordinatesFromZip(zip: string): { lat: number; lng: number } | null { + return SAN_DIEGO_ZIP_COORDINATES[zip] || null; +} diff --git a/app/utils/notifications.ts b/app/utils/notifications.ts deleted file mode 100644 index 00bb174..0000000 --- a/app/utils/notifications.ts +++ /dev/null @@ -1,182 +0,0 @@ -// Simple notification storage using localStorage -export interface NotificationRequest { - id: string; - libraryName: string; - passTypes: ('Pass Only' | 'Backpack Only' | 'Both')[]; - email?: string; // Optional for future email notifications - createdAt: string; - lastChecked?: string; -} - -export class NotificationManager { - private static STORAGE_KEY = 'park-pass-notifications'; - - static getNotifications(): NotificationRequest[] { - if (typeof window === 'undefined') return []; - try { - const stored = localStorage.getItem(this.STORAGE_KEY); - return stored ? JSON.parse(stored) : []; - } catch { - return []; - } - } - - static addNotification(request: Omit): NotificationRequest { - const newRequest: NotificationRequest = { - ...request, - id: Date.now().toString() + Math.random().toString(36).substr(2, 9), - createdAt: new Date().toISOString(), - }; - - const notifications = this.getNotifications(); - notifications.push(newRequest); - - if (typeof window !== 'undefined') { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(notifications)); - } - - return newRequest; - } - - static removeNotification(id: string): void { - const notifications = this.getNotifications().filter(n => n.id !== id); - if (typeof window !== 'undefined') { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(notifications)); - } - } - - static updateLastChecked(id: string): void { - const notifications = this.getNotifications(); - const notification = notifications.find(n => n.id === id); - if (notification) { - notification.lastChecked = new Date().toISOString(); - if (typeof window !== 'undefined') { - localStorage.setItem(this.STORAGE_KEY, JSON.stringify(notifications)); - } - } - } -} - -// Geolocation utilities -export function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { - const R = 3959; // Earth's radius in miles - const dLat = (lat2 - lat1) * Math.PI / 180; - const dLng = (lng2 - lng1) * Math.PI / 180; - const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * - Math.sin(dLng / 2) * Math.sin(dLng / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; -} - -export async function getCurrentLocation(): Promise<{ lat: number; lng: number } | null> { - return new Promise((resolve) => { - if (!navigator.geolocation) { - resolve(null); - return; - } - - navigator.geolocation.getCurrentPosition( - (position) => { - resolve({ - lat: position.coords.latitude, - lng: position.coords.longitude, - }); - }, - () => { - resolve(null); - }, - { timeout: 10000 } - ); - }); -} - -// Web Push Notification utilities -export class WebPushManager { - static async requestPermission(): Promise { - if (!('Notification' in window)) { - console.log('This browser does not support notifications'); - return false; - } - - if (Notification.permission === 'granted') { - return true; - } - - if (Notification.permission === 'denied') { - return false; - } - - const permission = await Notification.requestPermission(); - return permission === 'granted'; - } - - static async showNotification(title: string, body: string, options?: NotificationOptions): Promise { - if (!await this.requestPermission()) { - console.log('Notification permission denied'); - return; - } - - new Notification(title, { - body, - icon: '/favicon.ico', - badge: '/favicon.ico', - tag: 'park-pass-notification', - requireInteraction: true, - ...options - }); - } - - static isSupported(): boolean { - return 'Notification' in window; - } - - static getPermissionStatus(): NotificationPermission { - return Notification.permission; - } -} - -// Zip code to coordinates lookup (San Diego area zip codes) -const ZIP_COORDINATES: Record = { - // Central San Diego - '92101': { lat: 32.7157, lng: -117.1611, area: 'Downtown' }, - '92102': { lat: 32.7081, lng: -117.1367, area: 'Golden Hill' }, - '92103': { lat: 32.7441, lng: -117.1739, area: 'Hillcrest' }, - '92104': { lat: 32.7489, lng: -117.1364, area: 'Normal Heights' }, - '92105': { lat: 32.7089, lng: -117.1242, area: 'City Heights' }, - '92106': { lat: 32.7330, lng: -117.1430, area: 'Point Loma' }, - '92107': { lat: 32.7572, lng: -117.2528, area: 'Ocean Beach' }, - '92108': { lat: 32.7678, lng: -117.1189, area: 'Mission Valley' }, - '92109': { lat: 32.7889, lng: -117.2306, area: 'Pacific Beach' }, - '92110': { lat: 32.7742, lng: -117.1936, area: 'Mission Bay' }, - '92111': { lat: 32.7714, lng: -117.1789, area: 'Linda Vista' }, - '92113': { lat: 32.6781, lng: -117.0200, area: 'Skyline' }, - '92114': { lat: 32.7030, lng: -117.1289, area: 'Logan Heights' }, - '92115': { lat: 32.7328, lng: -117.1461, area: 'Oak Park' }, - '92116': { lat: 32.7644, lng: -117.1164, area: 'Kensington' }, - '92117': { lat: 32.8328, lng: -117.2050, area: 'Clairemont' }, - '92118': { lat: 32.6100, lng: -117.0842, area: 'Coronado' }, - '92119': { lat: 32.7482, lng: -117.0704, area: 'College Area' }, - '92120': { lat: 32.7678, lng: -117.0731, area: 'Del Cerro' }, - '92121': { lat: 32.8797, lng: -117.2072, area: 'Sorrento Valley' }, - '92122': { lat: 32.8544, lng: -117.2144, area: 'University City' }, - '92123': { lat: 32.8100, lng: -117.1350, area: 'Serra Mesa' }, - '92124': { lat: 32.8086, lng: -117.0547, area: 'Tierrasanta' }, - '92126': { lat: 32.8831, lng: -117.1558, area: 'Mira Mesa' }, - '92127': { lat: 32.9581, lng: -117.1089, area: 'Rancho Penasquitos' }, - '92128': { lat: 33.0200, lng: -117.1156, area: 'Rancho Bernardo' }, - '92129': { lat: 32.9584, lng: -117.1253, area: 'Rancho Penasquitos' }, - '92130': { lat: 32.9340, lng: -117.2340, area: 'Carmel Valley' }, - '92131': { lat: 32.9286, lng: -117.1311, area: 'Carmel Mountain' }, - '92132': { lat: 32.6747, lng: -117.0742, area: 'Paradise Hills' }, - '92154': { lat: 32.5592, lng: -117.0431, area: 'San Ysidro' }, - '92173': { lat: 32.6400, lng: -117.0200, area: 'San Diego East' }, -}; - -export function getCoordinatesFromZip(zipCode: string): { lat: number; lng: number; area: string } | null { - return ZIP_COORDINATES[zipCode] || null; -} - -export function isValidSanDiegoZip(zipCode: string): boolean { - return zipCode in ZIP_COORDINATES; -}