From e92c1450b121283a9b0c2d2f8023e6387d957725 Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Sun, 19 Apr 2026 08:02:50 -0400 Subject: [PATCH 01/15] just for marketing/socials to use --- src/components/Map/Map.tsx | 192 ++++++++++++++++++++++++++++++------- src/services/db.ts | 41 +++++++- 2 files changed, 194 insertions(+), 39 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 4f46f46f..532b7e30 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -4,14 +4,21 @@ import { useMap } from '@vis.gl/react-google-maps'; import { usePostHog } from 'posthog-js/react'; -import { type CSSProperties } from 'react'; +import { + type CSSProperties, + useState, + useCallback, + useRef, + 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 YOUR SUPABASE FETCH FUNCTION HERE +import { getBathroomData } from 'services/db.ts'; const style: CSSProperties = { width: '100%', @@ -26,23 +33,93 @@ const Map = () => { const posthog = usePostHog(); const { setSelectedResource } = useSelectedResource(); const { activeSearchLocation } = useActiveSearchLocation(); - const map = useMap(); - const { data: resources } = useActiveResources(); + // --- DATA FETCHING STATE --- + const [dbData, setDbData] = useState<{ + part1: ResourceEntry[]; + part2: ResourceEntry[]; + part3: ResourceEntry[]; + }>({ part1: [], part2: [], part3: [] }); + const [isLoadingData, setIsLoadingData] = useState(true); - const onMarkerClick = (resource: ResourceEntry) => { - setSelectedResource(resource); + // --- ANIMATION STATE --- + const [visibleResources, setVisibleResources] = useState([]); + const [currentPhaseLabel, setCurrentPhaseLabel] = + useState('Ready to visualize'); + const [isPlaying, setIsPlaying] = useState(false); + const timeoutsRef = useRef([]); + + // Fetch data on mount + useEffect(() => { + const loadData = async () => { + try { + setIsLoadingData(true); + const data = await getBathroomData(); + setDbData(data); + } catch (error) { + console.error('Error loading water data:', error); + setCurrentPhaseLabel('Error loading data'); + } finally { + setIsLoadingData(false); + } + }; - if (!map) { - return; - } + loadData(); - map.panTo({ - lat: resource.latitude, - lng: resource.longitude + return () => timeoutsRef.current.forEach(clearTimeout); + }, []); + + const staggerMarkers = ( + newResources: ResourceEntry[], + delayBetweenMarkers = 100 + ): Promise => { + return new Promise(resolve => { + if (!newResources || newResources.length === 0) { + resolve(); + return; + } + + newResources.forEach((resource, index) => { + const timeout = setTimeout(() => { + setVisibleResources(prev => [...prev, resource]); + if (index === newResources.length - 1) { + resolve(); + } + }, index * delayBetweenMarkers); + timeoutsRef.current.push(timeout); + }); }); + }; + + const playTimelapse = useCallback(async () => { + if (isPlaying || isLoadingData) return; + setIsPlaying(true); + setVisibleResources([]); + + setCurrentPhaseLabel('Part 1: 2024'); + await staggerMarkers(dbData.part1, 100); + await new Promise(r => setTimeout(r, 1000)); + + setCurrentPhaseLabel('Part 2: Summer 2025'); + await staggerMarkers(dbData.part2, 80); + + await new Promise(r => setTimeout(r, 1000)); + + setCurrentPhaseLabel('Part 3: Fall 2025 - Jan 2026'); + await staggerMarkers(dbData.part3, 50); + + setTimeout(() => { + setCurrentPhaseLabel('All Resources Mapped!'); + setIsPlaying(false); + }, 1500); + }, [isPlaying, isLoadingData, dbData]); + + 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, @@ -51,31 +128,72 @@ const Map = () => { }; return ( - - {resources?.map((resource, index) => ( - - ))} - - {activeSearchLocation ? ( - - ) : null} - +
+
+

+ {isLoadingData ? 'Loading Data...' : currentPhaseLabel} +

+ {!isPlaying && !isLoadingData && ( + + )} +
+ + + {visibleResources.map((resource, index) => ( + + ))} + + {activeSearchLocation ? ( + + ) : null} + +
); }; diff --git a/src/services/db.ts b/src/services/db.ts index 64f3a8aa..e926a2e6 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -3,15 +3,20 @@ 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'; // 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'; -const databaseApiKey = env.VITE_DB_API_KEY; +const databaseApiKey = + // env.VITE_DB_API_KEY || + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndhbnR5Y2Zibnp6b2NzYnRocXpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzcwNDY2OTgsImV4cCI6MjA1MjYyMjY5OH0.yczsMOx3Y-zsWu-GjYEajIb0yw9fYWEIUglmmfM1zCY'; 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); @@ -139,6 +144,38 @@ export const getContributors = async (): Promise => { 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`); + +// console.log(`Waster data: ${JSON.stringify(data, null, 2)}`); + export const getResourceProviders = async ( resourceId: string ): Promise => { From 9ce2e2e7ebe0950b6de3c1619306d03d5b42061c Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Sun, 19 Apr 2026 08:03:31 -0400 Subject: [PATCH 02/15] just for marketing/socials to use --- src/components/Map/Map.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 532b7e30..d15796be 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -17,7 +17,6 @@ import { type ResourceEntry } from 'types/ResourceEntry'; import useSelectedResource from 'hooks/useSelectedResource'; import useActiveSearchLocation from 'hooks/useActiveSearchLocation'; import ResourceMarker from 'components/ResourceMarker/ResourceMarker'; -// IMPORT YOUR SUPABASE FETCH FUNCTION HERE import { getBathroomData } from 'services/db.ts'; const style: CSSProperties = { @@ -35,7 +34,6 @@ const Map = () => { const { activeSearchLocation } = useActiveSearchLocation(); const map = useMap(); - // --- DATA FETCHING STATE --- const [dbData, setDbData] = useState<{ part1: ResourceEntry[]; part2: ResourceEntry[]; From af0e0fce109ca3561dea40ee25675fae65f48f7f Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Sun, 19 Apr 2026 08:46:27 -0400 Subject: [PATCH 03/15] just for marketing/socials to use --- src/components/Map/Map.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index d15796be..130dbab3 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -176,7 +176,7 @@ const Map = () => { fullscreenControl={false} gestureHandling="greedy" defaultCenter={activeSearchLocation || CITY_HALL_LOCATION} - mapId="DEMO_MAP_ID" + mapId="f0d6405d2136c67be3edaf26" > {visibleResources.map((resource, index) => ( Date: Mon, 20 Apr 2026 16:49:30 -0400 Subject: [PATCH 04/15] quick ui cahnges --- src/components/Map/Map.tsx | 6 +++--- src/services/db.ts | 38 +++++++++++++++++--------------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 130dbab3..e4859ad1 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -95,17 +95,17 @@ const Map = () => { setIsPlaying(true); setVisibleResources([]); - setCurrentPhaseLabel('Part 1: 2024'); + setCurrentPhaseLabel('2024'); await staggerMarkers(dbData.part1, 100); await new Promise(r => setTimeout(r, 1000)); - setCurrentPhaseLabel('Part 2: Summer 2025'); + setCurrentPhaseLabel('Summer 2025'); await staggerMarkers(dbData.part2, 80); await new Promise(r => setTimeout(r, 1000)); - setCurrentPhaseLabel('Part 3: Fall 2025 - Jan 2026'); + setCurrentPhaseLabel('Fall 2025 - Jan 2026'); await staggerMarkers(dbData.part3, 50); setTimeout(() => { diff --git a/src/services/db.ts b/src/services/db.ts index e926a2e6..3b21b633 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -3,13 +3,11 @@ 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'; -const databaseApiKey = - // env.VITE_DB_API_KEY || - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndhbnR5Y2Zibnp6b2NzYnRocXpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzcwNDY2OTgsImV4cCI6MjA1MjYyMjY5OH0.yczsMOx3Y-zsWu-GjYEajIb0yw9fYWEIUglmmfM1zCY'; +const databaseApiKey = env.VITE_DB_API_KEY; const resourceDatabaseName = 'resources'; const contributorDatabaseName = 'airtable_contributors'; const feedbackDatabaseName = 'user_feedbacks'; @@ -144,6 +142,21 @@ export const getContributors = async (): Promise => { return data; }; +export const getResourceProviders = async ( + resourceId: string +): Promise => { + const { data, error } = await supabase + .from(providersDatabaseName) + .select( + 'name, logo_url, url:website_url, resource_providers!inner(resource_id)' + ) + .eq('resource_providers.resource_id', resourceId); + if (error) { + throw error; + } + return data; +}; + export const getBathroomData = async () => { const [part1Res, part2Res, part3Res] = await Promise.all([ supabase.from(bathroom_part1).select('*'), @@ -174,22 +187,5 @@ 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`); -// console.log(`Waster data: ${JSON.stringify(data, null, 2)}`); - -export const getResourceProviders = async ( - resourceId: string -): Promise => { - const { data, error } = await supabase - .from(providersDatabaseName) - .select( - 'name, logo_url, url:website_url, resource_providers!inner(resource_id)' - ) - .eq('resource_providers.resource_id', resourceId); - if (error) { - throw error; - } - return data; -}; - export { supabase }; export default {}; From 3df21471f9f925245630f8dc12511d043c6f2248 Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Tue, 28 Apr 2026 19:17:25 -0400 Subject: [PATCH 05/15] add time line --- src/components/Map/Map.tsx | 175 ++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 20 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index e4859ad1..715f605c 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -27,6 +27,8 @@ const style: CSSProperties = { touchAction: 'none' }; +const TIMELINE_PHASES = ['2024', 'Summer 2025', 'Fall 2025 - Jan 2026']; + const Map = () => { const isMobile = useIsMobile(); const posthog = usePostHog(); @@ -45,10 +47,13 @@ const Map = () => { const [visibleResources, setVisibleResources] = useState([]); const [currentPhaseLabel, setCurrentPhaseLabel] = useState('Ready to visualize'); + + const [currentPhaseIndex, setCurrentPhaseIndex] = useState(-1); + const [timelineProgressPercentage, setTimelineProgressPercentage] = + useState(0); // NEW: Granular progress const [isPlaying, setIsPlaying] = useState(false); const timeoutsRef = useRef([]); - // Fetch data on mount useEffect(() => { const loadData = async () => { try { @@ -68,9 +73,11 @@ const Map = () => { return () => timeoutsRef.current.forEach(clearTimeout); }, []); + // NEW: Added an onProgress callback so the timeline moves with every single marker const staggerMarkers = ( newResources: ResourceEntry[], - delayBetweenMarkers = 100 + delayBetweenMarkers = 100, + onProgress: (phaseCompletionPercentage: number) => void ): Promise => { return new Promise(resolve => { if (!newResources || newResources.length === 0) { @@ -81,6 +88,10 @@ const Map = () => { newResources.forEach((resource, index) => { const timeout = setTimeout(() => { setVisibleResources(prev => [...prev, resource]); + + // Report progress from 0.0 to 1.0 for this specific phase + onProgress((index + 1) / newResources.length); + if (index === newResources.length - 1) { resolve(); } @@ -94,22 +105,39 @@ const Map = () => { if (isPlaying || isLoadingData) return; setIsPlaying(true); setVisibleResources([]); + setTimelineProgressPercentage(0); + + const segmentWidth = 100 / TIMELINE_PHASES.length; // Each phase takes up 33.33% of the bar - setCurrentPhaseLabel('2024'); - await staggerMarkers(dbData.part1, 100); + // Phase 1 + setCurrentPhaseIndex(0); + setCurrentPhaseLabel(TIMELINE_PHASES[0]); + await staggerMarkers(dbData.part1, 100, p => { + setTimelineProgressPercentage(0 * segmentWidth + p * segmentWidth); + }); - await new Promise(r => setTimeout(r, 1000)); + await new Promise(r => setTimeout(r, 800)); - setCurrentPhaseLabel('Summer 2025'); - await staggerMarkers(dbData.part2, 80); + // Phase 2 + setCurrentPhaseIndex(1); + setCurrentPhaseLabel(TIMELINE_PHASES[1]); + await staggerMarkers(dbData.part2, 80, p => { + setTimelineProgressPercentage(1 * segmentWidth + p * segmentWidth); + }); - await new Promise(r => setTimeout(r, 1000)); + await new Promise(r => setTimeout(r, 800)); - setCurrentPhaseLabel('Fall 2025 - Jan 2026'); - await staggerMarkers(dbData.part3, 50); + // Phase 3 + setCurrentPhaseIndex(2); + setCurrentPhaseLabel(TIMELINE_PHASES[2]); + await staggerMarkers(dbData.part3, 50, p => { + setTimelineProgressPercentage(2 * segmentWidth + p * segmentWidth); + }); setTimeout(() => { + setCurrentPhaseIndex(3); setCurrentPhaseLabel('All Resources Mapped!'); + setTimelineProgressPercentage(100); setIsPlaying(false); }, 1500); }, [isPlaying, isLoadingData, dbData]); @@ -135,33 +163,140 @@ const Map = () => { transform: 'translateX(-50%)', zIndex: 10, backgroundColor: 'white', - padding: '15px 25px', - borderRadius: '8px', - boxShadow: '0 4px 6px rgba(0,0,0,0.1)', + padding: '20px 30px', + borderRadius: '12px', + boxShadow: '0 8px 16px rgba(0,0,0,0.15)', textAlign: 'center', display: 'flex', flexDirection: 'column', - gap: '10px', - minWidth: '250px' + gap: '15px', + minWidth: '320px' }} > -

+

{isLoadingData ? 'Loading Data...' : currentPhaseLabel}

+ + {/* TIMELINE UI */} +
+ {/* Background Track */} +
+ + {/* Active Progress Track */} +
+ + {/* Timeline Nodes */} + {TIMELINE_PHASES.map((phase, index) => { + const isActive = currentPhaseIndex >= index; + // Place nodes exactly where the phase segments begin (0%, 33.3%, 66.6%) + const leftPos = `${index * (100 / TIMELINE_PHASES.length)}%`; + + return ( +
+ ); + })} + + {/* Final "Complete" Node at 100% */} +
= 3 ? '#007BFF' : '#e0e0e0', + border: '3px solid white', + transition: 'background-color 0.3s ease-in-out', + boxShadow: + currentPhaseIndex >= 3 + ? '0 0 0 2px rgba(0, 123, 255, 0.2)' + : 'none' + }} + /> +
+ {!isPlaying && !isLoadingData && ( )}
From 7fcb9a0bf14f68382c63634d4efcad6914cc8eff Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Tue, 5 May 2026 19:15:24 -0400 Subject: [PATCH 06/15] just pusing to keep the test link live --- src/components/Map/Map.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 715f605c..69477b28 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -136,7 +136,7 @@ const Map = () => { setTimeout(() => { setCurrentPhaseIndex(3); - setCurrentPhaseLabel('All Resources Mapped!'); + setCurrentPhaseLabel('Resources Mapped!'); setTimelineProgressPercentage(100); setIsPlaying(false); }, 1500); From bb0738e7942684f909897587df98f89d63b32b83 Mon Sep 17 00:00:00 2001 From: Anil Kumar Karapa <90452951+AnilKumar3494@users.noreply.github.com> Date: Mon, 18 May 2026 20:16:03 -0400 Subject: [PATCH 07/15] Update Map.tsx --- src/components/Map/Map.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 69477b28..183659b1 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -19,6 +19,7 @@ import useActiveSearchLocation from 'hooks/useActiveSearchLocation'; import ResourceMarker from 'components/ResourceMarker/ResourceMarker'; import { getBathroomData } from 'services/db.ts'; +// just for pr const style: CSSProperties = { width: '100%', height: '100vh', From 4a00c2c7df4020b61982a8ed8ce927f8fa2e667f Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Fri, 5 Jun 2026 23:16:37 -0400 Subject: [PATCH 08/15] ui updates --- src/components/Map/Map.tsx | 39 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 183659b1..b709f4fc 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -137,7 +137,7 @@ const Map = () => { setTimeout(() => { setCurrentPhaseIndex(3); - setCurrentPhaseLabel('Resources Mapped!'); + setCurrentPhaseLabel(''); setTimelineProgressPercentage(100); setIsPlaying(false); }, 1500); @@ -159,7 +159,7 @@ const Map = () => {
{ minWidth: '320px' }} > -

- {isLoadingData ? 'Loading Data...' : currentPhaseLabel} -

+
+

+ {isLoadingData ? 'Loading Data...' : currentPhaseLabel} +

+ {!isLoadingData && ( +

+ Bathrooms Mapped: {visibleResources.length} +

+ )} +
{/* TIMELINE UI */}
{ (e.currentTarget.style.backgroundColor = '#007BFF') } > - {visibleResources.length > 0 ? 'Replay Timeline' : 'Play Timelapse'} + {visibleResources.length > 0 ? '' : 'Play Timelapse'} )}
From e0eade823cf59200e9b0b2e1e5bdc86f88a2b194 Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Fri, 5 Jun 2026 23:19:28 -0400 Subject: [PATCH 09/15] ui updates --- src/components/Map/Map.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index b709f4fc..ae88e1df 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -19,7 +19,6 @@ import useActiveSearchLocation from 'hooks/useActiveSearchLocation'; import ResourceMarker from 'components/ResourceMarker/ResourceMarker'; import { getBathroomData } from 'services/db.ts'; -// just for pr const style: CSSProperties = { width: '100%', height: '100vh', @@ -74,7 +73,6 @@ const Map = () => { return () => timeoutsRef.current.forEach(clearTimeout); }, []); - // NEW: Added an onProgress callback so the timeline moves with every single marker const staggerMarkers = ( newResources: ResourceEntry[], delayBetweenMarkers = 100, @@ -90,7 +88,6 @@ const Map = () => { const timeout = setTimeout(() => { setVisibleResources(prev => [...prev, resource]); - // Report progress from 0.0 to 1.0 for this specific phase onProgress((index + 1) / newResources.length); if (index === newResources.length - 1) { @@ -108,9 +105,7 @@ const Map = () => { setVisibleResources([]); setTimelineProgressPercentage(0); - const segmentWidth = 100 / TIMELINE_PHASES.length; // Each phase takes up 33.33% of the bar - - // Phase 1 + const segmentWidth = 100 / TIMELINE_PHASES.length; setCurrentPhaseIndex(0); setCurrentPhaseLabel(TIMELINE_PHASES[0]); await staggerMarkers(dbData.part1, 100, p => { @@ -207,7 +202,6 @@ const Map = () => { zIndex: 1 }} > - {/* Background Track */}
{ {/* Timeline Nodes */} {TIMELINE_PHASES.map((phase, index) => { const isActive = currentPhaseIndex >= index; - // Place nodes exactly where the phase segments begin (0%, 33.3%, 66.6%) const leftPos = `${index * (100 / TIMELINE_PHASES.length)}%`; return ( From e0268ec8b1c86007ae8354977a44ab517447df9d Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Tue, 9 Jun 2026 18:25:34 -0400 Subject: [PATCH 10/15] without header and toolbar --- src/components/Head/Head.tsx | 3 ++- src/components/Toolbar/Toolbar.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Head/Head.tsx b/src/components/Head/Head.tsx index 53ed80e8..6995b012 100644 --- a/src/components/Head/Head.tsx +++ b/src/components/Head/Head.tsx @@ -8,7 +8,8 @@ const Head = () => { return ( - {isMobile ? : } + {/* {isMobile ? : } */} + <> ); }; diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx index 5853c229..363212dd 100644 --- a/src/components/Toolbar/Toolbar.tsx +++ b/src/components/Toolbar/Toolbar.tsx @@ -81,10 +81,12 @@ const Toolbar = () => { }; if (isMobile) { - return ; + // return ; + return <>; } - return ; + // return ; + return <>; }; export default Toolbar; From 7ea2783173b2352d84d98cfd075b59a1484725da Mon Sep 17 00:00:00 2001 From: Anil Kumar Karapa <90452951+AnilKumar3494@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:40:46 -0400 Subject: [PATCH 11/15] Modify comment for mobile and desktop head components Updated comment to indicate new link implementation. --- src/components/Head/Head.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Head/Head.tsx b/src/components/Head/Head.tsx index 6995b012..1dbb4f13 100644 --- a/src/components/Head/Head.tsx +++ b/src/components/Head/Head.tsx @@ -8,7 +8,8 @@ const Head = () => { return ( - {/* {isMobile ? : } */} + + {/* {isMobile ? : } comment for new link*/} <> ); From 5237797d21a13f6e08c4e8671d3c9cc1c249b534 Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Tue, 14 Jul 2026 19:23:06 -0400 Subject: [PATCH 12/15] UI Updates --- src/components/Head/Head.tsx | 8 +- src/components/Map/Map.tsx | 264 ++++++++++++++++++----------- src/components/Toolbar/Toolbar.tsx | 6 +- 3 files changed, 172 insertions(+), 106 deletions(-) diff --git a/src/components/Head/Head.tsx b/src/components/Head/Head.tsx index 1dbb4f13..4239a922 100644 --- a/src/components/Head/Head.tsx +++ b/src/components/Head/Head.tsx @@ -1,10 +1,10 @@ -import useIsMobile from 'hooks/useIsMobile'; -import MobileHead from 'components/MobileHead/MobileHead'; +// 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 ( diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index ae88e1df..cb792851 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -7,8 +7,7 @@ import { usePostHog } from 'posthog-js/react'; import { type CSSProperties, useState, - useCallback, - useRef, + useMemo, useEffect } from 'react'; import useIsMobile from 'hooks/useIsMobile'; @@ -29,6 +28,8 @@ const style: CSSProperties = { const TIMELINE_PHASES = ['2024', 'Summer 2025', 'Fall 2025 - Jan 2026']; +const STEP_DELAY = 90; + const Map = () => { const isMobile = useIsMobile(); const posthog = usePostHog(); @@ -43,16 +44,9 @@ const Map = () => { }>({ part1: [], part2: [], part3: [] }); const [isLoadingData, setIsLoadingData] = useState(true); - // --- ANIMATION STATE --- - const [visibleResources, setVisibleResources] = useState([]); - const [currentPhaseLabel, setCurrentPhaseLabel] = - useState('Ready to visualize'); - const [currentPhaseIndex, setCurrentPhaseIndex] = useState(-1); - const [timelineProgressPercentage, setTimelineProgressPercentage] = - useState(0); // NEW: Granular progress + const [currentStep, setCurrentStep] = useState(0); const [isPlaying, setIsPlaying] = useState(false); - const timeoutsRef = useRef([]); useEffect(() => { const loadData = async () => { @@ -62,81 +56,84 @@ const Map = () => { setDbData(data); } catch (error) { console.error('Error loading water data:', error); - setCurrentPhaseLabel('Error loading data'); } finally { setIsLoadingData(false); } }; loadData(); - - return () => timeoutsRef.current.forEach(clearTimeout); }, []); - const staggerMarkers = ( - newResources: ResourceEntry[], - delayBetweenMarkers = 100, - onProgress: (phaseCompletionPercentage: number) => void - ): Promise => { - return new Promise(resolve => { - if (!newResources || newResources.length === 0) { - resolve(); - return; - } - - newResources.forEach((resource, index) => { - const timeout = setTimeout(() => { - setVisibleResources(prev => [...prev, resource]); - - onProgress((index + 1) / newResources.length); - if (index === newResources.length - 1) { - resolve(); - } - }, index * delayBetweenMarkers); - timeoutsRef.current.push(timeout); - }); + 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 playTimelapse = useCallback(async () => { - if (isPlaying || isLoadingData) return; - setIsPlaying(true); - setVisibleResources([]); - setTimelineProgressPercentage(0); - - const segmentWidth = 100 / TIMELINE_PHASES.length; - setCurrentPhaseIndex(0); - setCurrentPhaseLabel(TIMELINE_PHASES[0]); - await staggerMarkers(dbData.part1, 100, p => { - setTimelineProgressPercentage(0 * segmentWidth + p * segmentWidth); - }); + const totalSteps = flatResources.length; - await new Promise(r => setTimeout(r, 800)); + 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]); - // Phase 2 - setCurrentPhaseIndex(1); - setCurrentPhaseLabel(TIMELINE_PHASES[1]); - await staggerMarkers(dbData.part2, 80, p => { - setTimelineProgressPercentage(1 * segmentWidth + p * segmentWidth); - }); + const visibleResources = useMemo( + () => flatResources.slice(0, currentStep).map(f => f.resource), + [flatResources, currentStep] + ); - await new Promise(r => setTimeout(r, 800)); + const activePhaseIndex = useMemo(() => { + if (currentStep <= 0) return -1; + if (currentStep <= phaseEndSteps[0]) return 0; + if (currentStep <= phaseEndSteps[1]) return 1; + return 2; + }, [currentStep, phaseEndSteps]); - // Phase 3 - setCurrentPhaseIndex(2); - setCurrentPhaseLabel(TIMELINE_PHASES[2]); - await staggerMarkers(dbData.part3, 50, p => { - setTimelineProgressPercentage(2 * segmentWidth + p * segmentWidth); - }); + const isComplete = totalSteps > 0 && currentStep >= totalSteps; + const progressPercentage = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0; + + + 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]); - setTimeout(() => { - setCurrentPhaseIndex(3); - setCurrentPhaseLabel(''); - setTimelineProgressPercentage(100); + const handlePlayPause = () => { + if (isLoadingData || totalSteps === 0) return; + if (isPlaying) { setIsPlaying(false); - }, 1500); - }, [isPlaying, isLoadingData, dbData]); + return; + } + if (currentStep >= totalSteps) { + setCurrentStep(0); + } + setIsPlaying(true); + }; + + 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); @@ -149,46 +146,67 @@ 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 (
-
+

- {isLoadingData ? 'Loading Data...' : currentPhaseLabel} + {displayLabel}

{!isLoadingData && (

- Bathrooms Mapped: {visibleResources.length} + Mapped: {visibleResources.length}

)}
@@ -198,7 +216,7 @@ const Map = () => { style={{ position: 'relative', height: '24px', - margin: '5px 10px', + margin: '2px 8px', zIndex: 1 }} > @@ -222,7 +240,7 @@ const Map = () => { position: 'absolute', top: '50%', left: '0', - width: `${timelineProgressPercentage}%`, + width: `${progressPercentage}%`, height: '4px', backgroundColor: '#007BFF', transform: 'translateY(-50%)', @@ -232,15 +250,15 @@ const Map = () => { }} /> - {/* Timeline Nodes */} {TIMELINE_PHASES.map((phase, index) => { - const isActive = currentPhaseIndex >= index; + const isActive = activePhaseIndex >= index; const leftPos = `${index * (100 / TIMELINE_PHASES.length)}%`; return (
seekToStep(phaseEndSteps[index])} style={{ position: 'absolute', left: leftPos, @@ -251,6 +269,7 @@ const Map = () => { 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)' @@ -260,9 +279,9 @@ const Map = () => { ); })} - {/* Final "Complete" Node at 100% */}
seekToStep(totalSteps)} style={{ position: 'absolute', left: '100%', @@ -271,21 +290,43 @@ const Map = () => { width: '16px', height: '16px', borderRadius: '50%', - backgroundColor: currentPhaseIndex >= 3 ? '#007BFF' : '#e0e0e0', + backgroundColor: isComplete ? '#007BFF' : '#e0e0e0', border: '3px solid white', + cursor: 'pointer', transition: 'background-color 0.3s ease-in-out', - boxShadow: - currentPhaseIndex >= 3 - ? '0 0 0 2px rgba(0, 123, 255, 0.2)' - : 'none' + boxShadow: isComplete + ? '0 0 0 2px rgba(0, 123, 255, 0.2)' + : 'none' }} />
- {!isPlaying && !isLoadingData && ( +
+ {TIMELINE_PHASES.map((phase, index) => ( + + ))} +
+ +
- )} + {currentStep > 0 && !isPlaying && ( + + )} +
{ 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; From 023efb432ee3e9437844761ba0eb6bdbba60e433 Mon Sep 17 00:00:00 2001 From: Anilkumar3494 Date: Sat, 18 Jul 2026 06:29:44 -0400 Subject: [PATCH 13/15] timeline improve --- src/components/Map/Map.tsx | 49 +++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index cb792851..660ea0d4 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -96,7 +96,21 @@ const Map = () => { }, [currentStep, phaseEndSteps]); const isComplete = totalSteps > 0 && currentStep >= totalSteps; - const progressPercentage = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0; + + 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]); useEffect(() => { @@ -250,15 +264,17 @@ const Map = () => { }} /> - {TIMELINE_PHASES.map((phase, index) => { - const isActive = activePhaseIndex >= index; + {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(phaseEndSteps[index])} + key={label} + title={`Jump to ${label}`} + onClick={() => seekToStep(step)} style={{ position: 'absolute', left: leftPos, @@ -278,27 +294,6 @@ const Map = () => { /> ); })} - -
seekToStep(totalSteps)} - style={{ - position: 'absolute', - left: '100%', - top: '50%', - transform: 'translate(-50%, -50%)', - width: '16px', - height: '16px', - borderRadius: '50%', - backgroundColor: isComplete ? '#007BFF' : '#e0e0e0', - border: '3px solid white', - cursor: 'pointer', - transition: 'background-color 0.3s ease-in-out', - boxShadow: isComplete - ? '0 0 0 2px rgba(0, 123, 255, 0.2)' - : 'none' - }} - />
Date: Tue, 21 Jul 2026 14:00:09 -0400 Subject: [PATCH 14/15] improve DOT and timeline --- src/components/Map/Map.tsx | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 660ea0d4..5ea9ce68 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -4,12 +4,7 @@ import { useMap } from '@vis.gl/react-google-maps'; import { usePostHog } from 'posthog-js/react'; -import { - type CSSProperties, - useState, - useMemo, - useEffect -} 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'; @@ -44,7 +39,6 @@ const Map = () => { }>({ part1: [], part2: [], part3: [] }); const [isLoadingData, setIsLoadingData] = useState(true); - const [currentStep, setCurrentStep] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -64,7 +58,6 @@ const Map = () => { loadData(); }, []); - const flatResources = useMemo(() => { const parts = [dbData.part1, dbData.part2, dbData.part3]; const flat: { resource: ResourceEntry; phaseIndex: number }[] = []; @@ -83,6 +76,11 @@ const Map = () => { 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] @@ -112,7 +110,6 @@ const Map = () => { return 100; }, [currentStep, totalSteps, dotSteps]); - useEffect(() => { if (!isPlaying || currentStep >= totalSteps) return; const timeout = setTimeout(() => { @@ -341,10 +338,10 @@ const Map = () => { {isPlaying ? 'Pause' : isComplete - ? 'Replay' - : currentStep > 0 - ? 'Resume' - : 'Play Timelapse'} + ? 'Replay' + : currentStep > 0 + ? 'Resume' + : 'Play Timelapse'} {currentStep > 0 && !isPlaying && (