import { useMemo, useState, useEffect, useRef } from 'react'; import { useCollection, useNotifications } from '@deplixo/sdk'; import { ParkingGrid } from './components/ParkingGrid.jsx'; import { SpotDetail } from './components/SpotDetail.jsx'; import { Legend } from './components/Legend.jsx'; const TOTAL_SPOTS = 40; const HOURS = Array.from({ length: 24 }, (_, i) => i); const HOUR_LABELS = HOURS.map(hour => `${String(hour).padStart(2, '0')}:00`); const NOTIFICATION_PREF_KEY = 'parktrack_notification_pref'; function getHourFromTimestamp(timestamp) { if (!timestamp) return null; const date = new Date(timestamp); return Number.isNaN(date.getTime()) ? null : date.getHours(); } function getSpotNumber(spotId) { const match = String(spotId ?? '').match(/\d+/); return match ? Number(match[0]) : null; } function getDistanceFromUserSpot(userSpotId, targetSpotId) { const userSpotNumber = getSpotNumber(userSpotId); const targetSpotNumber = getSpotNumber(targetSpotId); if (userSpotNumber === null || targetSpotNumber === null) return null; return Math.abs(userSpotNumber - targetSpotNumber); } function App() { const { items, loading, add, update, remove } = useCollection("parking_spots", { personal: true }); const { notifications, unreadCount, send, markRead } = useNotifications(); const [selectedSpot, setSelectedSpot] = useState(null); const [filter, setFilter] = useState("all"); const [hourView, setHourView] = useState("availability"); const [notificationsEnabled, setNotificationsEnabled] = useState(false); const [notifyRadius, setNotifyRadius] = useState(3); const [userSpotId, setUserSpotId] = useState(1); const [notificationStatus, setNotificationStatus] = useState('idle'); const alertedSpotsRef = useRef(new Set()); useEffect(() => { try { const saved = JSON.parse(window.name || 'null'); if (saved && typeof saved === 'object') { if (typeof saved.notificationsEnabled === 'boolean') setNotificationsEnabled(saved.notificationsEnabled); if (typeof saved.notifyRadius === 'number') setNotifyRadius(saved.notifyRadius); if (typeof saved.userSpotId === 'number') setUserSpotId(saved.userSpotId); } } catch { // no-op } }, []); useEffect(() => { try { window.name = JSON.stringify({ notificationsEnabled, notifyRadius, userSpotId }); } catch { // no-op } }, [notificationsEnabled, notifyRadius, userSpotId]); const spotMap = {}; items.forEach(item => { spotMap[item.value.spotId] = item; }); const takenCount = items.filter(i => i.value.status === "taken").length; const reservedCount = items.filter(i => i.value.status === "reserved").length; const available = TOTAL_SPOTS - takenCount; const taken = takenCount; const reserved = reservedCount; const nearbyAvailableSpots = useMemo(() => { if (!notificationsEnabled) return []; return Array.from({ length: TOTAL_SPOTS }, (_, index) => index + 1) .filter(spotId => { const distance = getDistanceFromUserSpot(userSpotId, spotId); if (distance === null || distance > notifyRadius) return false; const item = spotMap[spotId]; return !item || item.value.status === 'available'; }); }, [notificationsEnabled, notifyRadius, userSpotId, spotMap]); const hourlyAvailability = useMemo(() => { const buckets = HOURS.map(() => ({ total: 0, available: 0, taken: 0, reserved: 0 })); items.forEach(item => { const hour = getHourFromTimestamp(item.value.updatedAt); if (hour === null) return; const bucket = buckets[hour]; bucket.total += 1; if (item.value.status === 'taken') bucket.taken += 1; else if (item.value.status === 'reserved') bucket.reserved += 1; else bucket.available += 1; }); return buckets.map((bucket, hour) => ({ hour, label: HOUR_LABELS[hour], total: bucket.total, available: bucket.available, taken: bucket.taken, reserved: bucket.reserved, percentAvailable: bucket.total > 0 ? Math.round((bucket.available / bucket.total) * 100) : 0, percentTaken: bucket.total > 0 ? Math.round((bucket.taken / bucket.total) * 100) : 0, percentReserved: bucket.total > 0 ? Math.round((bucket.reserved / bucket.total) * 100) : 0, })); }, [items]); const chartPeak = Math.max(1, ...hourlyAvailability.map(h => h.total)); const handleSpotClick = (spotId) => { setSelectedSpot(spotId); }; const handleStatusChange = async (spotId, status, note) => { const existing = spotMap[spotId]; const previousStatus = existing?.value?.status; if (status === "available" && existing) { await remove(existing.id); } else if (existing) { await update(existing.id, { spotId, status, note, updatedAt: Date.now() }); } else if (status !== "available") { await add({ spotId, status, note, updatedAt: Date.now() }); } const wasNotified = alertedSpotsRef.current.has(spotId); const distance = getDistanceFromUserSpot(userSpotId, spotId); const isNearby = distance !== null && distance <= notifyRadius; const becameAvailable = previousStatus && previousStatus !== 'available' && status === 'available'; if (notificationsEnabled && isNearby && becameAvailable && !wasNotified) { alertedSpotsRef.current.add(spotId); try { setNotificationStatus('sending'); await send({ title: 'Nearby spot available', body: `Spot ${spotId} is now available within ${distance} spot(s) of your selected location.`, to: 'me', }); setNotificationStatus('sent'); } catch { setNotificationStatus('error'); } } if (status !== 'available') { alertedSpotsRef.current.delete(spotId); } setSelectedSpot(null); }; const requestNotifications = async () => { setNotificationStatus('requesting'); if (typeof window !== 'undefined' && 'Notification' in window && Notification.permission === 'default') { const permission = await Notification.requestPermission(); if (permission !== 'granted') { setNotificationsEnabled(false); setNotificationStatus('denied'); return; } } setNotificationsEnabled(true); setNotificationStatus('enabled'); }; const disableNotifications = () => { setNotificationsEnabled(false); setNotificationStatus('disabled'); }; if (loading) { return (
🅿️
Loading parking lot...
); } return (
🅿️

ParkTrack

{available} Open
{taken} Taken
{reserved} Reserved
{nearbyAvailableSpots.length} nearby spot{nearbyAvailableSpots.length === 1 ? '' : 's'} available
Notifications {notificationsEnabled ? 'on' : 'off'}

Nearby availability alerts

Get notified when a spot near your selected location becomes available.

{!notificationsEnabled ? ( ) : ( )}
Status: {notificationStatus} Unread notifications: {unreadCount}
{notifications.slice(0, 3).map(n => ( ))}

Availability by hour

Based on current spot activity timestamps in your parking data.

{hourlyAvailability.map((hourData) => { const visibleValue = hourView === 'availability' ? hourData.percentAvailable : hourData.total; const maxValue = hourView === 'availability' ? 100 : chartPeak; const fillHeight = `${Math.max(4, (visibleValue / maxValue) * 100)}%`; return (
{hourData.label} {hourView === 'availability' ? `${hourData.percentAvailable}%` : hourData.total}
A {hourData.available} T {hourData.taken} R {hourData.reserved}
); })}
{selectedSpot !== null && ( setSelectedSpot(null)} /> )}
); } ReactDOM.createRoot(document.getElementById("root")).render();