feat(address-map): MapLibre GL + Map.ir vector map & Persian reverse geocoding

Replace the broken-looking Leaflet/OSM address picker with a MapLibre GL
map using Map.ir vector tiles, plus Map.ir reverse geocoding for fast
coordinates -> Persian address.

- InteractiveMap: drop react-leaflet/leaflet; init a MapLibre map (Map.ir
  style via transformRequest x-api-key), a NavigationControl, and a fixed
  center pin — the map moves under the pin and the centre is reverse-
  geocoded on moveend (debounced). Wrapped dir=ltr so the map renders
  correctly inside the RTL page (fixes the scattered-tiles bug). Selected
  address shown in an overlay chip; city select / geolocation fly the map.
- reverse geocoding: Nominatim -> Map.ir (https://map.ir/reverse), which
  returns proper Persian addresses (Nominatim was weak for Iran and its
  TOS forbids production use).
- default map centre fixed to Tehran (matched the "تهران" selector).
- key via VITE_MAPIR_KEY (documented in .env.example; real key in env).
- deps: add maplibre-gl, remove leaflet/react-leaflet/@types/leaflet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arda Samadi 2026-06-28 01:20:37 +03:30
parent 668649d540
commit 3c7ee5b24c
4 changed files with 362 additions and 213 deletions

View File

@ -21,3 +21,6 @@ VITE_BUILD_TARGET=development
# Support Configuration # Support Configuration
VITE_SUPPORT_PHONE=+989223740993 VITE_SUPPORT_PHONE=+989223740993
# Map.ir key for the address map (MapLibre vector tiles + reverse geocoding)
VITE_MAPIR_KEY=your_mapir_api_key

View File

@ -1,15 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { import "maplibre-gl/dist/maplibre-gl.css";
MapContainer, import type { Map as MaplibreMap } from "maplibre-gl";
TileLayer, import { Search, X, ChevronLeft, MapPin } from "lucide-react";
Marker,
Popup,
useMapEvents,
useMap,
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import { Search, X, ChevronLeft } from "lucide-react";
import AppInput from "./AppInput"; import AppInput from "./AppInput";
import AppTextArea from "./AppTextArea"; import AppTextArea from "./AppTextArea";
import { import {
@ -32,6 +24,25 @@ import {
type IranianCity, type IranianCity,
} from "../data/iranian-cities"; } from "../data/iranian-cities";
// Map.ir config (vector tiles + reverse geocoding). The key is a client-side
// map key (sent with every tile request), provided via env.
const MAPIR_KEY = import.meta.env.VITE_MAPIR_KEY || "";
const MAPIR_STYLE = "https://map.ir/vector/styles/main/mapir-style.json";
// Reverse geocode (lat/lng -> Persian address) via Map.ir.
async function reverseGeocode(lat: number, lon: number): Promise<string> {
try {
const res = await fetch(`https://map.ir/reverse?lat=${lat}&lon=${lon}`, {
headers: { "x-api-key": MAPIR_KEY },
});
const data = await res.json();
return data?.address || data?.address_compact || "آدرس نامشخص";
} catch (error) {
console.error("Error fetching address:", error);
return "";
}
}
// Form data interface // Form data interface
interface FormData { interface FormData {
title: string; title: string;
@ -71,30 +82,6 @@ function convertPersianToEnglish(str: string): string {
return result; return result;
} }
// Component to change map view when city is selected
function CityChangeView({ center }: { center: [number, number] }) {
const map = useMap();
useEffect(() => {
map.setView(center, 13);
}, [center, map]);
return null;
}
// Component to handle map clicks
function MapClickHandler({
onMapClick,
}: {
onMapClick: (latlng: [number, number]) => void;
}) {
useMapEvents({
click: (e) => {
const { lat, lng } = e.latlng;
onMapClick([lat, lng]);
},
});
return null;
}
export default function InteractiveMap({ export default function InteractiveMap({
onLocationSelected, onLocationSelected,
initialAddress, initialAddress,
@ -112,7 +99,7 @@ export default function InteractiveMap({
isEditing ? "" : "تهران" isEditing ? "" : "تهران"
); );
const [currentCenter, setCurrentCenter] = useState<[number, number]>( const [currentCenter, setCurrentCenter] = useState<[number, number]>(
iranianCities[0].position (iranianCities.find((c) => c.name === "تهران") || iranianCities[0]).position
); );
const [mounted, setMounted] = useState<boolean>(false); const [mounted, setMounted] = useState<boolean>(false);
@ -166,34 +153,10 @@ export default function InteractiveMap({
isDefault: formData.is_default, isDefault: formData.is_default,
}); });
// Fix Leaflet's default icon path issues when the component mounts // MapLibre refs
useEffect(() => { const mapContainerRef = useRef<HTMLDivElement>(null);
// Fix Leaflet icon issues const mapRef = useRef<MaplibreMap | null>(null);
delete (L.Icon.Default.prototype as { _getIconUrl?: string })._getIconUrl; const moveDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
L.Icon.Default.mergeOptions({
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
iconRetinaUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
shadowUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
});
// Cleanup function to remove map instance when component unmounts
return () => {
// This code ensures previous maps are cleaned up
// eslint-disable-next-line react-hooks/exhaustive-deps
const mapContainer = document.getElementById(mapIdRef.current);
if (mapContainer) {
// Clean up previous map on this element
// In Leaflet version 1+, using map._leaflet no longer works
// and instead removeAttribute should be used
mapContainer.innerHTML = "";
mapContainer.removeAttribute("data-leaflet-internal-id");
mapContainer.removeAttribute("_leaflet_id");
console.log("Map container cleaned up successfully");
}
};
}, []);
// Initialize with the existing address data when in editing mode // Initialize with the existing address data when in editing mode
useEffect(() => { useEffect(() => {
@ -245,76 +208,94 @@ export default function InteractiveMap({
setFilteredCities(cities); setFilteredCities(cities);
}, [citySearchTerm, selectedProvince]); }, [citySearchTerm, selectedProvince]);
// Function to get address from coordinates
const getAddressFromCoordinates = async (latlng: [number, number]) => {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`,
{
headers: {
"Accept-Language": "fa",
},
}
);
const data = await response.json();
return data.display_name || "آدرس نامشخص";
} catch (error) {
console.error("Error fetching address:", error);
return "";
}
};
// Extract postal code from address data
const getPostalCodeFromAddress = async (latlng: [number, number]) => {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng[0]}&lon=${latlng[1]}&zoom=18&addressdetails=1&accept-language=fa&countrycodes=ir`,
{
headers: {
"Accept-Language": "fa",
},
}
);
const data = await response.json();
if (data && data.address && data.address.postcode) {
return data.address.postcode;
}
return "";
} catch (error) {
console.error("Error fetching postal code:", error);
return "";
}
};
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const handleMapClick = async (latlng: [number, number]) => { // Reverse-geocode a point and push it into the form. Used by the map's
const address = await getAddressFromCoordinates(latlng); // center pin (on move) — replaces the old click handler.
const postalCode = await getPostalCodeFromAddress(latlng); const applyLocation = useCallback(
async (latlng: [number, number]) => {
const address = await reverseGeocode(latlng[0], latlng[1]);
setSelectedPosition(latlng);
setSelectedAddress(address);
setFormData((prev) => ({
...prev,
main_address: address,
latitude: latlng[0],
longitude: latlng[1],
}));
if (onLocationSelected) {
onLocationSelected({ position: latlng, address });
}
},
[onLocationSelected]
);
setSelectedPosition(latlng); // Keep a stable ref so the map's event listeners always call the latest.
setSelectedAddress(address); const applyLocationRef = useRef(applyLocation);
useEffect(() => {
applyLocationRef.current = applyLocation;
}, [applyLocation]);
// Don't update postal_code in edit mode, preserve the original value // Initialize the MapLibre map (client-only, once) with the Map.ir style.
setFormData({ useEffect(() => {
...formData, if (!mounted || formOnly || !mapContainerRef.current || mapRef.current) {
main_address: address, return;
// Don't set postal_code here in either mode, preserve the existing value in edit mode }
latitude: latlng[0], let cancelled = false;
longitude: latlng[1], (async () => {
}); const maplibregl = (await import("maplibre-gl")).default;
if (cancelled || !mapContainerRef.current) return;
const start = selectedPosition || currentCenter; // [lat, lng]
const map = new maplibregl.Map({
container: mapContainerRef.current,
style: MAPIR_STYLE,
center: [start[1], start[0]], // MapLibre uses [lng, lat]
zoom: 14,
attributionControl: false,
transformRequest: (url: string) =>
url.startsWith("https://map.ir")
? { url, headers: { "x-api-key": MAPIR_KEY } }
: { url },
});
mapRef.current = map;
map.addControl(
new maplibregl.NavigationControl({ showCompass: false }),
"top-left"
);
// Reverse-geocode the centre whenever the map settles.
map.on("moveend", () => {
if (moveDebounceRef.current) clearTimeout(moveDebounceRef.current);
moveDebounceRef.current = setTimeout(() => {
const c = map.getCenter();
applyLocationRef.current([c.lat, c.lng]);
}, 350);
});
map.on("load", () => {
const c = map.getCenter();
applyLocationRef.current([c.lat, c.lng]);
});
})();
return () => {
cancelled = true;
if (moveDebounceRef.current) clearTimeout(moveDebounceRef.current);
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mounted, formOnly]);
// If using as a location picker in another component, call the callback // Recenter the map when the chosen city changes.
if (onLocationSelected) { useEffect(() => {
onLocationSelected({ if (mapRef.current) {
position: latlng, mapRef.current.flyTo({
address, center: [currentCenter[1], currentCenter[0]],
postalCode: isEditing ? formData.postal_code : postalCode, zoom: 14,
}); });
} }
}; }, [currentCenter]);
const handleCitySelect = (cityName: string) => { const handleCitySelect = (cityName: string) => {
setSelectedCity(cityName); setSelectedCity(cityName);
@ -499,7 +480,11 @@ export default function InteractiveMap({
const handleLocationFound = (event: Event) => { const handleLocationFound = (event: Event) => {
const customEvent = event as CustomEvent<{ lat: number; lng: number }>; const customEvent = event as CustomEvent<{ lat: number; lng: number }>;
const { lat, lng } = customEvent.detail; const { lat, lng } = customEvent.detail;
handleMapClick([lat, lng]); if (mapRef.current) {
mapRef.current.flyTo({ center: [lng, lat], zoom: 15 });
} else {
applyLocationRef.current([lat, lng]);
}
}; };
window.addEventListener( window.addEventListener(
@ -832,44 +817,30 @@ export default function InteractiveMap({
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
{/* Map Container */} {/* Map Container MapLibre + Map.ir vector tiles. dir=ltr keeps the
<div className="dd z-0 h-[500px] w-full relative"> map's internal positioning correct inside the RTL page. */}
<div id={mapIdRef.current} className="h-full w-full"> <div className="dd z-0 h-[500px] w-full relative" dir="ltr">
{mounted && ( <div
<MapContainer ref={mapContainerRef}
key={mapIdRef.current} id={mapIdRef.current}
center={currentCenter} className="h-full w-full"
zoom={13} />
scrollWheelZoom={true}
style={{ height: "100%", width: "100%" }}
>
<CityChangeView center={currentCenter} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapClickHandler onMapClick={handleMapClick} /> {/* Fixed center pin — the map moves under it; its tip marks the point */}
{/* <LocationButton /> */} <div className="pointer-events-none absolute left-1/2 top-1/2 z-[5] -translate-x-1/2 -translate-y-full drop-shadow-md">
<MapPin size={40} className="fill-RED text-RED" strokeWidth={1.5} />
{selectedPosition && (
<Marker position={selectedPosition}>
<Popup>
<div dir="rtl" className="text-right">
<p className="font-bold mb-1">آدرس:</p>
<p className="mb-2">{selectedAddress}</p>
<p className="mb-1">
<span className="font-bold">مختصات: </span>
{selectedPosition[0].toFixed(6)},{" "}
{selectedPosition[1].toFixed(6)}
</p>
</div>
</Popup>
</Marker>
)}
</MapContainer>
)}
</div> </div>
{/* Selected-address preview */}
{selectedAddress && (
<div
dir="rtl"
className="absolute left-3 right-3 bottom-3 z-[5] bg-WHITE/95 backdrop-blur rounded-xl shadow-md px-3.5 py-2.5"
>
<p className="text-R10 text-GRAY mb-0.5">موقعیت انتخابشده</p>
<p className="text-R12 font-bold line-clamp-2">{selectedAddress}</p>
</div>
)}
</div> </div>
{/* Select Location Button */} {/* Select Location Button */}

269
package-lock.json generated
View File

@ -35,13 +35,12 @@
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"isbot": "^4.1.0", "isbot": "^4.1.0",
"jalali-moment": "^3.3.11", "jalali-moment": "^3.3.11",
"leaflet": "^1.9.4",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"maplibre-gl": "^5.24.0",
"moment-jalaali": "^0.10.4", "moment-jalaali": "^0.10.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^15.4.1", "react-i18next": "^15.4.1",
"react-leaflet": "^4.2.1",
"remix-auth": "^3.6.0", "remix-auth": "^3.6.0",
"remix-auth-form": "^1.4.0", "remix-auth-form": "^1.4.0",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
@ -52,7 +51,6 @@
}, },
"devDependencies": { "devDependencies": {
"@remix-run/dev": "^2.16.0", "@remix-run/dev": "^2.16.0",
"@types/leaflet": "^1.9.17",
"@types/react": "^18.2.20", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",
@ -2002,6 +2000,119 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
"integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==",
"license": "MIT",
"engines": {
"node": ">= 22"
}
},
"node_modules/@mapbox/point-geometry": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
"license": "ISC"
},
"node_modules/@mapbox/tiny-sdf": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/unitbezier": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/vector-tile": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz",
"integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/point-geometry": "~1.1.0",
"@types/geojson": "^7946.0.16",
"pbf": "^4.0.2"
}
},
"node_modules/@mapbox/whoots-js": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@maplibre/geojson-vt": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz",
"integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==",
"license": "ISC",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "24.10.0",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz",
"integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/unitbezier": "^1.0.0",
"json-stringify-pretty-compact": "^4.0.0",
"minimist": "^1.2.8",
"quickselect": "^3.0.0",
"tinyqueue": "^3.0.0"
},
"bin": {
"gl-style-format": "dist/gl-style-format.mjs",
"gl-style-migrate": "dist/gl-style-migrate.mjs",
"gl-style-validate": "dist/gl-style-validate.mjs"
}
},
"node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
"integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
"license": "BSD-2-Clause"
},
"node_modules/@maplibre/mlt": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz",
"integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@mapbox/point-geometry": "^1.1.0"
}
},
"node_modules/@maplibre/vt-pbf": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz",
"integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==",
"license": "MIT",
"dependencies": {
"@mapbox/point-geometry": "^1.1.0",
"@types/geojson": "^7946.0.16",
"pbf": "^5.1.0"
}
},
"node_modules/@maplibre/vt-pbf/node_modules/pbf": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz",
"integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
}
},
"node_modules/@mdx-js/mdx": { "node_modules/@mdx-js/mdx": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz",
@ -3211,17 +3322,6 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@remix-run/dev": { "node_modules/@remix-run/dev": {
"version": "2.17.2", "version": "2.17.2",
"resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.17.2.tgz", "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.17.2.tgz",
@ -3948,7 +4048,6 @@
"version": "7946.0.16", "version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/hast": { "node_modules/@types/hast": {
@ -3975,16 +4074,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
"version": "3.0.15", "version": "3.0.15",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
@ -6859,6 +6948,12 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/earcut": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
"license": "ISC"
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -8555,6 +8650,12 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/gl-matrix": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
"license": "MIT"
},
"node_modules/glob": { "node_modules/glob": {
"version": "11.0.3", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
@ -9919,6 +10020,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
"license": "MIT"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -9961,6 +10068,12 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/kdbush": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
"license": "ISC"
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -10001,12 +10114,6 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -10168,6 +10275,40 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/maplibre-gl": {
"version": "5.24.0",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
"integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/point-geometry": "^1.1.0",
"@mapbox/tiny-sdf": "^2.1.0",
"@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^2.0.4",
"@mapbox/whoots-js": "^3.1.0",
"@maplibre/geojson-vt": "^6.1.0",
"@maplibre/maplibre-gl-style-spec": "^24.8.1",
"@maplibre/mlt": "^1.1.8",
"@maplibre/vt-pbf": "^4.3.0",
"@types/geojson": "^7946.0.16",
"earcut": "^3.0.2",
"gl-matrix": "^3.4.4",
"kdbush": "^4.0.2",
"murmurhash-js": "^1.0.0",
"pbf": "^4.0.1",
"potpack": "^2.1.0",
"quickselect": "^3.0.0",
"tinyqueue": "^3.0.0"
},
"engines": {
"node": ">=16.14.0",
"npm": ">=8.1.0"
},
"funding": {
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
}
},
"node_modules/markdown-extensions": { "node_modules/markdown-extensions": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz",
@ -11171,7 +11312,6 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -11500,6 +11640,12 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/murmurhash-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
"license": "MIT"
},
"node_modules/mute-stream": { "node_modules/mute-stream": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@ -12159,6 +12305,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pbf": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz",
"integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
}
},
"node_modules/peek-stream": { "node_modules/peek-stream": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
@ -12519,6 +12677,12 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/potpack": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
"license": "ISC"
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -12622,6 +12786,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/protocol-buffers-schema": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
"license": "MIT"
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -12720,6 +12890,12 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/quickselect": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
"license": "ISC"
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -12801,20 +12977,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-leaflet": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^2.1.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.2", "version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -13168,6 +13330,15 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
} }
}, },
"node_modules/resolve-protobuf-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
"license": "MIT",
"dependencies": {
"protocol-buffers-schema": "^3.3.1"
}
},
"node_modules/resolve.exports": { "node_modules/resolve.exports": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz",
@ -14631,6 +14802,12 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
"license": "ISC"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -44,13 +44,12 @@
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"isbot": "^4.1.0", "isbot": "^4.1.0",
"jalali-moment": "^3.3.11", "jalali-moment": "^3.3.11",
"leaflet": "^1.9.4",
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"maplibre-gl": "^5.24.0",
"moment-jalaali": "^0.10.4", "moment-jalaali": "^0.10.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^15.4.1", "react-i18next": "^15.4.1",
"react-leaflet": "^4.2.1",
"remix-auth": "^3.6.0", "remix-auth": "^3.6.0",
"remix-auth-form": "^1.4.0", "remix-auth-form": "^1.4.0",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
@ -61,7 +60,6 @@
}, },
"devDependencies": { "devDependencies": {
"@remix-run/dev": "^2.16.0", "@remix-run/dev": "^2.16.0",
"@types/leaflet": "^1.9.17",
"@types/react": "^18.2.20", "@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",