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 { 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) ?? []) : []
+4 -1
View File
@@ -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<HTMLDivElement | null>(null)
const [map, setMap] = useState<maplibregl.Map | null>(null)
const [status, setStatus] = useState<MapStatus>('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,
+12
View File
@@ -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 (
<>
<PageHero pageKey="projects" />
@@ -92,6 +103,7 @@ export function Projects() {
<LazyProjectsMapAll
projects={MAP_PROJECTS}
resolveProjectHref={resolveProjectHref}
imagesFor={imagesFor}
className="h-[350px] md:h-[450px] lg:h-[55vh] max-h-[600px] w-full"
/>
</div>