Major UX improvements: Smart search, navigation, fixed markers
1. Smart Search Integration (MapView.jsx, SmartSearchBar.jsx):
- Replace simple search bar with SmartSearchBar component
- Suggestions appear on focus (popular cities/regions)
- Suggestions update as user types (debounced 150ms)
- Click suggestion → fly to location with appropriate zoom
- Search triggers even with empty query for popular results
2. Navigation to Cities (MapView.jsx):
- New handlePickSuggestion() function
- Extracts coords from suggestion (supports coords array or lat/lon)
- Uses map.flyTo() with smart zoom levels per type:
* Cities: zoom 11
* Regions: zoom 7
* Posts: zoom 16
* Profiles: zoom 14
- Clears search after navigation
3. Fixed Marker Anchoring (markerManager.js):
- Changed anchor: "center" → "bottom"
- Markers now stay fixed at geographic position
- Pin design: circle image + triangular point (52px height)
- Point positioned at exact lat/lon location
- No more drifting when map moves!
4. Improved Pin Design:
- Circle (32px) with category color background
- White border + shadow for visibility
- Triangular point (16px) below circle
- Image inside circle if available
- Hover card positioned above pin (bottom: 58px)
- Smooth transitions on hover/click
Mobile-friendly: Works on cellphone without console, visual feedback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
069072dc14
commit
28e760d616
|
|
@ -17,6 +17,7 @@ import { usePostsEngine } from "./usePostsEngine";
|
|||
import { useAuth } from "../../auth/AuthContext";
|
||||
import FilterButtons from "../Filters/FilterButtons";
|
||||
import TimeFilterButtons from "../Filters/TimeFilterButtons";
|
||||
import SmartSearchBar from "../Search/SmartSearchBar";
|
||||
|
||||
export default function MapView({
|
||||
theme = "dark",
|
||||
|
|
@ -208,6 +209,35 @@ export default function MapView({
|
|||
map.flyTo({ center: userPosition, zoom: 9, speed: 1.2 });
|
||||
};
|
||||
|
||||
const handlePickSuggestion = (item, { zoom }) => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
// Extract coords from item
|
||||
let coords = null;
|
||||
if (Array.isArray(item.coords) && item.coords.length === 2) {
|
||||
coords = item.coords; // [lon, lat]
|
||||
} else if (typeof item.lon === 'number' && typeof item.lat === 'number') {
|
||||
coords = [item.lon, item.lat];
|
||||
}
|
||||
|
||||
if (!coords) {
|
||||
console.warn('[MapView] No coords for suggestion:', item);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[MapView] Flying to:', item.title, coords, 'zoom:', zoom);
|
||||
map.flyTo({
|
||||
center: coords,
|
||||
zoom: zoom || 12,
|
||||
speed: 1.4,
|
||||
essential: true
|
||||
});
|
||||
|
||||
// Clear search query after navigation
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
if (!authenticated) {
|
||||
alert("You must be logged in to place a wire.");
|
||||
|
|
@ -413,38 +443,15 @@ export default function MapView({
|
|||
<div ref={containerRef} className="map-container" />
|
||||
{status && <div className="map-status">{status}</div>}
|
||||
|
||||
{/* TOP: Search à gauche, 2 icônes à droite, sur une seule ligne */}
|
||||
{/* TOP: Search avec suggestions intelligentes */}
|
||||
<div className="map-overlay map-overlay-top">
|
||||
<div className="sw-top-row">
|
||||
<form
|
||||
className="sw-searchbar sw-top-search"
|
||||
onSubmit={handleSearchSubmit}
|
||||
role="search"
|
||||
aria-label="Search"
|
||||
>
|
||||
<i className="fa-solid fa-magnifying-glass sw-search-icon" />
|
||||
<input
|
||||
className="sw-search-input"
|
||||
type="search"
|
||||
placeholder="Look at… (city, topic, @user)"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
{searchQuery ? (
|
||||
<button
|
||||
type="button"
|
||||
className="sw-search-clear"
|
||||
onClick={handleClearSearch}
|
||||
aria-label="Clear search"
|
||||
title="Clear"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
) : null}
|
||||
</form>
|
||||
<SmartSearchBar
|
||||
placeholder="Search cities, places, posts..."
|
||||
onPick={handlePickSuggestion}
|
||||
onMySpot={handleFlyToMe}
|
||||
onPlaceTourWire={handleOpenCreate}
|
||||
/>
|
||||
|
||||
<div className="map-top-actions">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -479,34 +479,54 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
|
|||
root.style.position = "relative";
|
||||
root.style.cursor = "pointer";
|
||||
|
||||
// Marqueur circulaire simple
|
||||
// Marqueur pin avec image en haut et pointe en bas
|
||||
root.innerHTML = `
|
||||
<div class="sw-simple-marker" style="
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: ${color};
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
<div class="sw-simple-pin-container" style="
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
">
|
||||
${img ? `<img src="${img}" alt="" style="width:100%;height:100%;object-fit:cover;" />` : `
|
||||
<div style="
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
opacity: 0.9;
|
||||
"></div>
|
||||
`}
|
||||
<div class="sw-simple-marker" style="
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: ${color};
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
z-index: 2;
|
||||
">
|
||||
${img ? `<img src="${img}" alt="" style="width:100%;height:100%;object-fit:cover;" />` : `
|
||||
<div style="
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
opacity: 0.9;
|
||||
"></div>
|
||||
`}
|
||||
</div>
|
||||
<div class="sw-simple-pin-point" style="
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 16px solid ${color};
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
|
||||
margin-top: -2px;
|
||||
z-index: 1;
|
||||
"></div>
|
||||
</div>
|
||||
<div class="sw-simple-hover-card" style="
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
bottom: 58px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.9);
|
||||
opacity: 0;
|
||||
|
|
@ -521,6 +541,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
|
|||
padding: 10px 12px;
|
||||
border: 1px solid rgba(90, 190, 255, 0.7);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
white-space: normal;
|
||||
">
|
||||
<div style="font-weight: 900; font-size: 12px; color: #e8f5ff; margin-bottom: 4px;">
|
||||
${escapeHtml(post?.title || "Untitled")}
|
||||
|
|
@ -576,7 +597,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
|
|||
|
||||
const marker = new maplibregl.Marker({
|
||||
element: root,
|
||||
anchor: "center",
|
||||
anchor: "bottom", // La pointe du pin est à la position exacte
|
||||
})
|
||||
.setLngLat(lngLat)
|
||||
.addTo(map);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export default function SmartSearchBar({
|
|||
const [items, setItems] = useState([]);
|
||||
const [active, setActive] = useState(-1);
|
||||
const [err, setErr] = useState("");
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
|
||||
const abortRef = useRef(null);
|
||||
const boxRef = useRef(null);
|
||||
|
|
@ -80,45 +81,45 @@ export default function SmartSearchBar({
|
|||
return () => document.removeEventListener("mousedown", onDocDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const query = q.trim();
|
||||
setErr("");
|
||||
|
||||
// Fonction pour déclencher une recherche
|
||||
const triggerSearch = async (searchQuery) => {
|
||||
if (abortRef.current) abortRef.current.abort();
|
||||
|
||||
// Allow empty query to get popular suggestions
|
||||
// if (!query) {
|
||||
// setItems([]);
|
||||
// setLoading(false);
|
||||
// setActive(-1);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const ctrl = new AbortController();
|
||||
abortRef.current = ctrl;
|
||||
setLoading(true);
|
||||
setErr("");
|
||||
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
console.log('[SmartSearchBar] Searching for:', query);
|
||||
const res = await recoSearch(query, { signal: ctrl.signal, limit: 12 });
|
||||
console.log('[SmartSearchBar] Got results:', res?.length || 0, res);
|
||||
setItems(res);
|
||||
setOpen(true);
|
||||
setActive(res.length ? 0 : -1);
|
||||
} catch (e) {
|
||||
if (ctrl.signal.aborted) return;
|
||||
console.error('[SmartSearchBar] Search error:', e);
|
||||
setErr((e && e.message) ? e.message : "Search error");
|
||||
setItems([]);
|
||||
} finally {
|
||||
if (!ctrl.signal.aborted) setLoading(false);
|
||||
}
|
||||
}, 150); // Réduit de 220ms à 150ms pour réactivité accrue
|
||||
try {
|
||||
console.log('[SmartSearchBar] Searching for:', searchQuery);
|
||||
const res = await recoSearch(searchQuery, { signal: ctrl.signal, limit: 12 });
|
||||
console.log('[SmartSearchBar] Got results:', res?.length || 0, res);
|
||||
setItems(res);
|
||||
setOpen(true);
|
||||
setActive(res.length ? 0 : -1);
|
||||
setHasSearched(true);
|
||||
} catch (e) {
|
||||
if (ctrl.signal.aborted) return;
|
||||
console.error('[SmartSearchBar] Search error:', e);
|
||||
setErr((e && e.message) ? e.message : "Search error");
|
||||
setItems([]);
|
||||
} finally {
|
||||
if (!ctrl.signal.aborted) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const query = q.trim();
|
||||
|
||||
if (abortRef.current) abortRef.current.abort();
|
||||
|
||||
const t = setTimeout(() => {
|
||||
triggerSearch(query);
|
||||
}, 150); // Debounce 150ms
|
||||
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
ctrl.abort();
|
||||
if (abortRef.current) abortRef.current.abort();
|
||||
};
|
||||
}, [q]);
|
||||
|
||||
|
|
@ -164,7 +165,13 @@ export default function SmartSearchBar({
|
|||
className="sw-search__input"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onFocus={() => setOpen(true)}
|
||||
onFocus={() => {
|
||||
setOpen(true);
|
||||
// Si pas encore cherché et query vide, charger suggestions populaires
|
||||
if (!hasSearched && !q.trim()) {
|
||||
triggerSearch("");
|
||||
}
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
aria-label="Search"
|
||||
|
|
|
|||
Loading…
Reference in New Issue