From 304b1dbfc2c96610be055d98753ddf3900a45623 Mon Sep 17 00:00:00 2001 From: orangecoding Date: Sun, 22 Mar 2026 09:14:21 +0100 Subject: [PATCH 1/7] ui-improvements --- ui/src/App.jsx | 12 +- ui/src/components/cards/DashboardCard.less | 70 ++- .../components/cards/DashboardCardColors.less | 28 +- ui/src/components/navigation/Navigation.jsx | 13 +- ui/src/views/dashboard/Dashboard.jsx | 221 ++++---- ui/src/views/dashboard/Dashboard.less | 38 +- .../views/generalSettings/GeneralSettings.jsx | 484 +++++++++++------- .../generalSettings/GeneralSettings.less | 24 +- ui/src/views/listings/Map.jsx | 214 ++++---- ui/src/views/listings/Map.less | 61 ++- 10 files changed, 674 insertions(+), 491 deletions(-) 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..24765dce 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 #262a3a; + border-radius: 10px; + --pulse-color: rgba(255, 255, 255, 0.08); position: relative; z-index: 1; overflow: visible; @@ -27,11 +29,24 @@ will-change: opacity; } + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px -8px var(--pulse-color); + } + &__icon { font-size: 20px; 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 +56,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 +109,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..7394ad05 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: #4a5568; +@color-gray-text: #94a3b8; diff --git a/ui/src/components/navigation/Navigation.jsx b/ui/src/components/navigation/Navigation.jsx index eda88e98..7fa41da6 100644 --- a/ui/src/components/navigation/Navigation.jsx +++ b/ui/src/components/navigation/Navigation.jsx @@ -42,24 +42,21 @@ export default function Navigation({ isAdmin }) { ]; if (isAdmin) { - const settingsItems = [ - { itemKey: '/users', text: 'User Management' }, - { itemKey: '/userSettings', text: 'User Specific Settings' }, - { itemKey: '/generalSettings', text: 'General Settings' }, - ]; - items.push({ itemKey: 'settings', text: 'Settings', icon: , - items: settingsItems, + items: [ + { itemKey: '/users', text: 'User Management' }, + { itemKey: '/generalSettings', text: 'Settings' }, + ], }); } else { items.push({ itemKey: 'settings', text: 'Settings', icon: , - items: [{ itemKey: '/userSettings', text: 'User Specific Settings' }], + items: [{ itemKey: '/generalSettings', text: 'Settings' }], }); } diff --git a/ui/src/views/dashboard/Dashboard.jsx b/ui/src/views/dashboard/Dashboard.jsx index 63ad72cd..de1f51ff 100644 --- a/ui/src/views/dashboard/Dashboard.jsx +++ b/ui/src/views/dashboard/Dashboard.jsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { Button, Col, Row, Toast } from '@douyinfe/semi-ui-19'; +import { Button, Col, Row, Toast, Typography } from '@douyinfe/semi-ui-19'; import { IconTerminal, IconStar, @@ -22,7 +22,6 @@ import KpiCard from '../../components/cards/KpiCard.jsx'; import PieChartCard from '../../components/cards/PieChartCard.jsx'; import './Dashboard.less'; -import { SegmentPart } from '../../components/segment/SegmentPart.jsx'; import { xhrPost } from '../../services/xhr.js'; import { format } from '../../services/time/timeService.js'; @@ -35,129 +34,119 @@ export default function Dashboard() { const kpis = dashboard?.kpis || { totalJobs: 0, totalListings: 0, providersUsed: 0 }; const pieData = dashboard?.pie || []; + const { Text } = Typography; return (
+ General - - - - - } - description="Time interval for job execution" - /> - - - } - description="Last execution timestamp" - /> - - - } - description="Next execution timestamp" - /> - - - } description="Run a search now"> - - - - - + + } + 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..20cc6912 100644 --- a/ui/src/views/dashboard/Dashboard.less +++ b/ui/src/views/dashboard/Dashboard.less @@ -3,31 +3,31 @@ 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: #1e2130; + border: 1px solid #262a3a; + 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..e73de625 100644 --- a/ui/src/views/generalSettings/GeneralSettings.jsx +++ b/ui/src/views/generalSettings/GeneralSettings.jsx @@ -3,31 +3,53 @@ * 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, + Divider, + 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 { SegmentPart } from '../../components/segment/SegmentPart'; -import { Banner, Toast } from '@douyinfe/semi-ui-19'; +import { xhrPost, xhrGet } from '../../services/xhr'; +import { 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 { Title, Text } = Typography; + +function SectionHeader({ title, description }) { + return ( +
+ + {title} + + {description && ( + + {description} + + )} +
+ ); +} + function formatFromTimestamp(ts) { const date = new Date(ts); return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`; @@ -63,6 +85,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 +116,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 +212,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 +223,272 @@ 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 && ( - -
- + + + + Execution + + } + itemKey="execution" > - `${value}`.replace(/\D/g, '')} - onChange={(value) => setInterval(value)} - suffix={'minutes'} - /> - - - -
- - + + `${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)); + }} + /> +
+ +
+ +
-
- - - `${value}`.replace(/\D/g, '')} - onChange={(value) => setPort(value)} - /> - - - + + + + System + + } + itemKey="system" > - Warning
} - style={{ marginBottom: '1rem' }} - description={ -
- Changing the path later may result in data loss. -
- You must restart Fredy immediately after changing this setting! -
- } - /> - - { - setSqlitePath(value); - }} - /> - - - + + `${value}`.replace(/\D/g, '')} + onChange={(value) => setPort(value)} + style={{ maxWidth: 160 }} + /> + + + + + + setSqlitePath(value)} + style={{ maxWidth: 480 }} + /> + + + + +
+ + + +
+ + + + + setAnalyticsEnabled(e.target.checked)}> + Enable analytics + + + + + + setDemoMode(e.target.checked)}> + Enable demo mode + + +
+ +
+
+ + + + + User Settings + + } + itemKey="userSettings" > -
- { - setWorkingHourFrom(val == null ? null : formatFromTimestamp(val)); - }} +
+ - { - setWorkingHourTo(val == null ? null : formatFromTimestamp(val)); - }} + setAddress(v)} + onSearch={searchAddress} + placeholder="Enter your home address" + style={{ width: '100%', maxWidth: 480 }} /> + {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 +
+ +
+ +
- - - - - 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. -
- } - /> - - 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. -
- } - /> - - 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'} + /> +
+
+
+ Date: Sun, 22 Mar 2026 13:46:55 +0100 Subject: [PATCH 2/7] improving dashboard and settings --- ui/src/components/cards/DashboardCard.less | 7 +- .../components/cards/DashboardCardColors.less | 2 +- ui/src/views/dashboard/Dashboard.less | 5 +- .../views/generalSettings/GeneralSettings.jsx | 120 +++++++++--------- 4 files changed, 65 insertions(+), 69 deletions(-) diff --git a/ui/src/components/cards/DashboardCard.less b/ui/src/components/cards/DashboardCard.less index 24765dce..1af2fd92 100644 --- a/ui/src/components/cards/DashboardCard.less +++ b/ui/src/components/cards/DashboardCard.less @@ -6,7 +6,7 @@ margin-bottom: 16px; transition: transform 0.2s, box-shadow 0.2s; background-color: #181b26; - border: 1px solid #262a3a; + border: 1px solid #232735; border-radius: 10px; --pulse-color: rgba(255, 255, 255, 0.08); position: relative; @@ -29,11 +29,6 @@ will-change: opacity; } - &:hover { - transform: translateY(-2px); - box-shadow: 0 8px 32px -8px var(--pulse-color); - } - &__icon { font-size: 20px; display: flex; diff --git a/ui/src/components/cards/DashboardCardColors.less b/ui/src/components/cards/DashboardCardColors.less index 7394ad05..4e8857cd 100644 --- a/ui/src/components/cards/DashboardCardColors.less +++ b/ui/src/components/cards/DashboardCardColors.less @@ -15,5 +15,5 @@ @color-purple-text: #a78bfa; @color-gray-bg: rgba(148, 163, 184, 0.10); -@color-gray-border: #4a5568; +@color-gray-border: #323a47; @color-gray-text: #94a3b8; diff --git a/ui/src/views/dashboard/Dashboard.less b/ui/src/views/dashboard/Dashboard.less index 20cc6912..7d7b22a5 100644 --- a/ui/src/views/dashboard/Dashboard.less +++ b/ui/src/views/dashboard/Dashboard.less @@ -20,8 +20,9 @@ } &__pie-wrapper { - background: #1e2130; - border: 1px solid #262a3a; + background: #23242a; + border: 1px solid #37404e; + border-radius: 10px; padding: 24px; max-height: 320px; diff --git a/ui/src/views/generalSettings/GeneralSettings.jsx b/ui/src/views/generalSettings/GeneralSettings.jsx index e73de625..0cabf61b 100644 --- a/ui/src/views/generalSettings/GeneralSettings.jsx +++ b/ui/src/views/generalSettings/GeneralSettings.jsx @@ -261,66 +261,6 @@ const GeneralSettings = function GeneralSettings() { {!loading && ( <> - - - Execution - - } - itemKey="execution" - > -
- - `${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)); - }} - /> -
- -
- -
-
-
- @@ -413,6 +353,66 @@ const GeneralSettings = function GeneralSettings() {
+ + + Execution + + } + itemKey="execution" + > +
+ + `${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)); + }} + /> +
+ +
+ +
+
+
+ From 76a6a28d05e8b225363a1807be65843cf7ccd5b0 Mon Sep 17 00:00:00 2001 From: orangecoding Date: Mon, 23 Mar 2026 12:26:28 +0100 Subject: [PATCH 3/7] improve job overview --- ui/src/components/grid/jobs/JobGrid.jsx | 109 +++++++++-------------- ui/src/components/grid/jobs/JobGrid.less | 45 ++++++---- 2 files changed, 71 insertions(+), 83 deletions(-) diff --git a/ui/src/components/grid/jobs/JobGrid.jsx b/ui/src/components/grid/jobs/JobGrid.jsx index 3b763c15..5f15f5cc 100644 --- a/ui/src/components/grid/jobs/JobGrid.jsx +++ b/ui/src/components/grid/jobs/JobGrid.jsx @@ -20,6 +20,8 @@ import { Pagination, Toast, Empty, + Radio, + RadioGroup, } from '@douyinfe/semi-ui-19'; import { IconAlertTriangle, @@ -31,8 +33,9 @@ import { IconBriefcase, IconBell, IconSearch, - IconFilter, IconPlusCircle, + IconArrowUp, + IconArrowDown, } 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) => ( - + Date: Mon, 23 Mar 2026 12:36:57 +0100 Subject: [PATCH 4/7] improving job card --- ui/src/components/grid/jobs/JobGrid.jsx | 114 +++++++++++------------ ui/src/components/grid/jobs/JobGrid.less | 109 ++++++++++++++++++---- 2 files changed, 142 insertions(+), 81 deletions(-) diff --git a/ui/src/components/grid/jobs/JobGrid.jsx b/ui/src/components/grid/jobs/JobGrid.jsx index 5f15f5cc..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, @@ -36,6 +35,7 @@ import { IconPlusCircle, IconArrowUp, IconArrowDown, + IconHome, } from '@douyinfe/semi-icons'; import { useNavigate } from 'react-router-dom'; import ListingDeletionModal from '../../ListingDeletionModal.jsx'; @@ -252,77 +252,69 @@ const JobGrid = () => { {(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 8309a47b..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,6 +14,92 @@ box-shadow: 0 0 15px -3px rgb(78 78 78 / 70%); background-color: rgba(36, 36, 36, 1); } + + &__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 16px; + } + + &__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; + 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; + } } &__topbar { @@ -50,32 +138,13 @@ } } - &__header { - display: flex; - align-items: center; - justify-content: space-between; - } - &__title { margin-bottom: 0 !important; } - &__infoItem { - display: flex; - align-items: center; - width: 100%; - - .semi-typography { - display: flex; - align-items: center; - gap: 4px; - } - } - &__actions { display: flex; - justify-content: space-between; - gap: 8px; + gap: 6px; } &__pagination { From 1f6b3af69baf02164cc297d11c8f8f46e1a1b96a Mon Sep 17 00:00:00 2001 From: orangecoding Date: Mon, 23 Mar 2026 12:44:31 +0100 Subject: [PATCH 5/7] improving grid view of listings+ --- .../components/grid/listings/ListingsGrid.jsx | 197 ++++++++---------- .../grid/listings/ListingsGrid.less | 64 ++++-- 2 files changed, 135 insertions(+), 126 deletions(-) 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()}>
- - + `${value}`.replace(/\D/g, '')} + onChange={(value) => setPort(value)} + style={{ maxWidth: 160 }} /> - -
- - - - - setAnalyticsEnabled(e.target.checked)}> - Enable analytics - - - + - - setDemoMode(e.target.checked)}> - Enable demo mode - + + + setSqlitePath(value)} + /> + + + + setAnalyticsEnabled(e.target.checked)}> + Enable analytics + + + + + setDemoMode(e.target.checked)}> + Enable demo mode + +
+ + + Backup & Restore + + } + itemKey="backup" + > +
+ +
+ + + +
+
+
+
+ @@ -423,54 +416,54 @@ const GeneralSettings = function GeneralSettings() { itemKey="userSettings" >
- - setAddress(v)} - onSearch={searchAddress} - placeholder="Enter your home address" - style={{ width: '100%', maxWidth: 480 }} - /> - {coords && coords.lat === -1 && ( + + 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.'); - } - }} + style={{ marginBottom: 12 }} /> - Fetch detailed ImmoScout listings -
+
+ { + try { + await actions.userSettings.setImmoscoutDetails(checked); + Toast.success('ImmoScout details setting updated.'); + } catch { + Toast.error('Failed to update setting.'); + } + }} + /> + Fetch detailed ImmoScout listings +
+