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 { useAuth } from "../../auth/AuthContext";
import FilterButtons from "../Filters/FilterButtons"; import FilterButtons from "../Filters/FilterButtons";
import TimeFilterButtons from "../Filters/TimeFilterButtons"; import TimeFilterButtons from "../Filters/TimeFilterButtons";
import SmartSearchBar from "../Search/SmartSearchBar";
export default function MapView({ export default function MapView({
theme = "dark", theme = "dark",
@ -208,6 +209,35 @@ export default function MapView({
map.flyTo({ center: userPosition, zoom: 9, speed: 1.2 }); 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 = () => { const handleOpenCreate = () => {
if (!authenticated) { if (!authenticated) {
alert("You must be logged in to place a wire."); alert("You must be logged in to place a wire.");
@ -413,38 +443,15 @@ export default function MapView({
<div ref={containerRef} className="map-container" /> <div ref={containerRef} className="map-container" />
{status && <div className="map-status">{status}</div>} {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="map-overlay map-overlay-top">
<div className="sw-top-row"> <div className="sw-top-row">
<form <SmartSearchBar
className="sw-searchbar sw-top-search" placeholder="Search cities, places, posts..."
onSubmit={handleSearchSubmit} onPick={handlePickSuggestion}
role="search" onMySpot={handleFlyToMe}
aria-label="Search" onPlaceTourWire={handleOpenCreate}
> />
<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>
<div className="map-top-actions"> <div className="map-top-actions">
<button <button

View File

@ -479,34 +479,54 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
root.style.position = "relative"; root.style.position = "relative";
root.style.cursor = "pointer"; root.style.cursor = "pointer";
// Marqueur circulaire simple // Marqueur pin avec image en haut et pointe en bas
root.innerHTML = ` root.innerHTML = `
<div class="sw-simple-marker" style=" <div class="sw-simple-pin-container" style="
width: 32px; position: relative;
height: 32px; width: 40px;
border-radius: 50%; height: 52px;
background: ${color};
border: 2px solid white;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
display: flex; display: flex;
flex-direction: column;
align-items: center; 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 class="sw-simple-marker" style="
<div style=" width: 32px;
width: 12px; height: 32px;
height: 12px; border-radius: 50%;
border-radius: 50%; background: ${color};
background: white; border: 2px solid white;
opacity: 0.9; box-shadow: 0 2px 8px rgba(0,0,0,0.3);
"></div> 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>
<div class="sw-simple-hover-card" style=" <div class="sw-simple-hover-card" style="
position: absolute; position: absolute;
bottom: 40px; bottom: 58px;
left: 50%; left: 50%;
transform: translateX(-50%) scale(0.9); transform: translateX(-50%) scale(0.9);
opacity: 0; opacity: 0;
@ -521,6 +541,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
padding: 10px 12px; padding: 10px 12px;
border: 1px solid rgba(90, 190, 255, 0.7); border: 1px solid rgba(90, 190, 255, 0.7);
box-shadow: 0 4px 20px rgba(0,0,0,0.5); 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;"> <div style="font-weight: 900; font-size: 12px; color: #e8f5ff; margin-bottom: 4px;">
${escapeHtml(post?.title || "Untitled")} ${escapeHtml(post?.title || "Untitled")}
@ -576,7 +597,7 @@ export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRe
const marker = new maplibregl.Marker({ const marker = new maplibregl.Marker({
element: root, element: root,
anchor: "center", anchor: "bottom", // La pointe du pin est à la position exacte
}) })
.setLngLat(lngLat) .setLngLat(lngLat)
.addTo(map); .addTo(map);

View File

@ -67,6 +67,7 @@ export default function SmartSearchBar({
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [active, setActive] = useState(-1); const [active, setActive] = useState(-1);
const [err, setErr] = useState(""); const [err, setErr] = useState("");
const [hasSearched, setHasSearched] = useState(false);
const abortRef = useRef(null); const abortRef = useRef(null);
const boxRef = useRef(null); const boxRef = useRef(null);
@ -80,45 +81,45 @@ export default function SmartSearchBar({
return () => document.removeEventListener("mousedown", onDocDown); return () => document.removeEventListener("mousedown", onDocDown);
}, []); }, []);
useEffect(() => { // Fonction pour déclencher une recherche
const query = q.trim(); const triggerSearch = async (searchQuery) => {
setErr("");
if (abortRef.current) abortRef.current.abort(); 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(); const ctrl = new AbortController();
abortRef.current = ctrl; abortRef.current = ctrl;
setLoading(true); setLoading(true);
setErr("");
const t = setTimeout(async () => { try {
try { console.log('[SmartSearchBar] Searching for:', searchQuery);
console.log('[SmartSearchBar] Searching for:', query); const res = await recoSearch(searchQuery, { signal: ctrl.signal, limit: 12 });
const res = await recoSearch(query, { signal: ctrl.signal, limit: 12 }); console.log('[SmartSearchBar] Got results:', res?.length || 0, res);
console.log('[SmartSearchBar] Got results:', res?.length || 0, res); setItems(res);
setItems(res); setOpen(true);
setOpen(true); setActive(res.length ? 0 : -1);
setActive(res.length ? 0 : -1); setHasSearched(true);
} catch (e) { } catch (e) {
if (ctrl.signal.aborted) return; if (ctrl.signal.aborted) return;
console.error('[SmartSearchBar] Search error:', e); console.error('[SmartSearchBar] Search error:', e);
setErr((e && e.message) ? e.message : "Search error"); setErr((e && e.message) ? e.message : "Search error");
setItems([]); setItems([]);
} finally { } finally {
if (!ctrl.signal.aborted) setLoading(false); if (!ctrl.signal.aborted) setLoading(false);
} }
}, 150); // Réduit de 220ms à 150ms pour réactivité accrue };
useEffect(() => {
const query = q.trim();
if (abortRef.current) abortRef.current.abort();
const t = setTimeout(() => {
triggerSearch(query);
}, 150); // Debounce 150ms
return () => { return () => {
clearTimeout(t); clearTimeout(t);
ctrl.abort(); if (abortRef.current) abortRef.current.abort();
}; };
}, [q]); }, [q]);
@ -164,7 +165,13 @@ export default function SmartSearchBar({
className="sw-search__input" className="sw-search__input"
value={q} value={q}
onChange={(e) => setQ(e.target.value)} 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} onKeyDown={onKeyDown}
placeholder={placeholder} placeholder={placeholder}
aria-label="Search" aria-label="Search"