feat(map): زوم‌اوت آزاد تا دید کشوری، قاب نخست روی همهٔ پروژه‌ها با fitBounds، و تصاویر دیتابیس در پاپ‌آپ نقشه

This commit is contained in:
aramesh-site deploy
2026-10-01 15:15:32 +00:00
parent 6f1f42ba7a
commit 729b19bc12
3 changed files with 32 additions and 5 deletions
+16 -4
View File
@@ -1,5 +1,5 @@
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { MdArrowBack, MdOpenInNew } from 'react-icons/md' import { MdArrowBack, MdOpenInNew } from 'react-icons/md'
import { import {
@@ -32,8 +32,8 @@ export type ProjectsMapAllProps = {
/** /**
* نمای «همه‌ی پروژه‌ها» — همه‌ی مختصات شیت روی یک نقشه‌ی تمام‌عرض با * نمای «همه‌ی پروژه‌ها» — همه‌ی مختصات شیت روی یک نقشه‌ی تمام‌عرض با
* سنجاق‌های مسی، پاپ‌آپ کارتی و دو اکشن عمودی: صفحه‌ی پروژه و آدرس نشان. * سنجاق‌های مسی، پاپ‌آپ کارتی و دو اکشن عمودی: صفحه‌ی پروژه و آدرس نشان.
* پاپ‌آپ فقط با کلیک کاربر باز می‌شود و جابه‌جایی نقشه به محدودهٔ پروژه‌ها * پاپ‌آپ فقط با کلیک کاربر باز می‌شود؛ نمای نخست همه‌ی سنجاق‌ها را در قاب
* بسته است. * دارد و زوم‌اوت تا دید کشوری آزاد است.
*/ */
export function ProjectsMapAll({ export function ProjectsMapAll({
className = '', className = '',
@@ -43,9 +43,21 @@ export function ProjectsMapAll({
}: ProjectsMapAllProps) { }: ProjectsMapAllProps) {
const { t, href } = useLang() const { t, href } = useLang()
const bounds = useMemo(() => boundsForProjects(projects), [projects]) 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) 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 projectHref = href((activeProject && resolveProjectHref?.(activeProject)) || '/projects')
const images = activeProject ? (imagesFor?.(activeProject) ?? []) : [] const images = activeProject ? (imagesFor?.(activeProject) ?? []) : []
+4 -1
View File
@@ -12,6 +12,8 @@ type UseMapLibreOptions = {
scrollZoom?: boolean scrollZoom?: boolean
/** محدودهٔ مجاز جابه‌جایی نقشه — جلوگیری از رفتن به بیرون محدودهٔ پروژه‌ها */ /** محدودهٔ مجاز جابه‌جایی نقشه — جلوگیری از رفتن به بیرون محدودهٔ پروژه‌ها */
maxBounds?: maplibregl.LngLatBoundsLike maxBounds?: maplibregl.LngLatBoundsLike
/** کف بزرگ‌نمایی — زوم‌اوت بی‌معنا روی نقشهٔ خالی را می‌بندد */
minZoom?: number
} }
/** سقف انتظار سبک نقشه — سرور کاشی بی‌پاسخ یعنی خطای قابل تلاش دوباره، نه اسپینر بی‌پایان */ /** سقف انتظار سبک نقشه — سرور کاشی بی‌پاسخ یعنی خطای قابل تلاش دوباره، نه اسپینر بی‌پایان */
@@ -36,7 +38,7 @@ const ABSOLUTE_MAX_ZOOM = 22
/** سقف جایگزین وقتی maxzoom منبع در دسترس نیست — برابر آرشیو فعلی (z14 + ۳) */ /** سقف جایگزین وقتی maxzoom منبع در دسترس نیست — برابر آرشیو فعلی (z14 + ۳) */
const FALLBACK_MAX_ZOOM = 17 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<HTMLDivElement | null>(null) const containerRef = useRef<HTMLDivElement | null>(null)
const [map, setMap] = useState<maplibregl.Map | null>(null) const [map, setMap] = useState<maplibregl.Map | null>(null)
const [status, setStatus] = useState<MapStatus>('loading') const [status, setStatus] = useState<MapStatus>('loading')
@@ -68,6 +70,7 @@ export function useMapLibre({ center, zoom, scrollZoom = true, maxBounds }: UseM
center, center,
zoom, zoom,
maxBounds, maxBounds,
minZoom,
// سقف اولیه تا رسیدن maxzoom واقعی منبع؛ سپس applyZoomLimit دقیق می‌کند // سقف اولیه تا رسیدن maxzoom واقعی منبع؛ سپس applyZoomLimit دقیق می‌کند
maxZoom: FALLBACK_MAX_ZOOM, maxZoom: FALLBACK_MAX_ZOOM,
renderWorldCopies: false, renderWorldCopies: false,
+12
View File
@@ -81,6 +81,17 @@ export function Projects() {
return dbMatch ? `/projects/${dbMatch.slug}` : undefined; 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 ( return (
<> <>
<PageHero pageKey="projects" /> <PageHero pageKey="projects" />
@@ -92,6 +103,7 @@ export function Projects() {
<LazyProjectsMapAll <LazyProjectsMapAll
projects={MAP_PROJECTS} projects={MAP_PROJECTS}
resolveProjectHref={resolveProjectHref} resolveProjectHref={resolveProjectHref}
imagesFor={imagesFor}
className="h-[350px] md:h-[450px] lg:h-[55vh] max-h-[600px] w-full" className="h-[350px] md:h-[450px] lg:h-[55vh] max-h-[600px] w-full"
/> />
</div> </div>