UNPKG

strapi-plugin-map-box

Version:
750 lines (737 loc) 24 kB
import { useRef, useEffect, useState, useCallback } from "react"; import { PinMap } from "@strapi/icons"; import { jsx, jsxs } from "react/jsx-runtime"; import { Field, JSONInput } from "@strapi/design-system"; import Map, { FullscreenControl, NavigationControl, GeolocateControl, Marker } from "react-map-gl/mapbox"; import "mapbox-gl/dist/mapbox-gl.css"; import { useFetchClient } from "@strapi/strapi/admin"; import styled from "styled-components"; const __variableDynamicImportRuntimeHelper = (glob, path, segs) => { const v = glob[path]; if (v) { return typeof v === "function" ? v() : Promise.resolve(v); } return new Promise((_, reject) => { (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)( reject.bind( null, new Error( "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "") ) ) ); }); }; const PLUGIN_ID = "map-box"; const Initializer = ({ setPlugin }) => { const ref = useRef(setPlugin); useEffect(() => { ref.current(PLUGIN_ID); }, []); return null; }; const ControlsContainer = styled.div` position: absolute; top: 1rem; left: 1rem; right: 1rem; z-index: 10; `; const SearchRow = styled.div` display: flex; align-items: flex-start; gap: 8px; `; const SearchWrapper = styled.div` position: relative; flex: 1; max-width: 350px; `; const SearchInputContainer = styled.div` display: flex; align-items: center; background: white; border: 1px solid #dcdce4; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); &:focus-within { border-color: #4945ff; box-shadow: 0 0 0 2px rgba(73, 69, 255, 0.2); } `; const SearchIcon = styled.div` padding: 0 12px; color: #8e8e93; display: flex; align-items: center; `; const SearchInput = styled.input` flex: 1; padding: 12px 0; border: none; font-size: 14px; outline: none; background: transparent; &::placeholder { color: #8e8e93; } `; const ClearButton = styled.button` padding: 8px 12px; background: none; border: none; cursor: pointer; color: #8e8e93; display: flex; align-items: center; &:hover { color: #666; } `; const LoadingSpinner = styled.div` padding: 8px 12px; color: #4945ff; @keyframes spin { to { transform: rotate(360deg); } } svg { animation: spin 1s linear infinite; } `; const RefreshButton = styled.button` width: 44px; height: 44px; border-radius: 8px; background-color: ${(props) => props.$isRefreshing ? "#6c63ff" : "#4945ff"}; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); transition: all 0.15s ease; flex-shrink: 0; &:hover:not(:disabled) { background-color: #3832e0; } &:active:not(:disabled) { transform: scale(0.95); } &:disabled { cursor: not-allowed; } @keyframes spin { to { transform: rotate(360deg); } } svg.spinning { animation: spin 1s linear infinite; } `; const ResultsDropdown = styled.div` position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: white; border: 1px solid #dcdce4; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); max-height: 300px; overflow-y: auto; `; const ResultItem = styled.button` width: 100%; padding: 12px; display: flex; align-items: flex-start; gap: 12px; background: none; border: none; border-bottom: 1px solid #f0f0f0; cursor: pointer; text-align: left; transition: background-color 0.15s; &:last-child { border-bottom: none; } &:hover { background-color: #f6f6f9; } `; const ResultIcon = styled.div` width: 32px; height: 32px; border-radius: 6px; background-color: rgba(73, 69, 255, 0.1); display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #4945ff; `; const ResultTextContainer = styled.div` flex: 1; min-width: 0; `; const ResultTitle = styled.div` font-size: 14px; font-weight: 500; color: #32324d; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; const ResultSubtitle = styled.div` font-size: 12px; color: #8e8e93; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; `; const NoResults = styled.div` padding: 16px; text-align: center; color: #8e8e93; font-size: 14px; `; const getPlaceIcon = (placeType) => { if (placeType.includes("poi")) { return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" }) }); } if (placeType.includes("address")) { return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" }) }); } if (placeType.includes("place") || placeType.includes("locality")) { return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z" }) }); } if (placeType.includes("region") || placeType.includes("country")) { return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" }) }); } return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" }) }); }; const MapSearch = ({ searchQuery, setSearchQuery, searchResults, isSearching, onSelectResult, onClear, showResults, setShowResults, onRefresh, isRefreshing = false }) => { const handleInputChange = (e) => { setSearchQuery(e.target.value); setShowResults(true); }; const handleResultClick = (result) => { onSelectResult(result); setShowResults(false); }; const handleClear = () => { onClear(); setShowResults(false); }; return /* @__PURE__ */ jsx(ControlsContainer, { children: /* @__PURE__ */ jsxs(SearchRow, { children: [ /* @__PURE__ */ jsxs(SearchWrapper, { children: [ /* @__PURE__ */ jsxs(SearchInputContainer, { children: [ /* @__PURE__ */ jsx(SearchIcon, { children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" }) }) }), /* @__PURE__ */ jsx( SearchInput, { type: "text", value: searchQuery, onChange: handleInputChange, onFocus: () => setShowResults(true), placeholder: "Search for a location..." } ), isSearching && /* @__PURE__ */ jsx(LoadingSpinner, { children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z" }) }) }), searchQuery && !isSearching && /* @__PURE__ */ jsx(ClearButton, { onClick: handleClear, type: "button", children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) }) }) ] }), showResults && searchQuery.length > 2 && /* @__PURE__ */ jsx(ResultsDropdown, { children: searchResults.length > 0 ? searchResults.map((result) => { const [title, ...rest] = result.place_name.split(","); const subtitle = rest.join(",").trim(); return /* @__PURE__ */ jsxs( ResultItem, { onClick: () => handleResultClick(result), type: "button", children: [ /* @__PURE__ */ jsx(ResultIcon, { children: getPlaceIcon(result.place_type) }), /* @__PURE__ */ jsxs(ResultTextContainer, { children: [ /* @__PURE__ */ jsx(ResultTitle, { children: title }), subtitle && /* @__PURE__ */ jsx(ResultSubtitle, { children: subtitle }) ] }) ] }, result.id ); }) : !isSearching ? /* @__PURE__ */ jsx(NoResults, { children: "No results found" }) : null }) ] }), onRefresh && /* @__PURE__ */ jsx( RefreshButton, { onClick: onRefresh, disabled: isRefreshing, $isRefreshing: isRefreshing, type: "button", title: "Refresh to original location", children: isRefreshing ? /* @__PURE__ */ jsx( "svg", { className: "spinning", width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z" }) } ) : /* @__PURE__ */ jsxs( "svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [ /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" }), /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" }), /* @__PURE__ */ jsx("path", { d: "M16 21h5v-5" }) ] } ) } ) ] }) }); }; const DEFAULT_VIEW_STATE = { longitude: -122.4194, latitude: 37.7749, zoom: 13, pitch: 0, bearing: 0, padding: { top: 0, bottom: 0, left: 0, right: 0 } }; const useMapBoxSettings = () => { const { get } = useFetchClient(); const [config, setConfig] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchSettings = async () => { try { setIsLoading(true); const { data } = await get("/map-box/get-settings"); console.log("data from getSettings", data); setConfig(data); setError(null); } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch MapBox settings"); } finally { setIsLoading(false); } }; fetchSettings(); }, []); return { config, isLoading, error }; }; const useMapLocationHook = (initialValue) => { const [viewState, setViewState] = useState(DEFAULT_VIEW_STATE); const [markerPosition, setMarkerPosition] = useState({ longitude: DEFAULT_VIEW_STATE.longitude, latitude: DEFAULT_VIEW_STATE.latitude }); useEffect(() => { if (initialValue) { console.log("Initializing from previous value:", initialValue); const previousValue = initialValue; setViewState((prev) => ({ ...prev, longitude: previousValue.longitude, latitude: previousValue.latitude, zoom: previousValue.zoom, pitch: previousValue.pitch, bearing: previousValue.bearing })); setMarkerPosition({ longitude: previousValue.longitude, latitude: previousValue.latitude }); } }, []); return { viewState, setViewState, markerPosition, setMarkerPosition }; }; function DebugInfo({ searchResults, searchError, viewState, markerPosition, searchQuery, value }) { return /* @__PURE__ */ jsxs(DebugContainer, { children: [ /* @__PURE__ */ jsx("h4", { children: "Debug Information:" }), /* @__PURE__ */ jsxs(DebugSection, { children: [ /* @__PURE__ */ jsx("strong", { children: "Search Results:" }), /* @__PURE__ */ jsx(DebugPre, { children: searchResults ? JSON.stringify(searchResults, null, 2) : "No search results yet" }) ] }), searchError && /* @__PURE__ */ jsxs(ErrorMessage, { children: [ /* @__PURE__ */ jsx("strong", { children: "Error:" }), " ", searchError ] }), /* @__PURE__ */ jsxs(DebugSection, { children: [ /* @__PURE__ */ jsx("strong", { children: "Current View State:" }), /* @__PURE__ */ jsx(DebugPre, { children: JSON.stringify(viewState, null, 2) }) ] }), /* @__PURE__ */ jsxs(DebugSection, { children: [ /* @__PURE__ */ jsx("strong", { children: "Marker Position:" }), /* @__PURE__ */ jsx(DebugPre, { children: JSON.stringify(markerPosition, null, 2) }) ] }), /* @__PURE__ */ jsxs(DebugSection, { children: [ /* @__PURE__ */ jsx("strong", { children: "Search Query:" }), /* @__PURE__ */ jsx(DebugPre, { children: searchQuery || "No search query" }) ] }), /* @__PURE__ */ jsxs(DebugSection, { children: [ /* @__PURE__ */ jsx("strong", { children: "Current Value:" }), /* @__PURE__ */ jsx(DebugPre, { children: value ? JSON.stringify(value, null, 2) : "No value set" }) ] }) ] }); } const DebugContainer = styled.div` margin-top: 20px; padding: 1rem; background-color: #f5f5f5; border-radius: 4px; `; const DebugSection = styled.div` margin-bottom: 10px; `; const DebugPre = styled.pre` background: #ffffff; padding: 10px; border-radius: 4px; max-height: 200px; overflow: auto; margin: 5px 0; border: 1px solid #dcdce4; `; const ErrorMessage = styled.div` color: #d02b20; margin-bottom: 10px; padding: 10px; background-color: #fff5f5; border: 1px solid #ffd7d5; border-radius: 4px; `; function MapBoxField({ name, onChange, value, intlLabel, required }) { const { get } = useFetchClient(); const { config, isLoading, error } = useMapBoxSettings(); const { viewState, markerPosition, setViewState, setMarkerPosition } = useMapLocationHook(value); const { accessToken, debugMode } = config || {}; const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [showResults, setShowResults] = useState(false); const [searchError, setSearchError] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); const searchTimeoutRef = useRef(null); const initialValueRef = useRef(null); useEffect(() => { if (value && initialValueRef.current === null) { initialValueRef.current = { ...value }; } }, [value]); const updateMarkerPosition = useCallback( (lng, lat, address) => { setMarkerPosition({ longitude: lng, latitude: lat }); const newValue = { longitude: lng, latitude: lat, address: address || "Selected location", zoom: viewState.zoom, pitch: viewState.pitch, bearing: viewState.bearing }; onChange({ target: { name, value: newValue, type: "json" } }); }, [name, onChange, setMarkerPosition, viewState.zoom, viewState.pitch, viewState.bearing] ); useEffect(() => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } if (searchQuery.trim().length <= 2) { setSearchResults([]); setIsSearching(false); return; } setIsSearching(true); searchTimeoutRef.current = setTimeout(async () => { try { setSearchError(null); const encodedQuery = encodeURIComponent(searchQuery.trim()); const url = `/map-box/location-search/${encodedQuery}`; const { data } = await get(url); if (data.features) { setSearchResults( data.features.slice(0, 5).map((feature) => ({ id: feature.id, place_name: feature.place_name, center: feature.center, place_type: feature.place_type })) ); } else if (data.error) { setSearchError(data.error); setSearchResults([]); } else { setSearchResults([]); } } catch (error2) { console.error("Error searching location:", error2); setSearchError(error2 instanceof Error ? error2.message : "An error occurred"); setSearchResults([]); } finally { setIsSearching(false); } }, 300); return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, [searchQuery, get]); const handleSelectResult = useCallback( (result) => { const [longitude, latitude] = result.center; setViewState((prev) => ({ ...prev, longitude, latitude, zoom: 14, transitionDuration: 1e3 })); updateMarkerPosition(longitude, latitude, result.place_name); setSearchQuery(result.place_name.split(",")[0]); setSearchResults([]); setShowResults(false); }, [setViewState, updateMarkerPosition] ); const handleClearSearch = useCallback(() => { setSearchQuery(""); setSearchResults([]); setShowResults(false); }, []); const handleRefresh = useCallback(() => { setIsRefreshing(true); const originalValue = initialValueRef.current; if (originalValue) { setViewState((prev) => ({ ...prev, longitude: originalValue.longitude, latitude: originalValue.latitude, zoom: originalValue.zoom || 12, pitch: originalValue.pitch || 0, bearing: originalValue.bearing || 0 })); setMarkerPosition({ longitude: originalValue.longitude, latitude: originalValue.latitude }); onChange({ target: { name, value: originalValue, type: "json" } }); } setSearchQuery(""); setSearchResults([]); setShowResults(false); setTimeout(() => { setIsRefreshing(false); }, 500); }, [name, onChange, setViewState, setMarkerPosition]); const handleMapClick = (event) => { const { lngLat } = event; updateMarkerPosition(lngLat.lng, lngLat.lat); }; const handleMapMove = (evt) => { setViewState(evt.viewState); }; const handleMapMoveEnd = (evt) => { const { longitude, latitude, zoom, pitch, bearing } = evt.viewState; const newValue = { longitude: markerPosition.longitude, latitude: markerPosition.latitude, address: value?.address || "Selected location", zoom, pitch, bearing }; onChange({ target: { name, value: newValue, type: "json" } }); }; const handleMarkerDragEnd = (event) => { const { lngLat } = event; updateMarkerPosition(lngLat.lng, lngLat.lat); }; const handlePositionChange = (input) => { try { const value2 = JSON.parse(input); setViewState((prev) => ({ ...prev, longitude: value2.longitude, latitude: value2.latitude, zoom: value2.zoom || prev.zoom, pitch: value2.pitch || prev.pitch, bearing: value2.bearing || prev.bearing })); setMarkerPosition({ longitude: value2.longitude, latitude: value2.latitude }); onChange({ target: { name, value: value2, type: "json" } }); } catch { } }; const finalValue = { longitude: markerPosition.longitude, latitude: markerPosition.latitude, zoom: viewState.zoom, pitch: viewState.pitch, bearing: viewState.bearing, address: value?.address || "Selected location" }; const strValue = JSON.stringify(value || finalValue, null, 2); if (!accessToken || isLoading) { return /* @__PURE__ */ jsx("div", { children: "Loading..." }); } if (error) { return /* @__PURE__ */ jsxs("div", { children: [ "Error: ", error ] }); } return /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsxs("div", { style: { position: "relative", height: "500px", width: "100%" }, children: [ /* @__PURE__ */ jsx( MapSearch, { searchQuery, setSearchQuery, searchResults, isSearching, onSelectResult: handleSelectResult, onClear: handleClearSearch, showResults, setShowResults, onRefresh: handleRefresh, isRefreshing } ), /* @__PURE__ */ jsxs( Map, { ...viewState, onMove: handleMapMove, onMoveEnd: handleMapMoveEnd, onClick: handleMapClick, mapStyle: "mapbox://styles/mapbox/streets-v12", mapboxAccessToken: accessToken, attributionControl: false, style: { height: "100%", width: "100%" }, children: [ /* @__PURE__ */ jsx(FullscreenControl, {}), /* @__PURE__ */ jsx(NavigationControl, {}), /* @__PURE__ */ jsx(GeolocateControl, {}), /* @__PURE__ */ jsx( Marker, { longitude: markerPosition.longitude, latitude: markerPosition.latitude, color: "#4945ff", draggable: true, onDragEnd: handleMarkerDragEnd } ) ] } ) ] }), debugMode && /* @__PURE__ */ jsxs(Field.Root, { name, required, children: [ /* @__PURE__ */ jsx(Field.Label, { children: intlLabel?.defaultMessage ?? "Location" }), /* @__PURE__ */ jsx(JSONInput, { value: strValue, onChange: handlePositionChange }), /* @__PURE__ */ jsx(Field.Error, {}), /* @__PURE__ */ jsx(Field.Hint, {}) ] }), debugMode && /* @__PURE__ */ jsx( DebugInfo, { viewState, searchResults, searchError, markerPosition, searchQuery, value } ) ] }); } const index = { register(app) { app.customFields.register({ name: "map-box", type: "json", icon: PinMap, intlLabel: { id: "custom.fields.map-box.label", defaultMessage: "Map Box" }, intlDescription: { id: "custom.fields.map-box.description", defaultMessage: "Enter geographic coordinates" }, components: { Input: () => ({ default: MapBoxField }) } }); app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID }); }, async registerTrads({ locales }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-Byx4XI2L.mjs") }), `./translations/${locale}.json`, 3); return { data, locale }; } catch { return { data: {}, locale }; } }) ); } }; export { index as default }; //# sourceMappingURL=index.mjs.map