-
Notifications
You must be signed in to change notification settings - Fork 2
Fix duplicate Leaflet map controls on Overview and Deployment pages #508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -725,6 +725,19 @@ const OverViewMap: React.FC<{ | |
|
|
||
| type MobileView = 'map' | 'list' | ||
|
|
||
| // xl breakpoint = 1280px (matches Tailwind xl:) | ||
| const useIsDesktop = () => { | ||
| const [isDesktop, setIsDesktop] = useState(true) | ||
| useEffect(() => { | ||
| const mq = window.matchMedia('(min-width: 1280px)') | ||
| setIsDesktop(mq.matches) | ||
| const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches) | ||
| mq.addEventListener('change', handler) | ||
| return () => mq.removeEventListener('change', handler) | ||
| }, []) | ||
| return isDesktop | ||
| } | ||
|
Comment on lines
+729
to
+739
|
||
|
|
||
| // OverviewPage: NextPage | ||
| const OverviewPage: NextPage = () => { | ||
| const { mapsLoaded } = useGoogleMaps() | ||
|
|
@@ -733,6 +746,7 @@ const OverviewPage: NextPage = () => { | |
| const mounted = useRef(false) | ||
| const { setGlobalModalId } = useGlobalModalId() | ||
| const [mobileView, setMobileView] = useState<MobileView>('map') | ||
| const isDesktop = useIsDesktop() | ||
|
|
||
| useEffect(() => { | ||
| if (!mounted.current) { | ||
|
|
@@ -781,54 +795,55 @@ const OverviewPage: NextPage = () => { | |
| className={styles.content} | ||
| data-testid="vehicle-dashboard" | ||
| > | ||
| {/* Desktop view (keep existing Allotment layout) */} | ||
| <div className="hidden h-full w-full xl:block"> | ||
| <Allotment | ||
| separator | ||
| snap | ||
| defaultSizes={[75, 25]} | ||
| proportionalLayout | ||
| > | ||
| <Allotment.Pane>{primarySection}</Allotment.Pane> | ||
| <Allotment.Pane priority={LayoutPriority.High}> | ||
| {secondarySection} | ||
| </Allotment.Pane> | ||
| </Allotment> | ||
| </div> | ||
|
|
||
| {/* Mobile view (toggle between map and list) */} | ||
| <div className="flex h-full w-full flex-col xl:hidden"> | ||
| <div className="flex shrink-0 items-center gap-2 border-b border-slate-200 bg-white px-4 py-2"> | ||
| <button | ||
| type="button" | ||
| onClick={() => setMobileView('map')} | ||
| className={ | ||
| mobileView === 'map' | ||
| ? 'rounded bg-secondary-300/60 px-3 py-1 text-sm font-bold text-black' | ||
| : 'rounded px-3 py-1 text-sm font-bold text-slate-600' | ||
| } | ||
| > | ||
| Map | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => setMobileView('list')} | ||
| className={ | ||
| mobileView === 'list' | ||
| ? 'rounded bg-secondary-300/60 px-3 py-1 text-sm font-bold text-black' | ||
| : 'rounded px-3 py-1 text-sm font-bold text-slate-600' | ||
| } | ||
| {/* Single map instance: render one layout to avoid duplicate controls */} | ||
| {isDesktop ? ( | ||
| <div className="h-full w-full"> | ||
| <Allotment | ||
| separator | ||
| snap | ||
| defaultSizes={[75, 25]} | ||
| proportionalLayout | ||
| > | ||
| Vehicles | ||
| </button> | ||
| <Allotment.Pane>{primarySection}</Allotment.Pane> | ||
| <Allotment.Pane priority={LayoutPriority.High}> | ||
| {secondarySection} | ||
| </Allotment.Pane> | ||
| </Allotment> | ||
| </div> | ||
|
|
||
| <div className="min-h-0 flex-1 overflow-hidden"> | ||
| {mobileView === 'map' | ||
| ? primarySection | ||
| : secondarySection} | ||
| ) : ( | ||
| <div className="flex h-full w-full flex-col"> | ||
| <div className="flex shrink-0 items-center gap-2 border-b border-slate-200 bg-white px-4 py-2"> | ||
| <button | ||
| type="button" | ||
| onClick={() => setMobileView('map')} | ||
| className={ | ||
| mobileView === 'map' | ||
| ? 'rounded bg-secondary-300/60 px-3 py-1 text-sm font-bold text-black' | ||
| : 'rounded px-3 py-1 text-sm font-bold text-slate-600' | ||
| } | ||
| > | ||
| Map | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={() => setMobileView('list')} | ||
| className={ | ||
| mobileView === 'list' | ||
| ? 'rounded bg-secondary-300/60 px-3 py-1 text-sm font-bold text-black' | ||
| : 'rounded px-3 py-1 text-sm font-bold text-slate-600' | ||
| } | ||
| > | ||
| Vehicles | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="min-h-0 flex-1 overflow-hidden"> | ||
| {mobileView === 'map' | ||
| ? primarySection | ||
| : secondarySection} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </> | ||
| ) : ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,6 +74,18 @@ const DeploymentMap = dynamic(() => import('../../components/DeploymentMap'), { | |
| type AvailableTab = 'vehicle' | 'depth' | null | ||
| type MobileView = 'main' | 'sidebar' | ||
|
|
||
| const useIsDesktop = () => { | ||
| const [isDesktop, setIsDesktop] = useState(true) | ||
|
||
| useEffect(() => { | ||
| const mq = window.matchMedia('(min-width: 1280px)') | ||
| setIsDesktop(mq.matches) | ||
| const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches) | ||
| mq.addEventListener('change', handler) | ||
| return () => mq.removeEventListener('change', handler) | ||
| }, []) | ||
| return isDesktop | ||
| } | ||
|
|
||
| const Vehicle: NextPage = () => { | ||
| const { authenticated } = useTethysApiContext() | ||
| const { mapsLoaded } = useGoogleMaps() | ||
|
|
@@ -94,6 +106,7 @@ const Vehicle: NextPage = () => { | |
| } | ||
|
|
||
| const [mobileView, setMobileView] = useState<MobileView>('main') | ||
| const isDesktop = useIsDesktop() | ||
|
|
||
| const params = (router.query?.deployment ?? []) as string[] | ||
| const vehicleName = params[0] | ||
|
|
@@ -452,27 +465,26 @@ const Vehicle: NextPage = () => { | |
| authenticated={authenticated} | ||
| /> | ||
|
|
||
| {/* Desktop view */} | ||
| <div className="hidden min-h-0 flex-1 xl:flex"> | ||
| <div className={styles.content}> | ||
| <Allotment | ||
| separator | ||
| defaultSizes={[75, 25]} | ||
| className="min-h-0" | ||
| > | ||
| <Allotment.Pane minSize={720}> | ||
| {primarySection} | ||
| </Allotment.Pane> | ||
| <Allotment.Pane minSize={512}> | ||
| {secondarySection} | ||
| </Allotment.Pane> | ||
| </Allotment> | ||
| {/* Single map instance: render one layout to avoid duplicate controls */} | ||
| {isDesktop ? ( | ||
| <div className="min-h-0 flex-1 flex"> | ||
| <div className={styles.content}> | ||
| <Allotment | ||
| separator | ||
| defaultSizes={[75, 25]} | ||
| className="min-h-0" | ||
| > | ||
| <Allotment.Pane minSize={720}> | ||
| {primarySection} | ||
| </Allotment.Pane> | ||
| <Allotment.Pane minSize={512}> | ||
| {secondarySection} | ||
| </Allotment.Pane> | ||
| </Allotment> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Mobile view (toggle between main and sidebar) */} | ||
| <div className="flex min-h-0 flex-1 xl:hidden"> | ||
| <div className={clsx(styles.content, 'flex-col')}> | ||
| ) : ( | ||
| <div className="flex min-h-0 flex-1 flex-col"> | ||
| <div className="flex shrink-0 items-center gap-2 border-b border-slate-200 bg-white px-4 py-2"> | ||
| <button | ||
| type="button" | ||
|
|
@@ -504,7 +516,7 @@ const Vehicle: NextPage = () => { | |
| {mobileView === 'main' ? primarySection : secondarySection} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </Layout> | ||
| </div> | ||
| </SelectedStationsProvider> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useIsDesktop hook defaults to true in useState, then updates to the actual media query value in useEffect. This can cause a hydration mismatch in Next.js SSR: the server renders thinking it's desktop, but on the client the actual media query might indicate mobile. This can lead to React hydration warnings. Consider adding a typeof window check or using a null initial state to avoid SSR issues, or set the initial state to undefined and handle that case in the component logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review. We’re keeping the current approach: useState(true) to avoid hydration mismatches, and we’ll hold off on extracting the hook for now since dash5 is desktop-only. We may revisit when we add mobile support.