From 8094ab21b5b1e93ba55e1b7c3abcef800147bf9f Mon Sep 17 00:00:00 2001 From: MH Hung Date: Tue, 30 Sep 2025 16:38:59 +0800 Subject: [PATCH] feat: adjust layout toggle and map resize handling --- client/src/app/App.tsx | 23 +++++++++- .../src/components/layout/PrimaryLayout.tsx | 42 +++++++++++++++---- client/src/features/map/TravelMap.tsx | 26 ++++++++++-- .../src/features/visits/hooks/useResizeMap.ts | 13 ++++++ client/src/locales/en/translation.json | 4 +- client/src/locales/zh-Hant/translation.json | 4 +- 6 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 client/src/features/visits/hooks/useResizeMap.ts diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index a6bd55e..9b5ac68 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -1,12 +1,33 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { VisitProvider } from '../features/visits/context/VisitProvider'; +import { useCallback, useState } from 'react'; import { PrimaryLayout } from '../components/layout/PrimaryLayout'; import { VisitSidebar } from '../components/layout/VisitSidebar'; import { TravelMap } from '../features/map/TravelMap'; import { VisitModal } from '../components/overlay/VisitModal'; +import { useVisitModalStore } from '../state/useVisitModalStore'; function HomePage() { - return } content={} />; + const openForCreate = useVisitModalStore((state) => state.openForCreate); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + + const handleSidebarToggle = useCallback( + (isOpen: boolean) => { + setIsSidebarOpen(isOpen); + if (!isOpen) { + useVisitModalStore.getState().close(); + } + }, + [] + ); + + return ( + } + content={} + onSidebarToggle={handleSidebarToggle} + /> + ); } export function App() { diff --git a/client/src/components/layout/PrimaryLayout.tsx b/client/src/components/layout/PrimaryLayout.tsx index 0d20068..0b899ab 100644 --- a/client/src/components/layout/PrimaryLayout.tsx +++ b/client/src/components/layout/PrimaryLayout.tsx @@ -1,22 +1,40 @@ -import type { ReactNode } from 'react'; +import { useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { LanguageSwitcher } from './LanguageSwitcher'; interface PrimaryLayoutProps { sidebar: ReactNode; content: ReactNode; + onSidebarToggle?: (isOpen: boolean) => void; } -export function PrimaryLayout({ sidebar, content }: PrimaryLayoutProps) { +export function PrimaryLayout({ sidebar, content, onSidebarToggle }: PrimaryLayoutProps) { const { t } = useTranslation(); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + + useEffect(() => { + onSidebarToggle?.(isSidebarOpen); + }, [isSidebarOpen, onSidebarToggle]); return (
-
-

{t('common.appName')}

-

{t('common.tagline')}

+
+ +
+

{t('common.appName')}

+

{t('common.tagline')}

+
-
- {sidebar} -
+
{content}
diff --git a/client/src/features/map/TravelMap.tsx b/client/src/features/map/TravelMap.tsx index 248b907..0a6d26d 100644 --- a/client/src/features/map/TravelMap.tsx +++ b/client/src/features/map/TravelMap.tsx @@ -1,11 +1,13 @@ import { MapContainer, Marker, Popup, TileLayer, useMapEvents } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import type { LeafletMouseEvent } from 'leaflet'; import { ensureLeafletConfig } from './leafletConfig'; import { useVisitModalStore } from '../../state/useVisitModalStore'; import { useVisitsQuery } from '../visits/hooks/useVisitQueries'; import { useTranslation } from 'react-i18next'; +import { useResizeMap } from '../visits/hooks/useResizeMap'; +import type { Map as LeafletMap } from 'leaflet'; const INITIAL_POSITION: [number, number] = [20, 0]; const INITIAL_ZOOM = 2; @@ -23,7 +25,12 @@ function MapEvents() { return null; } -export function TravelMap() { +interface TravelMapProps { + onTriggerCreate?: (location?: { lat: number; lng: number }) => void; + sidebarOpen?: boolean; +} + +export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProps) { useEffect(() => { ensureLeafletConfig(); }, []); @@ -32,10 +39,21 @@ export function TravelMap() { const openForCreate = useVisitModalStore((state) => state.openForCreate); const { t } = useTranslation(); + const mapRef = useRef(null); + useResizeMap(mapRef, [sidebarOpen]); return (
- + { + mapRef.current = mapInstance; + mapInstance.invalidateSize(); + }} + className="h-full w-full z-0" + center={INITIAL_POSITION} + zoom={INITIAL_ZOOM} + scrollWheelZoom + > openForCreate()} + onClick={() => (onTriggerCreate ? onTriggerCreate() : openForCreate())} > + diff --git a/client/src/features/visits/hooks/useResizeMap.ts b/client/src/features/visits/hooks/useResizeMap.ts new file mode 100644 index 0000000..ac326f7 --- /dev/null +++ b/client/src/features/visits/hooks/useResizeMap.ts @@ -0,0 +1,13 @@ +import { useEffect, type DependencyList } from 'react'; +import type { Map as LeafletMap } from 'leaflet'; +import { useVisitModalStore } from '../../../state/useVisitModalStore'; + +export function useResizeMap(mapRef: React.RefObject, deps: DependencyList = []) { + const isModalOpen = useVisitModalStore((state) => state.isOpen); + + useEffect(() => { + const map = mapRef.current; + if (!map) return; + setTimeout(() => map.invalidateSize(), 0); + }, [mapRef, isModalOpen, ...deps]); +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index be0cb18..8e8e70f 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -18,7 +18,9 @@ "add": "Add Visit", "drawerTitleCreate": "Add Visit", "drawerTitleEdit": "Edit Visit", - "close": "Close" + "close": "Close", + "toggleOpen": "Show sidebar", + "toggleClose": "Hide sidebar" }, "summary": { "title": "Stats", diff --git a/client/src/locales/zh-Hant/translation.json b/client/src/locales/zh-Hant/translation.json index 258855f..813f953 100644 --- a/client/src/locales/zh-Hant/translation.json +++ b/client/src/locales/zh-Hant/translation.json @@ -18,7 +18,9 @@ "add": "新增足跡", "drawerTitleCreate": "新增足跡", "drawerTitleEdit": "編輯足跡", - "close": "關閉" + "close": "關閉", + "toggleOpen": "展開側邊欄", + "toggleClose": "收合側邊欄" }, "summary": { "title": "統計",