diff --git a/package.json b/package.json index 76fcad45..1b9f52e3 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fredy", - "version": "20.0.8", + "version": "20.1.0", "description": "[F]ind [R]eal [E]states [d]amn eas[y].", "scripts": { "prepare": "husky", diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 7d7888c8..cff9c619 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -8,7 +8,6 @@ import React, { useEffect } from 'react'; import InsufficientPermission from './components/permission/InsufficientPermission'; import PermissionAwareRoute from './components/permission/PermissionAwareRoute'; import GeneralSettings from './views/generalSettings/GeneralSettings'; -import UserSettings from './views/userSettings/UserSettings'; import JobMutation from './views/jobs/mutation/JobMutation'; import UserMutator from './views/user/mutation/UserMutator'; import { useActions, useSelector } from './services/state/store'; @@ -127,15 +126,8 @@ export default function FredyApp() { } /> - } /> - - - - } - /> + } /> + } /> } /> diff --git a/ui/src/components/cards/DashboardCard.less b/ui/src/components/cards/DashboardCard.less index ea0c778a..1af2fd92 100644 --- a/ui/src/components/cards/DashboardCard.less +++ b/ui/src/components/cards/DashboardCard.less @@ -1,12 +1,14 @@ +@import './DashboardCardColors.less'; + .dashboard-card { width: 100%; height: 140px; margin-bottom: 16px; - transition: transform 0.2s; - background-color: rgba(36, 36, 36, 0.9); - backdrop-filter: blur(8px); - border: 1px solid var(--semi-color-border); - --pulse-color: rgba(255, 255, 255, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + background-color: #181b26; + border: 1px solid #232735; + border-radius: 10px; + --pulse-color: rgba(255, 255, 255, 0.08); position: relative; z-index: 1; overflow: visible; @@ -32,6 +34,14 @@ display: flex; align-items: center; justify-content: center; + color: var(--card-accent, #94a3b8); + } + + &__title { + color: var(--semi-color-text-2) !important; + font-size: 12px !important; + text-transform: uppercase; + letter-spacing: 0.05em; } &__content { @@ -41,32 +51,51 @@ &__value { font-weight: 700; margin-bottom: 4px; - color: var(--semi-color-text-0); + color: var(--card-accent, var(--semi-color-text-0)); + } + + &__desc { + color: var(--semi-color-text-3) !important; } &.blue { - --pulse-color: var(--semi-color-primary); - box-shadow: 0 4px 20px -5px var(--pulse-color); + --pulse-color: @color-blue-border; + --card-accent: @color-blue-text; + background-color: @color-blue-bg; + border-color: @color-blue-border; + box-shadow: 0 2px 16px -6px @color-blue-border; } &.orange { - --pulse-color: var(--semi-color-warning); - box-shadow: 0 4px 20px -5px var(--pulse-color); + --pulse-color: @color-orange-border; + --card-accent: @color-orange-text; + background-color: @color-orange-bg; + border-color: @color-orange-border; + box-shadow: 0 2px 16px -6px @color-orange-border; } &.green { - --pulse-color: var(--semi-color-success); - box-shadow: 0 4px 20px -5px var(--pulse-color); + --pulse-color: @color-green-border; + --card-accent: @color-green-text; + background-color: @color-green-bg; + border-color: @color-green-border; + box-shadow: 0 2px 16px -6px @color-green-border; } &.purple { - --pulse-color: var(--semi-color-info); - box-shadow: 0 4px 20px -5px var(--pulse-color); + --pulse-color: @color-purple-border; + --card-accent: @color-purple-text; + background-color: @color-purple-bg; + border-color: @color-purple-border; + box-shadow: 0 2px 16px -6px @color-purple-border; } &.gray { - --pulse-color: rgba(255, 255, 255, 0.2); - box-shadow: 0 4px 20px -5px var(--pulse-color); + --pulse-color: @color-gray-border; + --card-accent: @color-gray-text; + background-color: @color-gray-bg; + border-color: @color-gray-border; + box-shadow: 0 2px 16px -6px @color-gray-border; } } @@ -75,6 +104,6 @@ opacity: 0.1; } 50% { - opacity: 0.5; + opacity: 0.4; } -} \ No newline at end of file +} diff --git a/ui/src/components/cards/DashboardCardColors.less b/ui/src/components/cards/DashboardCardColors.less index 36408e9e..4e8857cd 100644 --- a/ui/src/components/cards/DashboardCardColors.less +++ b/ui/src/components/cards/DashboardCardColors.less @@ -1,19 +1,19 @@ -@color-blue-bg: rgba(0, 123, 255, 0.24); -@color-blue-border: #1E40AFFF; +@color-blue-bg: rgba(96, 165, 250, 0.10); +@color-blue-border: #3b6ea8; @color-blue-text: #60a5fa; -@color-orange-bg: rgba(250, 91, 5, 0.12); -@color-orange-border: #992f0c; -@color-orange-text: #FB923CFF; +@color-orange-bg: rgba(251, 146, 60, 0.10); +@color-orange-border: #c2622a; +@color-orange-text: #fb923c; -@color-green-bg: rgba(38, 250, 5, 0.12); -@color-green-border: #278832; -@color-green-text: #33f308; +@color-green-bg: rgba(52, 211, 153, 0.10); +@color-green-border: #2a8a61; +@color-green-text: #34d399; -@color-purple-bg: rgba(91, 3, 218, 0.38); -@color-purple-border: #7500c3; -@color-purple-text: #b15fff; +@color-purple-bg: rgba(167, 139, 250, 0.10); +@color-purple-border: #6d4fc2; +@color-purple-text: #a78bfa; -@color-gray-bg: rgba(110, 110, 110, 0.38); -@color-gray-border: #807f7f; -@color-gray-text: #bab9b9; +@color-gray-bg: rgba(148, 163, 184, 0.10); +@color-gray-border: #323a47; +@color-gray-text: #94a3b8; diff --git a/ui/src/components/grid/jobs/JobGrid.jsx b/ui/src/components/grid/jobs/JobGrid.jsx index 3b763c15..9e1ab61e 100644 --- a/ui/src/components/grid/jobs/JobGrid.jsx +++ b/ui/src/components/grid/jobs/JobGrid.jsx @@ -9,7 +9,6 @@ import { Col, Row, Button, - Space, Typography, Divider, Switch, @@ -20,6 +19,8 @@ import { Pagination, Toast, Empty, + Radio, + RadioGroup, } from '@douyinfe/semi-ui-19'; import { IconAlertTriangle, @@ -31,8 +32,10 @@ import { IconBriefcase, IconBell, IconSearch, - IconFilter, IconPlusCircle, + IconArrowUp, + IconArrowDown, + IconHome, } from '@douyinfe/semi-icons'; import { useNavigate } from 'react-router-dom'; import ListingDeletionModal from '../../ListingDeletionModal.jsx'; @@ -59,8 +62,6 @@ const JobGrid = () => { const [sortDir, setSortDir] = useState('asc'); const [freeTextFilter, setFreeTextFilter] = useState(null); const [activityFilter, setActivityFilter] = useState(null); - const [showFilterBar, setShowFilterBar] = useState(false); - const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [pendingDeletion, setPendingDeletion] = useState(null); // { type: 'job'|'listings', jobId } @@ -200,73 +201,45 @@ const JobGrid = () => { return (
- +
-
- } showClear placeholder="Search" onChange={handleFilterChange} /> -
- - - {showFilterBar && ( -
- -
-
- Filter by: -
-
- -
-
- -
-
- Sort by: -
-
- - - -
-
-
-
- )} + + } + showClear + placeholder="Search" + onChange={handleFilterChange} + /> + + { + const v = e.target.value; + setActivityFilter(v === 'all' ? null : v === 'true'); + }} + > + All + Active + Inactive + + + + +
{(jobsData?.result || []).length === 0 && ( { {(jobsData?.result || []).map((job) => ( - - + + +
+
+ {job.name} -
- {job.isOnlyShared && ( - -
- -
-
- )} -
+
+
+ {job.isOnlyShared && ( + +
+ +
+
+ )} {job.running && ( RUNNING )}
- } - > -
- -
- } size="small"> - Is active: - - onJobStatusChanged(job.id, checked)} - style={{ marginLeft: 'auto' }} - checked={job.enabled} - disabled={job.isOnlyShared} - size="small" - /> -
-
- } size="small"> - Listings: - - - {job.numberOfFoundListings || 0} - -
-
- } size="small"> - Providers: - - - {job.provider.length || 0} - -
-
- } size="small"> - Adapters: - - - {job.notificationAdapter.length || 0} - -
-
+
- +
+
+ {job.numberOfFoundListings || 0} + + Listings + +
+
+ {job.provider.length || 0} + + Providers + +
+
+ {job.notificationAdapter.length || 0} + + Adapters + +
+
+ + +
+
+ onJobStatusChanged(job.id, checked)} + checked={job.enabled} + disabled={job.isOnlyShared} + size="small" + /> + + Active + +
diff --git a/ui/src/components/grid/jobs/JobGrid.less b/ui/src/components/grid/jobs/JobGrid.less index 5ce81019..6c62a543 100644 --- a/ui/src/components/grid/jobs/JobGrid.less +++ b/ui/src/components/grid/jobs/JobGrid.less @@ -1,3 +1,5 @@ +@import '../../cards/DashboardCardColors.less'; + .jobGrid { &__card { height: 100%; @@ -12,55 +14,137 @@ box-shadow: 0 0 15px -3px rgb(78 78 78 / 70%); background-color: rgba(36, 36, 36, 1); } - } - &__searchbar { - display: flex; - gap: .5rem; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; - } + &__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 16px; + } - &__toolbar { - &__card { - border-radius: var(--semi-border-radius-medium); + &__name { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + + &__dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + background-color: var(--semi-color-text-3); + + &--active { + background-color: #21aa21; + } + } + + &__stats { + display: flex; + gap: 8px; + } + + &__stat { + flex: 1; display: flex; flex-direction: column; - gap: .3rem; - background: rgba(36, 36, 36, 0.9); - backdrop-filter: blur(8px); - padding: 0.5rem; - border: 1px solid var(--semi-color-border); + align-items: center; + background: rgba(255, 255, 255, 0.04); + border: 1px solid transparent; + border-radius: var(--semi-border-radius-small); + padding: 10px 4px 8px; + + &__number { + font-size: 22px; + font-weight: 600; + color: var(--semi-color-text-0); + line-height: 1.2; + } + + &__label { + font-size: 11px; + color: var(--semi-color-text-3); + display: flex; + align-items: center; + gap: 3px; + margin-top: 4px; + } + + &--blue { + background: @color-blue-bg; + border-color: @color-blue-border; + .jobGrid__card__stat__number { color: @color-blue-text; } + .jobGrid__card__stat__label { color: @color-blue-text; opacity: 0.7; } + } + + &--orange { + background: @color-orange-bg; + border-color: @color-orange-border; + .jobGrid__card__stat__number { color: @color-orange-text; } + .jobGrid__card__stat__label { color: @color-orange-text; opacity: 0.7; } + } + + &--purple { + background: @color-purple-bg; + border-color: @color-purple-border; + .jobGrid__card__stat__number { color: @color-purple-text; } + .jobGrid__card__stat__label { color: @color-purple-text; opacity: 0.7; } + } + } + + &__footer { + display: flex; + align-items: center; + justify-content: space-between; } } - &__header { + &__topbar { display: flex; + flex-wrap: nowrap; align-items: center; - justify-content: space-between; - } + gap: 8px; + margin-bottom: 16px; - &__title { - margin-bottom: 0 !important; - } + .jobGrid__topbar__search { + flex: 1; + min-width: 0; + } - &__infoItem { - display: flex; - align-items: center; - width: 100%; + @media (max-width: 768px) { + flex-wrap: wrap; - .semi-typography { - display: flex; - align-items: center; - gap: 4px; + .semi-button:first-child { + flex-shrink: 0; + } + + .jobGrid__topbar__search { + flex: 1; + min-width: 160px; + } + + .semi-radio-group { + flex: 1; + } + + .semi-select { + flex: 1; + min-width: 100px; + width: auto !important; + } } } + &__title { + margin-bottom: 0 !important; + } + &__actions { display: flex; - justify-content: space-between; - gap: 8px; + gap: 6px; } &__pagination { diff --git a/ui/src/components/grid/listings/ListingsGrid.jsx b/ui/src/components/grid/listings/ListingsGrid.jsx index d555db21..e47f00c8 100644 --- a/ui/src/components/grid/listings/ListingsGrid.jsx +++ b/ui/src/components/grid/listings/ListingsGrid.jsx @@ -10,15 +10,15 @@ import { Row, Image, Button, - Space, Typography, Pagination, Toast, Divider, Input, Select, - Popover, Empty, + Radio, + RadioGroup, } from '@douyinfe/semi-ui-19'; import { IconBriefcase, @@ -30,9 +30,10 @@ import { IconStar, IconStarStroked, IconSearch, - IconFilter, IconActivity, IconEyeOpened, + IconArrowUp, + IconArrowDown, } from '@douyinfe/semi-icons'; import { useNavigate } from 'react-router-dom'; import ListingDeletionModal from '../../ListingDeletionModal.jsx'; @@ -64,8 +65,6 @@ const ListingsGrid = () => { const [jobNameFilter, setJobNameFilter] = useState(null); const [activityFilter, setActivityFilter] = useState(null); const [providerFilter, setProviderFilter] = useState(null); - const [showFilterBar, setShowFilterBar] = useState(false); - const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [listingToDelete, setListingToDelete] = useState(null); @@ -129,107 +128,84 @@ const ListingsGrid = () => { return (
-
- } showClear placeholder="Search" onChange={handleFilterChange} /> - -
-
-
-
- {showFilterBar && ( -
- -
-
- Filter by: -
-
- +
+ } + showClear + placeholder="Search" + onChange={handleFilterChange} + /> - + { + const v = e.target.value; + setActivityFilter(v === 'all' ? null : v === 'true'); + }} + > + All + Active + Inactive + - + { + const v = e.target.value; + setWatchListFilter(v === 'all' ? null : v === 'true'); + }} + > + All + Watched + Unwatched + - -
-
- + -
-
- Sort by: -
-
- + - -
-
- -
- )} + + +
{(listingsData?.result || []).length === 0 && ( { )} {(listingsData?.result || []).map((item) => ( - + { {cap(item.title)} - - } size="small"> - {item.price} € - +
+ + {item.price} € +
+
} @@ -305,18 +282,17 @@ const ListingsGrid = () => { ) : ( }> - Distance cannot be calculated, provide an address + Distance cannot be calculated )} - +
-
+
e.stopPropagation()}>
- - - - - + + } + description="Time interval for job execution" + /> - - - - - } - description="Total number of jobs" - /> - - - } - description="Total listings found" - /> - - - } - description="Total active listings" - /> - - - } - description="Avg. Price of listings" - /> - - - + + } + description="Last execution timestamp" + /> + + + } + description="Next execution timestamp" + /> + + + } description="Run a search now"> + + + + + + Overview + + + } + description="Total number of jobs" + /> + + + } + description="Total listings found" + /> + + + } + description="Total active listings" + /> + + + } + description="Avg. Price of listings" + /> - + Provider Insights +
- +
); } diff --git a/ui/src/views/dashboard/Dashboard.less b/ui/src/views/dashboard/Dashboard.less index da62d633..7d7b22a5 100644 --- a/ui/src/views/dashboard/Dashboard.less +++ b/ui/src/views/dashboard/Dashboard.less @@ -3,31 +3,32 @@ flex-direction: column; flex: 1; + &__section-label { + display: block; + font-size: 11px !important; + font-weight: 600 !important; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #5a6478 !important; + margin-bottom: 10px; + margin-top: 4px; + } + &__row { - margin-bottom: 24px; + margin-bottom: 8px; flex-wrap: wrap; - - .semi-col { - margin-bottom: 0; // Handled by Row gutter - } } - &__provider-insights { + &__pie-wrapper { + background: #23242a; + border: 1px solid #37404e; + + border-radius: 10px; + padding: 24px; + max-height: 320px; flex: 1; display: flex; flex-direction: column; - margin: 0 !important; - - .semi-card-body { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - max-height: 300px; - - > * { - flex: 1; - } - } + justify-content: center; } } diff --git a/ui/src/views/generalSettings/GeneralSettings.jsx b/ui/src/views/generalSettings/GeneralSettings.jsx index 084c2416..d0da96a2 100644 --- a/ui/src/views/generalSettings/GeneralSettings.jsx +++ b/ui/src/views/generalSettings/GeneralSettings.jsx @@ -3,31 +3,38 @@ * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ -import React from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; -import { useActions, useSelector } from '../../services/state/store'; +import { useActions, useSelector, useIsLoading } from '../../services/state/store'; -import { Divider, TimePicker, Button, Checkbox, Input, Modal } from '@douyinfe/semi-ui-19'; +import { + Tabs, + TabPane, + TimePicker, + Button, + Checkbox, + Input, + Modal, + Typography, + AutoComplete, + Switch, + Banner, +} from '@douyinfe/semi-ui-19'; import { InputNumber } from '@douyinfe/semi-ui-19'; -import { xhrPost } from '../../services/xhr'; +import { xhrPost, xhrGet } from '../../services/xhr'; +import { Toast } from '@douyinfe/semi-ui-19'; import { SegmentPart } from '../../components/segment/SegmentPart'; -import { Banner, Toast } from '@douyinfe/semi-ui-19'; import { downloadBackup as downloadBackupZip, precheckRestore as clientPrecheckRestore, restore as clientRestore, } from '../../services/backupRestoreClient'; -import { - IconSave, - IconCalendar, - IconRefresh, - IconSignal, - IconLineChartStroked, - IconSearch, - IconFolder, -} from '@douyinfe/semi-icons'; +import { IconSave, IconRefresh, IconSignal, IconHome, IconFolder } from '@douyinfe/semi-icons'; +import debounce from 'lodash/debounce'; import './GeneralSettings.less'; +const { Text } = Typography; + function formatFromTimestamp(ts) { const date = new Date(ts); return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`; @@ -63,6 +70,14 @@ const GeneralSettings = function GeneralSettings() { const [restoreBusy, setRestoreBusy] = React.useState(false); const [selectedRestoreFile, setSelectedRestoreFile] = React.useState(null); + // User settings state + const homeAddress = useSelector((state) => state.userSettings.settings.home_address); + const immoscoutDetails = useSelector((state) => state.userSettings.settings.immoscout_details); + const [address, setAddress] = useState(homeAddress?.address || ''); + const [coords, setCoords] = useState(homeAddress?.coords || null); + const saving = useIsLoading(actions.userSettings.setHomeAddress); + const [dataSource, setDataSource] = useState([]); + React.useEffect(() => { async function init() { await actions.generalSettings.getGeneralSettings(); @@ -86,6 +101,11 @@ const GeneralSettings = function GeneralSettings() { init(); }, [settings]); + useEffect(() => { + setAddress(homeAddress?.address || ''); + setCoords(homeAddress?.coords || null); + }, [homeAddress]); + const nullOrEmpty = (val) => val == null || val.length === 0; const handleStore = async () => { @@ -177,7 +197,6 @@ const GeneralSettings = function GeneralSettings() { if (!file) return; setSelectedRestoreFile(file); await precheckRestore(file); - // reset the input to allow same file re-select ev.target.value = ''; }, [precheckRestore], @@ -189,180 +208,280 @@ const GeneralSettings = function GeneralSettings() { } }, []); + const handleSaveUserSettings = async () => { + try { + const responseJson = await actions.userSettings.setHomeAddress(address); + setCoords(responseJson.coords); + await actions.userSettings.getUserSettings(); + Toast.success('Settings saved. Distance calculations are running in the background.'); + } catch (error) { + Toast.error(error.json?.error || 'Error while saving settings'); + } + }; + + const debouncedSearch = useMemo( + () => + debounce((value) => { + xhrGet(`/api/user/settings/autocomplete?q=${encodeURIComponent(value)}`) + .then((response) => { + if (response.status === 200) { + setDataSource(response.json); + } + }) + .catch(() => {}); + }, 300), + [], + ); + + const searchAddress = (value) => { + if (!value) { + setDataSource([]); + return; + } + debouncedSearch(value); + }; + return ( -
+
{!loading && ( - -
- + + + + System + + } + itemKey="system" > - `${value}`.replace(/\D/g, '')} - onChange={(value) => setInterval(value)} - suffix={'minutes'} - /> - - - -
- - - +
+ + `${value}`.replace(/\D/g, '')} + onChange={(value) => setPort(value)} + style={{ maxWidth: 160 }} + /> + + + + + setSqlitePath(value)} + /> + + + + setAnalyticsEnabled(e.target.checked)}> + Enable analytics + + + + + setDemoMode(e.target.checked)}> + Enable demo mode + + + +
+ +
- - - - `${value}`.replace(/\D/g, '')} - onChange={(value) => setPort(value)} - /> - - - + + + + Execution + + } + itemKey="execution" > - Warning
} - style={{ marginBottom: '1rem' }} - description={ -
- Changing the path later may result in data loss. -
- You must restart Fredy immediately after changing this setting! +
+ + `${value}`.replace(/\D/g, '')} + onChange={(value) => setInterval(value)} + suffix={'minutes'} + style={{ maxWidth: 200 }} + /> + + + +
+ { + setWorkingHourFrom(val == null ? null : formatFromTimestamp(val)); + }} + /> + { + setWorkingHourTo(val == null ? null : formatFromTimestamp(val)); + }} + />
- } - /> - - { - setSqlitePath(value); - }} - /> -
- - -
- { - setWorkingHourFrom(val == null ? null : formatFromTimestamp(val)); - }} - /> - { - setWorkingHourTo(val == null ? null : formatFromTimestamp(val)); - }} - /> + + +
+ +
-
- - - - Explanation
} - style={{ marginBottom: '1rem' }} - description={ -
- Analytics are disabled by default. If you choose to enable them, we will begin tracking the - following: -
-
    -
  • Name of active provider (e.g. Immoscout)
  • -
  • Name of active adapter (e.g. Console)
  • -
  • language
  • -
  • os
  • -
  • node version
  • -
  • arch
  • -
- The data is sent anonymously and helps me understand which providers or adapters are being used the - most. In the end it helps me to improve fredy. + + + + + Backup & Restore + + } + itemKey="backup" + > +
+ +
+ + +
- } - /> - - setAnalyticsEnabled(e.target.checked)}> - {' '} - Enabled - -
- - - - - Explanation
} - style={{ marginBottom: '1rem' }} - description={ -
- In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also - all database files will be set back to the default values at midnight. + +
+
+ + + + User Settings + + } + itemKey="userSettings" + > +
+ + setAddress(v)} + onSearch={searchAddress} + placeholder="Enter your home address" + style={{ width: '100%' }} + /> + {coords && coords.lat === -1 && ( + + )} + + + + +
+ { + try { + await actions.userSettings.setImmoscoutDetails(checked); + Toast.success('ImmoScout details setting updated.'); + } catch { + Toast.error('Failed to update setting.'); + } + }} + /> + Fetch detailed ImmoScout listings
- } - /> - - setDemoMode(e.target.checked)}> - {' '} - Enabled - -
- - - -
- + + +
+ +
+
+ + + )} + {restoreModalVisible && ( state.jobsData.jobs); const [jobId, setJobId] = useState(null); const [priceRange, setPriceRange] = useState([0, 0]); - const [showFilterBar, setShowFilterBar] = useState(false); const [distanceFilter, setDistanceFilter] = useState(0); const [deleteModalVisible, setDeleteModalVisible] = useState(false); @@ -92,10 +92,8 @@ export default function MapView() { }; }, [navigate]); - // Get map instance reference after MapComponent renders useEffect(() => { if (mapContainer.current && !map.current) { - // Wait for MapComponent to initialize the map const checkMapReady = () => { if (mapContainer.current?.map) { map.current = mapContainer.current.map; @@ -132,8 +130,6 @@ export default function MapView() { if (!map.current) return; if (homeAddress?.coords) { - // We only want to zoom/fly when distanceFilter OR homeAddress actually change, - // not on every render. useEffect dependency array handles this. if (distanceFilter > 0) { const bounds = getBoundsFromCenter([homeAddress.coords.lng, homeAddress.coords.lat], distanceFilter); @@ -290,7 +286,7 @@ export default function MapView() { const popup = new maplibregl.Popup({ offset: 25 }).setHTML(popupContent); - let color = '#3FB1CE'; // Default blue-ish + let color = '#3FB1CE'; if (distanceFilter > 0 && homeAddress?.coords) { const dist = distanceMeters( homeAddress.coords.lat, @@ -315,114 +311,17 @@ export default function MapView() { return (
-
-
- Map View - -
- 3D Buildings - setShow3dBuildings(v)} /> -
-
- -
-
-
-
- - {showFilterBar && ( -
- -
-
- Filter by: -
-
- -
-
- -
-
- Distance: -
-
- -
-
- -
-
- Price Range (€): -
-
-
- {priceRange[0]} € - {priceRange[1]} € -
- { - setPriceRange(val); - }} - tipFormatter={(val) => `${val} €`} - /> -
-
-
-
- )} - {!homeAddress && ( - You have not set your home address yet. Please do so in the user settings{' '} - to use the distance filter. + No home address set. Configure it in user settings to use the distance + filter. } /> @@ -433,10 +332,103 @@ export default function MapView() { type="info" bordered closeIcon={null} - description="Keep in mind, only listings with proper adresses are being shown on this map." + style={{ marginBottom: '8px' }} + description="Only listings with valid addresses are shown on this map." /> - +
+ + + {/* Floating filter panel */} +
+
+ + Job + + +
+ +
+ + Distance + + +
+ +
+ + Price (€) + +
+
+ {priceRange[0]} + {priceRange[1]} +
+ setPriceRange(val)} + /> +
+
+ +
+ + Style + + +
+ +
+ + 3D Buildings + + setShow3dBuildings(v)} + disabled={style === 'SATELLITE'} + /> +
+
+
+