feat: adjust layout toggle and map resize handling

This commit is contained in:
2025-09-30 16:38:59 +08:00
parent ac298047dd
commit 8094ab21b5
6 changed files with 97 additions and 15 deletions

View File

@@ -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 <PrimaryLayout sidebar={<VisitSidebar />} content={<TravelMap />} />;
const openForCreate = useVisitModalStore((state) => state.openForCreate);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const handleSidebarToggle = useCallback(
(isOpen: boolean) => {
setIsSidebarOpen(isOpen);
if (!isOpen) {
useVisitModalStore.getState().close();
}
},
[]
);
return (
<PrimaryLayout
sidebar={<VisitSidebar />}
content={<TravelMap onTriggerCreate={openForCreate} sidebarOpen={isSidebarOpen} />}
onSidebarToggle={handleSidebarToggle}
/>
);
}
export function App() {

View File

@@ -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 (
<div className="flex h-screen flex-col">
<header className="border-b border-slate-200 bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div>
<h1 className="text-2xl font-bold">{t('common.appName')}</h1>
<p className="text-sm text-slate-500">{t('common.tagline')}</p>
<div className="flex items-center gap-3">
<button
type="button"
aria-label={t(isSidebarOpen ? 'sidebar.toggleClose' : 'sidebar.toggleOpen')}
aria-controls="visit-sidebar"
aria-expanded={isSidebarOpen}
className="rounded bg-slate-100 p-2 text-slate-600 transition hover:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-primary"
onClick={() => setIsSidebarOpen((prev) => !prev)}
>
<span className="block text-xl leading-none">{isSidebarOpen ? '⟨' : '☰'}</span>
</button>
<div>
<h1 className="text-2xl font-bold">{t('common.appName')}</h1>
<p className="text-sm text-slate-500">{t('common.tagline')}</p>
</div>
</div>
<div className="flex items-center gap-4">
<nav className="flex items-center gap-3 text-sm text-slate-500">
@@ -32,9 +50,17 @@ export function PrimaryLayout({ sidebar, content }: PrimaryLayoutProps) {
</div>
</header>
<main className="flex flex-1 overflow-hidden">
<section className="w-full max-w-lg border-r border-slate-200">
{sidebar}
</section>
<aside
id="visit-sidebar"
aria-hidden={!isSidebarOpen}
className={`flex-shrink-0 min-w-0 overflow-hidden transition-[flex-basis,max-width] duration-300 ease-in-out ${
isSidebarOpen
? 'basis-[25%] max-w-[25%] border-r border-slate-200'
: 'pointer-events-none basis-0 max-w-0'
}`}
>
{isSidebarOpen ? <div className="h-full">{sidebar}</div> : null}
</aside>
<section className="flex-1">{content}</section>
</main>
</div>

View File

@@ -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<LeafletMap | null>(null);
useResizeMap(mapRef, [sidebarOpen]);
return (
<div className="relative h-full w-full">
<MapContainer className="h-full w-full z-0" center={INITIAL_POSITION} zoom={INITIAL_ZOOM} scrollWheelZoom>
<MapContainer
whenCreated={(mapInstance) => {
mapRef.current = mapInstance;
mapInstance.invalidateSize();
}}
className="h-full w-full z-0"
center={INITIAL_POSITION}
zoom={INITIAL_ZOOM}
scrollWheelZoom
>
<TileLayer
attribution='\u00a9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@@ -60,7 +78,7 @@ export function TravelMap() {
type="button"
aria-label={t('sidebar.add')}
className="absolute bottom-5 right-5 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-2xl font-bold text-white shadow-lg transition hover:bg-primary/90"
onClick={() => openForCreate()}
onClick={() => (onTriggerCreate ? onTriggerCreate() : openForCreate())}
>
+
</button>

View File

@@ -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<LeafletMap | null>, deps: DependencyList = []) {
const isModalOpen = useVisitModalStore((state) => state.isOpen);
useEffect(() => {
const map = mapRef.current;
if (!map) return;
setTimeout(() => map.invalidateSize(), 0);
}, [mapRef, isModalOpen, ...deps]);
}

View File

@@ -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",

View File

@@ -18,7 +18,9 @@
"add": "新增足跡",
"drawerTitleCreate": "新增足跡",
"drawerTitleEdit": "編輯足跡",
"close": "關閉"
"close": "關閉",
"toggleOpen": "展開側邊欄",
"toggleClose": "收合側邊欄"
},
"summary": {
"title": "統計",