From 729b19bc12133bf9d3d78f47e10487a9cc6fcc62 Mon Sep 17 00:00:00 2001 From: aramesh-site deploy Date: Thu, 1 Oct 2026 15:15:32 +0000 Subject: [PATCH] =?UTF-8?q?feat(map):=20=D8=B2=D9=88=D9=85=E2=80=8C=D8=A7?= =?UTF-8?q?=D9=88=D8=AA=20=D8=A2=D8=B2=D8=A7=D8=AF=20=D8=AA=D8=A7=20=D8=AF?= =?UTF-8?q?=DB=8C=D8=AF=20=DA=A9=D8=B4=D9=88=D8=B1=DB=8C=D8=8C=20=D9=82?= =?UTF-8?q?=D8=A7=D8=A8=20=D9=86=D8=AE=D8=B3=D8=AA=20=D8=B1=D9=88=DB=8C=20?= =?UTF-8?q?=D9=87=D9=85=D9=87=D9=94=20=D9=BE=D8=B1=D9=88=DA=98=D9=87?= =?UTF-8?q?=E2=80=8C=D9=87=D8=A7=20=D8=A8=D8=A7=20fitBounds=D8=8C=20=D9=88?= =?UTF-8?q?=20=D8=AA=D8=B5=D8=A7=D9=88=DB=8C=D8=B1=20=D8=AF=DB=8C=D8=AA?= =?UTF-8?q?=D8=A7=D8=A8=DB=8C=D8=B3=20=D8=AF=D8=B1=20=D9=BE=D8=A7=D9=BE?= =?UTF-8?q?=E2=80=8C=D8=A2=D9=BE=20=D9=86=D9=82=D8=B4=D9=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/map/ProjectsMapAll.tsx | 20 ++++++++++++++++---- frontend/src/map/useMapLibre.ts | 5 ++++- frontend/src/pages/Projects.tsx | 12 ++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/frontend/src/map/ProjectsMapAll.tsx b/frontend/src/map/ProjectsMapAll.tsx index 5956470..2d93d06 100644 --- a/frontend/src/map/ProjectsMapAll.tsx +++ b/frontend/src/map/ProjectsMapAll.tsx @@ -1,5 +1,5 @@ import { createPortal } from 'react-dom' -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import { Link } from 'react-router-dom' import { MdArrowBack, MdOpenInNew } from 'react-icons/md' import { @@ -32,8 +32,8 @@ export type ProjectsMapAllProps = { /** * نمای «همه‌ی پروژه‌ها» — همه‌ی مختصات شیت روی یک نقشه‌ی تمام‌عرض با * سنجاق‌های مسی، پاپ‌آپ کارتی و دو اکشن عمودی: صفحه‌ی پروژه و آدرس نشان. - * پاپ‌آپ فقط با کلیک کاربر باز می‌شود و جابه‌جایی نقشه به محدودهٔ پروژه‌ها - * بسته است. + * پاپ‌آپ فقط با کلیک کاربر باز می‌شود؛ نمای نخست همه‌ی سنجاق‌ها را در قاب + * دارد و زوم‌اوت تا دید کشوری آزاد است. */ export function ProjectsMapAll({ className = '', @@ -43,9 +43,21 @@ export function ProjectsMapAll({ }: ProjectsMapAllProps) { const { t, href } = useLang() const bounds = useMemo(() => boundsForProjects(projects), [projects]) - const { containerRef, map, status, retry } = useMapLibre({ center: ISFAHAN_CENTER, zoom: 12, maxBounds: bounds }) + // بدون maxBounds — زوم‌اوت آزاد تا کف زوم ۵ (دید کشوری)؛ نمای نخست با fitBounds + // همهٔ سنجاق‌ها را در یک قاب می‌نشیند. + const { containerRef, map, status, retry } = useMapLibre({ + center: ISFAHAN_CENTER, + zoom: 12, + minZoom: 5, + }) const { popupHost, activeProject, closePopup } = useProjectMarkers(map, projects) + // نمای نخست: قاب همهٔ پروژه‌ها — بعد از آماده‌شدن نقشه یک‌بار اجرا می‌شود + useEffect(() => { + if (!map || !bounds) return + map.fitBounds(bounds, { padding: 48, maxZoom: 14, duration: 0 }) + }, [map, bounds]) + const projectHref = href((activeProject && resolveProjectHref?.(activeProject)) || '/projects') const images = activeProject ? (imagesFor?.(activeProject) ?? []) : [] diff --git a/frontend/src/map/useMapLibre.ts b/frontend/src/map/useMapLibre.ts index a753fce..0fa64ec 100644 --- a/frontend/src/map/useMapLibre.ts +++ b/frontend/src/map/useMapLibre.ts @@ -12,6 +12,8 @@ type UseMapLibreOptions = { scrollZoom?: boolean /** محدودهٔ مجاز جابه‌جایی نقشه — جلوگیری از رفتن به بیرون محدودهٔ پروژه‌ها */ maxBounds?: maplibregl.LngLatBoundsLike + /** کف بزرگ‌نمایی — زوم‌اوت بی‌معنا روی نقشهٔ خالی را می‌بندد */ + minZoom?: number } /** سقف انتظار سبک نقشه — سرور کاشی بی‌پاسخ یعنی خطای قابل تلاش دوباره، نه اسپینر بی‌پایان */ @@ -36,7 +38,7 @@ const ABSOLUTE_MAX_ZOOM = 22 /** سقف جایگزین وقتی maxzoom منبع در دسترس نیست — برابر آرشیو فعلی (z14 + ۳) */ const FALLBACK_MAX_ZOOM = 17 -export function useMapLibre({ center, zoom, scrollZoom = true, maxBounds }: UseMapLibreOptions) { +export function useMapLibre({ center, zoom, scrollZoom = true, maxBounds, minZoom }: UseMapLibreOptions) { const containerRef = useRef(null) const [map, setMap] = useState(null) const [status, setStatus] = useState('loading') @@ -68,6 +70,7 @@ export function useMapLibre({ center, zoom, scrollZoom = true, maxBounds }: UseM center, zoom, maxBounds, + minZoom, // سقف اولیه تا رسیدن maxzoom واقعی منبع؛ سپس applyZoomLimit دقیق می‌کند maxZoom: FALLBACK_MAX_ZOOM, renderWorldCopies: false, diff --git a/frontend/src/pages/Projects.tsx b/frontend/src/pages/Projects.tsx index fdbcb95..2b4f7aa 100644 --- a/frontend/src/pages/Projects.tsx +++ b/frontend/src/pages/Projects.tsx @@ -81,6 +81,17 @@ export function Projects() { return dbMatch ? `/projects/${dbMatch.slug}` : undefined; }; + // تصاویر پاپ‌آپ نقشه — تصویر شاخص + گالری پروژهٔ متناظر در دیتابیس؛ + // پروژهٔ بدون تصویر یا بدون متناظر، جای‌نگهدار می‌گیرد. + const imagesFor = (mapProject: MapProject): string[] => { + const dbMatch = dbProjects.find( + (p) => p.title === mapProject.name || p.title_en === mapProject.name, + ); + if (!dbMatch) return []; + const gallery = Array.isArray(dbMatch.gallery) ? dbMatch.gallery : []; + return [dbMatch.image, ...gallery].filter((src): src is string => !!src); + }; + return ( <> @@ -92,6 +103,7 @@ export function Projects() {