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:
Your Name 2025-12-23 15:39:58 -05:00
parent 069072dc14
commit 28e760d616
3 changed files with 118 additions and 83 deletions

View File

@ -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

View File

@ -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);

View File

@ -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"