diff --git a/src/components/Head/Head.tsx b/src/components/Head/Head.tsx index 53ed80e8..f1a7dd69 100644 --- a/src/components/Head/Head.tsx +++ b/src/components/Head/Head.tsx @@ -1,14 +1,17 @@ -import useIsMobile from 'hooks/useIsMobile'; -import MobileHead from 'components/MobileHead/MobileHead'; +// redeploying for test link -- akk +// import useIsMobile from 'hooks/useIsMobile'; +// import MobileHead from 'components/MobileHead/MobileHead'; import { HeaderProvider } from 'contexts/HeaderContext'; // Import the HeaderContext component -import DesktopHead from '../DesktopHead/DesktopHead'; +// import DesktopHead from '../DesktopHead/DesktopHead'; const Head = () => { - const isMobile = useIsMobile(); + // const isMobile = useIsMobile(); return ( - {isMobile ? : } + + {/* {isMobile ? : } comment for new link*/} + <> ); }; diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 4f46f46f..5ea9ce68 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -4,14 +4,14 @@ import { useMap } from '@vis.gl/react-google-maps'; import { usePostHog } from 'posthog-js/react'; -import { type CSSProperties } from 'react'; +import { type CSSProperties, useState, useMemo, useEffect } from 'react'; import useIsMobile from 'hooks/useIsMobile'; import { CITY_HALL_LOCATION } from 'constants/defaults'; import { type ResourceEntry } from 'types/ResourceEntry'; import useSelectedResource from 'hooks/useSelectedResource'; -import useActiveResources from 'hooks/useActiveResources'; import useActiveSearchLocation from 'hooks/useActiveSearchLocation'; import ResourceMarker from 'components/ResourceMarker/ResourceMarker'; +import { getBathroomData } from 'services/db.ts'; const style: CSSProperties = { width: '100%', @@ -21,28 +21,135 @@ const style: CSSProperties = { touchAction: 'none' }; +const TIMELINE_PHASES = ['2024', 'Summer 2025', 'Fall 2025 - Jan 2026']; + +const STEP_DELAY = 90; + const Map = () => { const isMobile = useIsMobile(); const posthog = usePostHog(); const { setSelectedResource } = useSelectedResource(); const { activeSearchLocation } = useActiveSearchLocation(); - const map = useMap(); - const { data: resources } = useActiveResources(); + const [dbData, setDbData] = useState<{ + part1: ResourceEntry[]; + part2: ResourceEntry[]; + part3: ResourceEntry[]; + }>({ part1: [], part2: [], part3: [] }); + const [isLoadingData, setIsLoadingData] = useState(true); - const onMarkerClick = (resource: ResourceEntry) => { - setSelectedResource(resource); + const [currentStep, setCurrentStep] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + const loadData = async () => { + try { + setIsLoadingData(true); + const data = await getBathroomData(); + setDbData(data); + } catch (error) { + console.error('Error loading water data:', error); + } finally { + setIsLoadingData(false); + } + }; + + loadData(); + }, []); + + const flatResources = useMemo(() => { + const parts = [dbData.part1, dbData.part2, dbData.part3]; + const flat: { resource: ResourceEntry; phaseIndex: number }[] = []; + parts.forEach((part, phaseIndex) => { + part.forEach(resource => flat.push({ resource, phaseIndex })); + }); + return flat; + }, [dbData]); + + const totalSteps = flatResources.length; + + const phaseEndSteps = useMemo(() => { + const c1 = dbData.part1.length; + const c2 = c1 + dbData.part2.length; + const c3 = c2 + dbData.part3.length; + return [c1, c2, c3]; + }, [dbData]); + + // The step each dot on the track represents: dot 0 is the start, and dot + // i+1 is the end of phase i. Chips seek to these same steps, so clicking a + // chip always lands the progress bar exactly on the next dot. + const dotSteps = useMemo(() => [0, ...phaseEndSteps], [phaseEndSteps]); + + const visibleResources = useMemo( + () => flatResources.slice(0, currentStep).map(f => f.resource), + [flatResources, currentStep] + ); + + const activePhaseIndex = useMemo(() => { + if (currentStep <= 0) return -1; + if (currentStep <= phaseEndSteps[0]) return 0; + if (currentStep <= phaseEndSteps[1]) return 1; + return 2; + }, [currentStep, phaseEndSteps]); + + const isComplete = totalSteps > 0 && currentStep >= totalSteps; + + const progressPercentage = useMemo(() => { + if (totalSteps === 0 || currentStep <= 0) return 0; + const segment = 100 / TIMELINE_PHASES.length; + for (let i = 0; i < TIMELINE_PHASES.length; i++) { + const start = dotSteps[i]; + const end = dotSteps[i + 1]; + if (currentStep <= end) { + const span = end - start; + const withinPhase = span > 0 ? (currentStep - start) / span : 1; + return i * segment + withinPhase * segment; + } + } + return 100; + }, [currentStep, totalSteps, dotSteps]); - if (!map) { + useEffect(() => { + if (!isPlaying || currentStep >= totalSteps) return; + const timeout = setTimeout(() => { + setCurrentStep(s => { + const next = s + 1; + if (next >= totalSteps) setIsPlaying(false); + return next; + }); + }, STEP_DELAY); + return () => clearTimeout(timeout); + }, [isPlaying, currentStep, totalSteps]); + + const handlePlayPause = () => { + if (isLoadingData || totalSteps === 0) return; + if (isPlaying) { + setIsPlaying(false); return; } + if (currentStep >= totalSteps) { + setCurrentStep(0); + } + setIsPlaying(true); + }; - map.panTo({ - lat: resource.latitude, - lng: resource.longitude - }); + const seekToStep = (step: number) => { + setIsPlaying(false); + setCurrentStep(step); + }; + const displayLabel = useMemo(() => { + if (isLoadingData) return 'Loading Data...'; + if (currentStep === 0) return 'Ready to visualize'; + if (isComplete) return 'Complete'; + return TIMELINE_PHASES[activePhaseIndex] ?? ''; + }, [isLoadingData, currentStep, isComplete, activePhaseIndex]); + + const onMarkerClick = (resource: ResourceEntry) => { + setSelectedResource(resource); + if (!map) return; + map.panTo({ lat: resource.latitude, lng: resource.longitude }); posthog.capture('LocationClicked', { resourceType: resource.resource_type, name: resource.name, @@ -50,32 +157,239 @@ const Map = () => { }); }; + const chipStyle = (active: boolean): CSSProperties => ({ + padding: '5px 10px', + fontSize: '0.78rem', + fontWeight: 600, + borderRadius: '999px', + border: active ? '1px solid #007BFF' : '1px solid #d0d5dd', + backgroundColor: active ? '#007BFF' : 'white', + color: active ? 'white' : '#60718C', + cursor: 'pointer', + whiteSpace: 'nowrap', + transition: 'all 0.15s ease' + }); + return ( - - {resources?.map((resource, index) => ( - - ))} - - {activeSearchLocation ? ( - - ) : null} - +
+
+
+

+ {displayLabel} +

+ {!isLoadingData && ( +

+ Mapped: {visibleResources.length} +

+ )} +
+ + {/* TIMELINE UI */} +
+
+ + {/* Active Progress Track */} +
+ + {dotSteps.map((step, index) => { + const isActive = currentStep > 0 && currentStep >= step; + const leftPos = `${index * (100 / TIMELINE_PHASES.length)}%`; + const label = + index === 0 ? 'Start' : `End of ${TIMELINE_PHASES[index - 1]}`; + + return ( +
seekToStep(step)} + style={{ + position: 'absolute', + left: leftPos, + top: '50%', + transform: 'translate(-50%, -50%)', + width: '16px', + height: '16px', + borderRadius: '50%', + backgroundColor: isActive ? '#007BFF' : '#e0e0e0', + border: '3px solid white', + cursor: 'pointer', + transition: 'background-color 0.3s ease-in-out', + boxShadow: isActive + ? '0 0 0 2px rgba(0, 123, 255, 0.2)' + : 'none' + }} + /> + ); + })} +
+ +
+ {TIMELINE_PHASES.map((phase, index) => ( + + ))} +
+ +
+ + {currentStep > 0 && !isPlaying && ( + + )} +
+
+ + + {visibleResources.map((resource, index) => ( + + ))} + + {activeSearchLocation ? ( + + ) : null} + +
); }; diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx index 5853c229..5ebb7b43 100644 --- a/src/components/Toolbar/Toolbar.tsx +++ b/src/components/Toolbar/Toolbar.tsx @@ -1,7 +1,7 @@ import useIsMobile from 'hooks/useIsMobile'; import getClosest from 'utils/getClosest'; -import MobileToolbar from './MobileToolbar'; -import DesktopToolbar from './DesktopToolbar'; +// import MobileToolbar from './MobileToolbar'; +// import DesktopToolbar from './DesktopToolbar'; import useSelectedResource from 'hooks/useSelectedResource'; import useActiveResources from 'hooks/useActiveResources'; import useActiveSearchLocation from 'hooks/useActiveSearchLocation'; @@ -22,6 +22,8 @@ const Toolbar = () => { useActiveSearchLocation(); const { setToolbarModal } = useToolbarContext(); + // Kept for when the toolbar is re-enabled (passed to Mobile/DesktopToolbar). + // eslint-disable-next-line @typescript-eslint/no-unused-vars const onNearMeClick = async () => { const [userLocation, isLocationServiceDisabled] = await getUserLocation(); let location: google.maps.LatLngLiteral; @@ -81,10 +83,12 @@ const Toolbar = () => { }; if (isMobile) { - return ; + // return ; + return <>; } - return ; + // return ; + return <>; }; export default Toolbar; diff --git a/src/services/db.ts b/src/services/db.ts index 64f3a8aa..3b21b633 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -3,7 +3,7 @@ import type { Provider, ResourceEntry } from 'types/ResourceEntry'; import type { ResourceTypeOption } from 'hooks/useResourceType'; import type { Contributor } from 'types/Contributor'; import type { FeedbackForm } from 'types/FeedbackEntry'; -import { env } from 'config'; +import { env } from 'config.ts'; // Need access to the database? Please refer to .example.env and message us in the #phlask-data channel on Slack const databaseUrl = 'https://wantycfbnzzocsbthqzs.supabase.co'; @@ -12,6 +12,9 @@ const resourceDatabaseName = 'resources'; const contributorDatabaseName = 'airtable_contributors'; const feedbackDatabaseName = 'user_feedbacks'; const providersDatabaseName = 'providers'; +const bathroom_part1 = 'bathroom_part1'; +const bathroom_part2 = 'bathroom_part2'; +const bathroom_part3 = 'bathroom_part3'; const supabase = createClient(databaseUrl, databaseApiKey); @@ -154,5 +157,35 @@ export const getResourceProviders = async ( return data; }; +export const getBathroomData = async () => { + const [part1Res, part2Res, part3Res] = await Promise.all([ + supabase.from(bathroom_part1).select('*'), + supabase.from(bathroom_part2).select('*'), + supabase.from(bathroom_part3).select('*') + ]); + + if (part1Res.error || part2Res.error || part3Res.error) { + throw new Error( + `Failed to fetch water data: ${ + part1Res.error?.message || + part2Res.error?.message || + part3Res.error?.message + }` + ); + } + + return { + part1: part1Res.data || [], + part2: part2Res.data || [], + part3: part3Res.data || [] + }; +}; + +const data = await getBathroomData(); + +console.log(`Part 1 has ${data.part1.length} items`); +console.log(`Part 2 has ${data.part2.length} items`); +console.log(`Part 3 has ${data.part3.length} items`); + export { supabase }; export default {};