Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion test/ui/mapTheme.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ describe('the page', () => {

it('carries the fullscreen toggle on the first heading, not floating above the box', () => {
expect(viewJsx).toMatch(/panels=\{\(controls, expandButton\) =>/);
expect(viewJsx).toMatch(/\{t\('map\.groupMap'\)\}\s*\{expandButton\}/);
// The text is wrapped in its own span so a phone can hide it without hiding the button next to
// it - see Map.mobile.less, which repeats it as the fold pill's own label below 430px.
expect(viewJsx).toMatch(/\{t\('map\.groupMap'\)\}<\/span>\s*\{expandButton\}/);
// The map renders it itself only where no panel took it.
expect(mapJsx).toMatch(/\{!controlsInPanels && expandButton\}/);
// Smaller in there than on the map, and it needs two classes to beat Semi's own height.
Expand Down
20 changes: 20 additions & 0 deletions ui/src/components/map/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,26 @@ export default function Map({
cooperativeGestures,
});

// MapLibre's own attribution control opens itself - a paragraph of text over the map, not the
// "i" it collapses to - until the map is first panned or zoomed. That visibility runs on its
// own `maplibregl-compact-show` class rather than the `<details>` element's native `open`
// state (maplibre-gl.css keys the attribution text's `display` off the class alone), so
// removing the class is what actually collapses it; the class is what `_toggleAttribution`
// re-adds on a click, so the "i" still opens and closes it same as before.
//
// Only below the width Map.mobile.less's own breakpoint uses: a map that size is small enough
// that the full text is the map, and wide desktop embeds keep the attribution they had. Added
// synchronously (`AttributionControl.onAdd` runs, and adds the class, inside the `Map`
// constructor above, before the style has even loaded) and again on `load` in case MapLibre
// re-adds it once the style is ready.
const collapseAttribution = () => {
const container = mapContainerRef.current;
if (!container || container.offsetWidth > 430) return;
container.querySelector('.maplibregl-ctrl-attrib')?.classList.remove('maplibregl-compact-show');
};
collapseAttribution();
mapRef.current.once('load', collapseAttribution);

// Left, because the panels now live in the top right corner and a zoom button hiding behind
// them helps nobody. Where drawing is enabled MapLibre simply stacks these below its tools.
mapRef.current.addControl(
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
"map.noHomeAddressLink": "Benutzereinstellungen",
"map.noHomeAddressAfter": ", um den Entfernungsfilter zu nutzen.",
"map.groupMap": "Karte",
"map.foldToggle": "Kartenfilter ein- oder ausblenden",
"map.groupListings": "Inserate",
"map.searchPlaceholder": "Adresse oder Ort suchen",
"map.searchLabel": "Adresssuche auf der Karte",
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
"map.noHomeAddressLink": "user settings",
"map.noHomeAddressAfter": " to use the distance filter.",
"map.groupMap": "Map",
"map.foldToggle": "Show or hide the map filters",
"map.groupListings": "Listings",
"map.searchPlaceholder": "Search an address or place",
"map.searchLabel": "Address search on the map",
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
"map.noHomeAddressLink": "los ajustes de usuario",
"map.noHomeAddressAfter": " para usar el filtro de distancia.",
"map.groupMap": "Mapa",
"map.foldToggle": "Mostrar u ocultar los filtros del mapa",
"map.groupListings": "Anuncios",
"map.searchPlaceholder": "Buscar una dirección o un lugar",
"map.searchLabel": "Búsqueda de direcciones en el mapa",
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
"map.noHomeAddressLink": "les paramètres utilisateur",
"map.noHomeAddressAfter": " pour utiliser le filtre de distance.",
"map.groupMap": "Carte",
"map.foldToggle": "Afficher ou masquer les filtres de la carte",
"map.groupListings": "Annonces",
"map.searchPlaceholder": "Rechercher une adresse ou un lieu",
"map.searchLabel": "Recherche d'adresse sur la carte",
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
"map.noHomeAddressLink": "impostazioni utente",
"map.noHomeAddressAfter": " per usare il filtro per distanza.",
"map.groupMap": "Mappa",
"map.foldToggle": "Mostra o nascondi i filtri della mappa",
"map.groupListings": "Annunci",
"map.searchPlaceholder": "Cerca un indirizzo o un luogo",
"map.searchLabel": "Ricerca di indirizzi sulla mappa",
Expand Down
1 change: 1 addition & 0 deletions ui/src/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@
"map.noHomeAddressLink": "kullanıcı ayarlarından",
"map.noHomeAddressAfter": " uzaklık filtresini kullanmak için yapılandırın.",
"map.groupMap": "Harita",
"map.foldToggle": "Harita filtrelerini göster veya gizle",
"map.groupListings": "İlanlar",
"map.searchPlaceholder": "Adres veya yer ara",
"map.searchLabel": "Haritada adres arama",
Expand Down
216 changes: 122 additions & 94 deletions ui/src/views/listings/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import {
groupListingsByPosition,
} from './mapUtils.js';
import { Select, Switch, Toast, Typography } from '@douyinfe/semi-ui-19';
import { IconChevronDown } from '@douyinfe/semi-icons';

import _RangeSlider from 'react-range-slider-input';
import 'react-range-slider-input/dist/style.css';
import './Map.less';
import './Map.mobile.less';
import { xhrDelete, errorMessage } from '../../services/xhr.js';
import { Link, useNavigate, useSearchParams } from 'react-router';
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
Expand Down Expand Up @@ -661,112 +663,138 @@ export default function MapView() {
what the map is showing, so they read as one panel with a line between them rather
than as two identical boxes four pixels apart, neither of them with a heading. */
<div className="map-panel">
{/* The fullscreen toggle rides on this heading rather than floating above the
panel: it is a control over the map as a whole, and this is the line that names
the map. */}
<div className="map-panel__groupTitle">
{/* The fold toggle: a plain checkbox driving CSS, not React state, so the panel
starts folded on a phone and stays open everywhere else without a resize
listener - see Map.mobile.less for the media query that pins it open above the
breakpoint and the one that ties `:checked` to the body's visibility.
Unchecked by default: nothing here reads the fold state back, it is view, not
data, so there is nothing to initialise beyond the folded starting point. */}
<input
type="checkbox"
id="map-panel-fold"
className="map-panel__fold-input"
aria-label={t('map.foldToggle')}
/>
<label htmlFor="map-panel-fold" className="map-panel__fold-label">
{t('map.groupMap')}
{expandButton}
</div>
{controls}

<div className="map-panel__divider" />

<div className="map-panel__groupTitle">{t('map.groupListings')}</div>

<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterJobLabel')}
</Text>
<Select
placeholder={t('map.filterJobPlaceholder')}
showClear
size="small"
onChange={(val) => setJobId(val)}
value={jobId}
style={{ width: 160 }}
>
{jobs?.map((j) => (
<Select.Option key={j.id} value={j.id}>
{j.name}
</Select.Option>
))}
</Select>
</div>
<IconChevronDown className="map-panel__fold-chevron" />
</label>

<div className="map-panel__body">
{/* The fullscreen toggle rides on this heading rather than floating above the
panel: it is a control over the map as a whole, and this is the line that
names the map. The text repeats the fold label above once unfolded, so
Map.mobile.less hides it below the fold breakpoint and leaves the button. */}
<div className="map-panel__groupTitle">
<span className="map-panel__groupTitle-text">{t('map.groupMap')}</span>
{expandButton}
</div>
{controls}

<div className="map-panel__divider" />

<div className="map-panel__groupTitle">{t('map.groupListings')}</div>

<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterJobLabel')}
</Text>
<Select
placeholder={t('map.filterJobPlaceholder')}
showClear
size="small"
onChange={(val) => setJobId(val)}
value={jobId}
style={{ width: 160 }}
>
{jobs?.map((j) => (
<Select.Option key={j.id} value={j.id}>
{j.name}
</Select.Option>
))}
</Select>
</div>

{/* Disabled rather than hidden, and it says why one line below. A control that
{/* Disabled rather than hidden, and it says why one line below. A control that
cannot work is the honest place for that sentence - it used to be a full-width
banner above the map, on every visit, for a fact that never changes. */}
<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterDistanceLabel')}
</Text>
<Select
placeholder={t('map.filterDistanceNone')}
size="small"
disabled={!hasHome}
onChange={(val) => setDistanceFilter(val)}
value={distanceFilter}
style={{ width: 100 }}
>
<Select.Option value={0}>{t('map.filterDistanceNone')}</Select.Option>
<Select.Option value={5}>5 km</Select.Option>
<Select.Option value={10}>10 km</Select.Option>
<Select.Option value={15}>15 km</Select.Option>
<Select.Option value={20}>20 km</Select.Option>
<Select.Option value={25}>25 km</Select.Option>
</Select>
</div>
<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterDistanceLabel')}
</Text>
<Select
placeholder={t('map.filterDistanceNone')}
size="small"
disabled={!hasHome}
onChange={(val) => setDistanceFilter(val)}
value={distanceFilter}
style={{ width: 100 }}
>
<Select.Option value={0}>{t('map.filterDistanceNone')}</Select.Option>
<Select.Option value={5}>5 km</Select.Option>
<Select.Option value={10}>10 km</Select.Option>
<Select.Option value={15}>15 km</Select.Option>
<Select.Option value={20}>20 km</Select.Option>
<Select.Option value={25}>25 km</Select.Option>
</Select>
</div>

{/* Locked rather than hidden, for the same reason as the ring above. Unlike the
{/* Locked rather than hidden, for the same reason as the ring above. Unlike the
distance ring, which recolours pins, this one hides them: a commute ceiling is
asked as "show me only what I could live with". */}
<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterCommuteLabel')}
</Text>
<Select
placeholder={t('map.filterCommuteNone')}
showClear
size="small"
disabled={!hasHome}
onChange={(val) => setCommuteFilter(val ?? null)}
value={commuteFilter}
style={{ width: 150 }}
>
{COMMUTE_OPTIONS.map(({ mode, minutes }) =>
minutes.map((max) => (
<Select.Option key={`${mode}:${max}`} value={`${mode}:${max}`}>
{t('listings.filterCommuteOption', { mode: t(`travelTime.mode.${mode}`), minutes: max })}
</Select.Option>
)),
)}
</Select>
</div>
<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterCommuteLabel')}
</Text>
<Select
placeholder={t('map.filterCommuteNone')}
showClear
size="small"
disabled={!hasHome}
onChange={(val) => setCommuteFilter(val ?? null)}
value={commuteFilter}
style={{ width: 150 }}
>
{COMMUTE_OPTIONS.map(({ mode, minutes }) =>
minutes.map((max) => (
<Select.Option key={`${mode}:${max}`} value={`${mode}:${max}`}>
{t('listings.filterCommuteOption', { mode: t(`travelTime.mode.${mode}`), minutes: max })}
</Select.Option>
)),
)}
</Select>
</div>

<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterPriceLabel')}
</Text>
<div className="map-view-container__price-slider">
<div className="map__rangesliderLabels">
<span>{formatEuroCompact(priceRange[0], locale)}</span>
<span>{formatEuroCompact(priceRange[1] || getMaxPrice(), locale)}</span>
<div className="map-panel__row">
<Text size="small" strong className="map-panel__label">
{t('map.filterPriceLabel')}
</Text>
<div className="map-view-container__price-slider">
<div className="map__rangesliderLabels">
<span>{formatEuroCompact(priceRange[0], locale)}</span>
<span>{formatEuroCompact(priceRange[1] || getMaxPrice(), locale)}</span>
</div>
<RangeSlider
min={0}
max={getMaxPrice()}
step={100}
value={priceRange}
onInput={handlePriceRange}
/>
</div>
<RangeSlider min={0} max={getMaxPrice()} step={100} value={priceRange} onInput={handlePriceRange} />
</div>
</div>

{!hasHome && (
<div className="map-panel__hint">
{t('map.noHomeAddressBefore')}
<Link to="/settings/travel-time">{t('map.noHomeAddressLink')}</Link>
{t('map.noHomeAddressAfter')}
</div>
)}
{!hasHome && (
<div className="map-panel__hint">
{t('map.noHomeAddressBefore')}
<Link to="/settings/travel-time">{t('map.noHomeAddressLink')}</Link>
{t('map.noHomeAddressAfter')}
</div>
)}

<div className="map-panel__divider" />
<MapLegend hasStacks={hasStacks} hasRing={distanceFilter > 0 && hasHome} hasHome={hasHome} />
<div className="map-panel__divider" />
<MapLegend hasStacks={hasStacks} hasRing={distanceFilter > 0 && hasHome} hasHome={hasHome} />
</div>
</div>
)}
/>
Expand Down
Loading