276 lines
9.3 KiB
TypeScript
276 lines
9.3 KiB
TypeScript
'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<NotificationRequest[]>(() =>
|
|
NotificationManager.getNotifications()
|
|
);
|
|
const [showAddForm, setShowAddForm] = useState(false);
|
|
const [selectedLibrary, setSelectedLibrary] = useState('');
|
|
const [selectedPassTypes, setSelectedPassTypes] = useState<string[]>([]);
|
|
const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>(
|
|
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 (
|
|
<div style={{ marginTop: '15px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
|
|
<h4 style={{ marginBottom: '10px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<span>🔔 Notifications ({notifications.length})</span>
|
|
<button
|
|
onClick={() => setShowAddForm(!showAddForm)}
|
|
style={{
|
|
fontSize: '12px',
|
|
padding: '4px 8px',
|
|
backgroundColor: '#4CAF50',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{showAddForm ? 'Cancel' : '+ Add'}
|
|
</button>
|
|
</h4>
|
|
|
|
{/* Notification Permission Status */}
|
|
{WebPushManager.isSupported() && (
|
|
<div style={{
|
|
marginBottom: '10px',
|
|
padding: '8px',
|
|
backgroundColor: notificationPermission === 'granted' ? '#e8f5e8' : '#fff3cd',
|
|
borderRadius: '4px',
|
|
fontSize: '12px',
|
|
border: `1px solid ${notificationPermission === 'granted' ? '#4CAF50' : '#ffc107'}`
|
|
}}>
|
|
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
|
Push Notifications: {notificationPermission === 'granted' ? '✅ Enabled' : '❌ Disabled'}
|
|
</div>
|
|
{notificationPermission !== 'granted' && (
|
|
<div>
|
|
<div style={{ marginBottom: '6px', color: '#666' }}>
|
|
Enable push notifications to get alerts when passes become available.
|
|
</div>
|
|
<button
|
|
onClick={requestNotificationPermission}
|
|
style={{
|
|
fontSize: '11px',
|
|
padding: '4px 8px',
|
|
backgroundColor: '#007cba',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '3px',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
Enable Notifications
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{showAddForm && (
|
|
<div style={{
|
|
backgroundColor: '#f9f9f9',
|
|
padding: '10px',
|
|
borderRadius: '4px',
|
|
marginBottom: '10px',
|
|
fontSize: '13px'
|
|
}}>
|
|
<div style={{ marginBottom: '8px' }}>
|
|
<div style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>
|
|
Library:
|
|
</div>
|
|
<select
|
|
value={selectedLibrary}
|
|
onChange={(e) => setSelectedLibrary(e.target.value)}
|
|
style={{ width: '100%', padding: '4px', fontSize: '12px' }}
|
|
aria-label="Select library"
|
|
>
|
|
<option value="">Select a library...</option>
|
|
{locations.map(loc => (
|
|
<option key={loc.name} value={loc.name}>{loc.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: '8px' }}>
|
|
<div style={{ display: 'block', marginBottom: '4px', fontWeight: 'bold' }}>
|
|
Pass Types:
|
|
</div>
|
|
{['Pass Only', 'Backpack Only', 'Both'].map(type => (
|
|
<div key={type} style={{ display: 'block', fontSize: '12px' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedPassTypes.includes(type)}
|
|
onChange={(e) => handlePassTypeChange(type, e.target.checked)}
|
|
style={{ marginRight: '6px' }}
|
|
id={`passtype-${type}`}
|
|
/>
|
|
<label htmlFor={`passtype-${type}`}>
|
|
{type}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleAddNotification}
|
|
disabled={!selectedLibrary || selectedPassTypes.length === 0}
|
|
style={{
|
|
padding: '6px 12px',
|
|
backgroundColor: selectedLibrary && selectedPassTypes.length > 0 ? '#007cba' : '#ccc',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: selectedLibrary && selectedPassTypes.length > 0 ? 'pointer' : 'not-allowed',
|
|
fontSize: '12px',
|
|
}}
|
|
>
|
|
Add Notification
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{notifications.length > 0 && (
|
|
<>
|
|
<div style={{ maxHeight: '120px', overflowY: 'auto', marginBottom: '8px' }}>
|
|
{notifications.map(notification => (
|
|
<div key={notification.id} style={{
|
|
fontSize: '11px',
|
|
marginBottom: '6px',
|
|
padding: '6px',
|
|
backgroundColor: '#f0f0f0',
|
|
borderRadius: '3px',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center'
|
|
}}>
|
|
<div>
|
|
<div style={{ fontWeight: 'bold' }}>{notification.libraryName}</div>
|
|
<div style={{ color: '#666' }}>
|
|
{notification.passTypes.join(', ')}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => handleRemoveNotification(notification.id)}
|
|
style={{
|
|
backgroundColor: '#f44336',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '2px',
|
|
padding: '2px 6px',
|
|
fontSize: '10px',
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={checkNotifications}
|
|
style={{
|
|
width: '100%',
|
|
padding: '8px',
|
|
backgroundColor: '#ff9800',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer',
|
|
fontSize: '12px',
|
|
fontWeight: 'bold',
|
|
}}
|
|
>
|
|
Check Now
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|